) => {
+ e.stopPropagation();
+ setAnchorEl(null);
+ };
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Arrows/ArrowLeft.tsx b/client/src/Components/v2/Arrows/ArrowLeft.tsx
new file mode 100644
index 000000000..01d1342c8
--- /dev/null
+++ b/client/src/Components/v2/Arrows/ArrowLeft.tsx
@@ -0,0 +1,36 @@
+import LeftArrow from "@/assets/icons/left-arrow.svg?react";
+import LeftArrowDouble from "@/assets/icons/left-arrow-double.svg?react";
+import LeftArrowLong from "@/assets/icons/left-arrow-long.svg?react";
+
+export const ArrowLeft = ({
+ type,
+ color = "#667085",
+ ...props
+}: {
+ type?: string;
+ color?: string | undefined;
+ [key: string]: any;
+}) => {
+ if (type === "double") {
+ return (
+
+ );
+ } else if (type === "long") {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+};
diff --git a/client/src/Components/v2/Arrows/ArrowRight.tsx b/client/src/Components/v2/Arrows/ArrowRight.tsx
new file mode 100644
index 000000000..c73165f16
--- /dev/null
+++ b/client/src/Components/v2/Arrows/ArrowRight.tsx
@@ -0,0 +1,28 @@
+import RightArrow from "@/assets/icons/right-arrow.svg?react";
+import RightArrowDouble from "@/assets/icons/right-arrow-double.svg?react";
+
+export const ArrowRight = ({
+ type,
+ color = "#667085",
+ ...props
+}: {
+ type?: string;
+ color?: string | undefined;
+ [key: string]: any;
+}) => {
+ if (type === "double") {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+};
diff --git a/client/src/Components/v2/Auth/AuthBasePage.tsx b/client/src/Components/v2/Auth/AuthBasePage.tsx
new file mode 100644
index 000000000..ba4230a6d
--- /dev/null
+++ b/client/src/Components/v2/Auth/AuthBasePage.tsx
@@ -0,0 +1,48 @@
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import { HeaderAuth } from "@/Components/v2/Auth";
+import Logo from "@/assets/icons/checkmate-icon.svg?react";
+
+import type { StackProps } from "@mui/material/Stack";
+import { useTheme } from "@mui/material/styles";
+import { Typography } from "@mui/material";
+
+interface AuthBasePageProps extends StackProps {
+ title?: string;
+ subtitle?: string;
+ children: React.ReactNode;
+}
+
+export const AuthBasePage: React.FC = ({
+ children,
+ title,
+ subtitle,
+ ...props
+}) => {
+ const theme = useTheme();
+ return (
+
+
+
+
+
+
+ {title}
+ {subtitle}
+ {children}
+
+
+ );
+};
diff --git a/client/src/Components/v2/Auth/HeaderAuth.jsx b/client/src/Components/v2/Auth/HeaderAuth.jsx
new file mode 100644
index 000000000..4e437cd7e
--- /dev/null
+++ b/client/src/Components/v2/Auth/HeaderAuth.jsx
@@ -0,0 +1,26 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import Logo from "@/assets/icons/checkmate-icon.svg?react";
+
+import { useTheme } from "@mui/material/styles";
+import { useTranslation } from "react-i18next";
+import { LanguageSelector, ThemeSwitch } from "@/Components/v2/Inputs";
+
+export const HeaderAuth = () => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Auth/index.jsx b/client/src/Components/v2/Auth/index.jsx
new file mode 100644
index 000000000..84b742113
--- /dev/null
+++ b/client/src/Components/v2/Auth/index.jsx
@@ -0,0 +1,2 @@
+export { HeaderAuth } from "./HeaderAuth";
+export { AuthBasePage } from "./AuthBasePage";
diff --git a/client/src/Components/v2/DesignElements/BaseBox.tsx b/client/src/Components/v2/DesignElements/BaseBox.tsx
new file mode 100644
index 000000000..14c4af1c4
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/BaseBox.tsx
@@ -0,0 +1,23 @@
+import Box from "@mui/material/Box";
+import { useTheme } from "@mui/material/styles";
+import type { SxProps } from "@mui/material/styles";
+
+type BaseBoxProps = React.PropsWithChildren<{ sx?: SxProps }>;
+
+export const BaseBox: React.FC = ({ children, sx }) => {
+ const theme = useTheme();
+ return (
+
+ {children}
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/BasePage.tsx b/client/src/Components/v2/DesignElements/BasePage.tsx
new file mode 100644
index 000000000..f458cbb0a
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/BasePage.tsx
@@ -0,0 +1,80 @@
+import Stack from "@mui/material/Stack";
+import { ErrorFallback, EmptyFallback } from "./Fallback";
+
+import type { StackProps } from "@mui/material/Stack";
+import { useTheme } from "@mui/material/styles";
+import { useTranslation } from "react-i18next";
+interface BasePageProps extends StackProps {
+ children: React.ReactNode;
+}
+
+export const BasePage: React.FC = ({
+ children,
+ ...props
+}: {
+ children: React.ReactNode;
+}) => {
+ const theme = useTheme();
+ return (
+
+ {children}
+
+ );
+};
+
+interface BasePageWithStatesProps extends StackProps {
+ loading: boolean;
+ error: any;
+ items: any[];
+ page: string;
+ actionLink?: string;
+ children: React.ReactNode;
+}
+
+const isEmpty = (items: any[]) => {
+ if (!items) return true;
+ if (Array.isArray(items) && items.length === 0) return true;
+ return false;
+};
+
+export const BasePageWithStates: React.FC = ({
+ loading,
+ error,
+ items,
+ page,
+ actionLink,
+ children,
+ ...props
+}: BasePageWithStatesProps) => {
+ const { t } = useTranslation();
+
+ if (loading) {
+ return null;
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (isEmpty(items)) {
+ return (
+
+ );
+ }
+
+ return {children};
+};
diff --git a/client/src/Components/v2/DesignElements/BulletPointCheck.tsx b/client/src/Components/v2/DesignElements/BulletPointCheck.tsx
new file mode 100644
index 000000000..03147e011
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/BulletPointCheck.tsx
@@ -0,0 +1,45 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import CheckOutlined from "@/assets/icons/check-outlined.svg?react";
+import { useTheme } from "@mui/material/styles";
+
+export const BulletPointCheck = ({
+ text,
+ variant = "info",
+}: {
+ text: string;
+ noHighlightText?: string;
+ variant?: "success" | "error" | "info";
+}) => {
+ const theme = useTheme();
+ const colors: Record = {
+ success: theme.palette.success.main,
+ error: theme.palette.error.main,
+ info: theme.palette.primary.contrastTextSecondary,
+ };
+
+ return (
+
+
+
+ {text}
+
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/Dot.tsx b/client/src/Components/v2/DesignElements/Dot.tsx
new file mode 100644
index 000000000..ca3884cfb
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/Dot.tsx
@@ -0,0 +1,23 @@
+export const Dot = ({
+ color = "gray",
+ size = "4px",
+ style,
+}: {
+ color?: string;
+ size?: string;
+ style?: React.CSSProperties;
+}) => {
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/Fallback.tsx b/client/src/Components/v2/DesignElements/Fallback.tsx
new file mode 100644
index 000000000..f7b48a113
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/Fallback.tsx
@@ -0,0 +1,158 @@
+import Box from "@mui/material/Box";
+import Stack from "@mui/material/Stack";
+import OutputAnimation from "@/assets/Animations/output.gif";
+import DarkmodeOutput from "@/assets/Animations/darkmodeOutput.gif";
+import Typography from "@mui/material/Typography";
+import { BulletPointCheck } from "@/Components/v2/DesignElements";
+import { Button } from "@/Components/v2/Inputs";
+
+import { useNavigate } from "react-router";
+import { useMediaQuery } from "@mui/material";
+import { useTheme } from "@mui/material/styles";
+import { useSelector } from "react-redux";
+
+import type { BoxProps } from "@mui/material";
+
+interface BaseFallbackProps extends BoxProps {
+ children: React.ReactNode;
+}
+
+export const BaseFallback: React.FC = ({ children, ...props }) => {
+ const theme = useTheme();
+ const mode = useSelector((state: any) => state.ui.mode);
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+
+ return (
+
+
+
+
+
+ {children}
+
+
+
+ );
+};
+
+export const ErrorFallback = ({
+ title,
+ subtitle,
+}: {
+ title: string;
+ subtitle: string;
+}) => {
+ const theme = useTheme();
+ return (
+
+
+ {title}
+
+ {subtitle}
+
+ );
+};
+
+export const EmptyFallback = ({
+ page,
+ title,
+ bullets,
+ actionButtonText,
+ actionLink,
+}: {
+ page: string;
+ title: string;
+ bullets: any;
+ actionButtonText: string;
+ actionLink: string;
+}) => {
+ const theme = useTheme();
+ const navigate = useNavigate();
+ return (
+
+
+
+ {title}
+
+
+ {bullets?.map((bullet: string, index: number) => (
+
+ ))}
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/PulseDot.tsx b/client/src/Components/v2/DesignElements/PulseDot.tsx
new file mode 100644
index 000000000..a785b5a0d
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/PulseDot.tsx
@@ -0,0 +1,44 @@
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import { useTheme } from "@mui/material/styles";
+export const PulseDot = ({ color }: { color: string }) => {
+ const theme = useTheme();
+ return (
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/SplitBox.tsx b/client/src/Components/v2/DesignElements/SplitBox.tsx
new file mode 100644
index 000000000..f5a09d8f5
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/SplitBox.tsx
@@ -0,0 +1,68 @@
+import { Fragment } from "react";
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import { useTheme } from "@mui/material/styles";
+import { useMediaQuery } from "@mui/material";
+export const SplitBox = ({
+ left,
+ right,
+}: {
+ left: React.ReactNode;
+ right: React.ReactNode;
+}) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ return (
+
+
+ {left}
+
+
+ {right}
+
+
+ );
+};
+
+export const ConfigBox = ({
+ title,
+ subtitle,
+ rightContent,
+}: {
+ title: string;
+ subtitle: string;
+ rightContent: React.ReactNode;
+}) => {
+ return (
+
+
+ {title}
+
+ {subtitle}
+
+ }
+ right={rightContent}
+ />
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/StatBox.tsx b/client/src/Components/v2/DesignElements/StatBox.tsx
new file mode 100644
index 000000000..3450444b2
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/StatBox.tsx
@@ -0,0 +1,57 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { useTheme } from "@mui/material/styles";
+import { useMediaQuery } from "@mui/material";
+import type { PaletteKey } from "@/Utils/Theme/v2/theme";
+import { BaseBox } from "@/Components/v2/DesignElements";
+
+type GradientBox = React.PropsWithChildren<{ palette?: PaletteKey }>;
+
+export const GradientBox: React.FC = ({ children, palette }) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ const bg = palette
+ ? `linear-gradient(to bottom right, ${theme.palette[palette].main} 30%, ${theme.palette[palette].lowContrast} 70%)`
+ : `linear-gradient(340deg, ${theme.palette.tertiary.main} 10%, ${theme.palette.primary.main} 45%)`;
+
+ return (
+
+ {children}
+
+ );
+};
+
+type StatBoxProps = React.PropsWithChildren<{
+ title: string;
+ subtitle: string;
+ palette?: PaletteKey;
+}>;
+
+export const StatBox: React.FC = ({
+ title,
+ subtitle,
+ palette,
+ children,
+}) => {
+ const theme = useTheme();
+ const textColor = palette ? theme.palette[palette].contrastText : "inherit";
+
+ return (
+
+
+ {title}
+ {subtitle}
+ {children}
+
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/StatusBox.tsx b/client/src/Components/v2/DesignElements/StatusBox.tsx
new file mode 100644
index 000000000..8cd5cfbc1
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/StatusBox.tsx
@@ -0,0 +1,100 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import Box from "@mui/material/Box";
+import { BaseBox } from "@/Components/v2/DesignElements";
+import Background from "@/assets/Images/background-grid.svg?react";
+import { useTranslation } from "react-i18next";
+
+import { useTheme } from "@mui/material/styles";
+
+type StatusBoxProps = React.PropsWithChildren<{}>;
+
+export const BGBox: React.FC = ({ children }) => {
+ const theme = useTheme();
+ return (
+
+
+
+
+ {children}
+
+ );
+};
+
+const StatusBox = ({
+ label,
+ n,
+ color,
+}: {
+ label: string;
+ n: number;
+ color: string | undefined;
+}) => {
+ const theme = useTheme();
+ return (
+
+
+
+ {label}
+
+
+ {n}
+
+
+
+ );
+};
+
+export const UpStatusBox = ({ n }: { n: number }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+
+ );
+};
+
+export const DownStatusBox = ({ n }: { n: number }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+
+ );
+};
+
+export const PausedStatusBox = ({ n }: { n: number }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/StatusLabel.tsx b/client/src/Components/v2/DesignElements/StatusLabel.tsx
new file mode 100644
index 000000000..a314c09b0
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/StatusLabel.tsx
@@ -0,0 +1,41 @@
+import Box from "@mui/material/Box";
+import { BaseBox } from "@/Components/v2/DesignElements";
+import type { MonitorStatus } from "@/Types/Monitor";
+
+import { getStatusPalette } from "@/Utils/MonitorUtils";
+import { useTheme } from "@mui/material/styles";
+
+export const StatusLabel = ({
+ status,
+ isActive,
+}: {
+ status: MonitorStatus;
+ isActive?: boolean;
+}) => {
+ const theme = useTheme();
+ const palette = getStatusPalette(status);
+ const transformedText = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
+
+ return (
+
+
+ {isActive === false ? "Paused" : transformedText}
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/Table.tsx b/client/src/Components/v2/DesignElements/Table.tsx
new file mode 100644
index 000000000..bf75fd9bf
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/Table.tsx
@@ -0,0 +1,215 @@
+import Paper from "@mui/material/Paper";
+import Table from "@mui/material/Table";
+import TableBody from "@mui/material/TableBody";
+import TableCell from "@mui/material/TableCell";
+import TableContainer from "@mui/material/TableContainer";
+import TableHead from "@mui/material/TableHead";
+import TableRow from "@mui/material/TableRow";
+
+import IconButton from "@mui/material/IconButton";
+import LastPageIcon from "@mui/icons-material/LastPage";
+import FirstPageIcon from "@mui/icons-material/FirstPage";
+import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft";
+import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
+
+import Box from "@mui/material/Box";
+import TablePagination from "@mui/material/TablePagination";
+import type { TablePaginationProps } from "@mui/material/TablePagination";
+
+import { useTheme } from "@mui/material/styles";
+import { useMediaQuery } from "@mui/material";
+export type Header = {
+ id: number | string;
+ content: React.ReactNode;
+ onClick?: (event: React.MouseEvent, row: T) => void;
+ render: (row: T) => React.ReactNode;
+};
+
+type DataTableProps = {
+ headers: Header[];
+ data: T[];
+ onRowClick?: (row: T) => void;
+};
+
+export function DataTable<
+ T extends {
+ id?: string | number;
+ _id?: string | number;
+ onRowClick?: (row: T) => void;
+ },
+>({ headers, data, onRowClick }: DataTableProps) {
+ const theme = useTheme();
+ if (data.length === 0 || headers.length === 0) return No data
;
+ return (
+
+
+
+
+ {headers.map((header, idx) => {
+ return (
+
+ {header.content}
+
+ );
+ })}
+
+
+
+ {data.map((row) => {
+ const key = row.id || row._id || Math.random();
+
+ return (
+ (onRowClick ? onRowClick(row) : null)}
+ >
+ {headers.map((header, index) => {
+ return (
+ header.onClick!(e, row) : undefined
+ }
+ >
+ {header.render(row)}
+
+ );
+ })}
+
+ );
+ })}
+
+
+
+ );
+}
+
+interface TablePaginationActionsProps {
+ count: number;
+ page: number;
+ rowsPerPage: number;
+ onPageChange: (event: React.MouseEvent, newPage: number) => void;
+}
+
+function TablePaginationActions(props: TablePaginationActionsProps) {
+ const theme = useTheme();
+ const { count, page, rowsPerPage, onPageChange } = props;
+
+ const handleFirstPageButtonClick = (event: React.MouseEvent) => {
+ onPageChange(event, 0);
+ };
+
+ const handleBackButtonClick = (event: React.MouseEvent) => {
+ onPageChange(event, page - 1);
+ };
+
+ const handleNextButtonClick = (event: React.MouseEvent) => {
+ onPageChange(event, page + 1);
+ };
+
+ const handleLastPageButtonClick = (event: React.MouseEvent) => {
+ onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
+ };
+
+ return (
+
+
+ {theme.direction === "rtl" ? : }
+
+
+ {theme.direction === "rtl" ? : }
+
+ = Math.ceil(count / rowsPerPage) - 1}
+ aria-label="next page"
+ >
+ {theme.direction === "rtl" ? : }
+
+ = Math.ceil(count / rowsPerPage) - 1}
+ aria-label="last page"
+ >
+ {theme.direction === "rtl" ? : }
+
+
+ );
+}
+
+export const Pagination: React.FC = ({ ...props }) => {
+ const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm"));
+ const theme = useTheme();
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/index.tsx b/client/src/Components/v2/DesignElements/index.tsx
new file mode 100644
index 000000000..3574daffb
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/index.tsx
@@ -0,0 +1,9 @@
+export { SplitBox as HorizontalSplitBox, ConfigBox } from "./SplitBox";
+export { BasePage, BasePageWithStates } from "./BasePage";
+export { BGBox, UpStatusBox, DownStatusBox, PausedStatusBox } from "./StatusBox";
+export { DataTable as Table, Pagination } from "./Table";
+export { GradientBox, StatBox } from "./StatBox";
+export { BaseBox } from "./BaseBox";
+export { StatusLabel } from "./StatusLabel";
+export { BaseFallback, ErrorFallback, EmptyFallback } from "./Fallback";
+export { BulletPointCheck } from "./BulletPointCheck";
diff --git a/client/src/Components/v2/Inputs/AutoComplete.tsx b/client/src/Components/v2/Inputs/AutoComplete.tsx
new file mode 100644
index 000000000..73afa365a
--- /dev/null
+++ b/client/src/Components/v2/Inputs/AutoComplete.tsx
@@ -0,0 +1,48 @@
+import Autocomplete from "@mui/material/Autocomplete";
+import type { AutocompleteProps } from "@mui/material/Autocomplete";
+import { TextInput } from "@/Components/v2/Inputs/TextInput";
+import { CheckboxInput } from "@/Components/v2/Inputs/Checkbox";
+import ListItem from "@mui/material/ListItem";
+import { useTheme } from "@mui/material/styles";
+
+type AutoCompleteInputProps = Omit<
+ AutocompleteProps,
+ "renderInput"
+> & {
+ renderInput?: AutocompleteProps["renderInput"];
+};
+
+export const AutoCompleteInput: React.FC = ({ ...props }) => {
+ const theme = useTheme();
+ return (
+ (
+
+ )}
+ getOptionKey={(option) => option._id}
+ renderTags={() => null}
+ renderOption={(props, option, { selected }) => {
+ const { key, ...optionProps } = props;
+ return (
+
+
+ {option.name}
+
+ );
+ }}
+ sx={{
+ "&.MuiAutocomplete-root .MuiAutocomplete-input": {
+ padding: `0 ${theme.spacing(5)}`,
+ },
+ }}
+ />
+ );
+};
diff --git a/client/src/Components/v2/Inputs/Button.tsx b/client/src/Components/v2/Inputs/Button.tsx
new file mode 100644
index 000000000..2f28e8eda
--- /dev/null
+++ b/client/src/Components/v2/Inputs/Button.tsx
@@ -0,0 +1,11 @@
+import Button from "@mui/material/Button";
+import type { ButtonProps } from "@mui/material/Button";
+
+export const ButtonInput: React.FC = ({ sx, ...props }) => {
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/Inputs/ButtonGroup.tsx b/client/src/Components/v2/Inputs/ButtonGroup.tsx
new file mode 100644
index 000000000..3d8acd8cc
--- /dev/null
+++ b/client/src/Components/v2/Inputs/ButtonGroup.tsx
@@ -0,0 +1,13 @@
+import ButtonGroup from "@mui/material/ButtonGroup";
+import type { ButtonGroupProps } from "@mui/material/ButtonGroup";
+export const ButtonGroupInput: React.FC = ({
+ orientation,
+ ...props
+}) => {
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/Inputs/Checkbox.tsx b/client/src/Components/v2/Inputs/Checkbox.tsx
new file mode 100644
index 000000000..bc2cedd03
--- /dev/null
+++ b/client/src/Components/v2/Inputs/Checkbox.tsx
@@ -0,0 +1,23 @@
+import Checkbox from "@mui/material/Checkbox";
+import type { CheckboxProps } from "@mui/material/Checkbox";
+import CheckboxOutline from "@/assets/icons/checkbox-outline.svg?react";
+import CheckboxFilled from "@/assets/icons/checkbox-filled.svg?react";
+import { useTheme } from "@mui/material/styles";
+type CheckboxInputProps = CheckboxProps & {
+ label?: string;
+};
+
+export const CheckboxInput: React.FC = ({ label, ...props }) => {
+ const theme = useTheme();
+ return (
+ }
+ checkedIcon={}
+ sx={{
+ "&:hover": { backgroundColor: "transparent" },
+ "& svg": { width: theme.spacing(8), height: theme.spacing(8) },
+ }}
+ />
+ );
+};
diff --git a/client/src/Components/v2/Inputs/LanguageSelector.tsx b/client/src/Components/v2/Inputs/LanguageSelector.tsx
new file mode 100644
index 000000000..f7953c873
--- /dev/null
+++ b/client/src/Components/v2/Inputs/LanguageSelector.tsx
@@ -0,0 +1,64 @@
+import "flag-icons/css/flag-icons.min.css";
+import { Select } from "@/Components/v2/Inputs";
+import MenuItem from "@mui/material/MenuItem";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+
+import { useTranslation } from "react-i18next";
+import { useTheme } from "@mui/material/styles";
+import { useDispatch } from "react-redux";
+import { useSelector } from "react-redux";
+import { setLanguage } from "@/Features/UI/uiSlice";
+import type { SelectChangeEvent } from "@mui/material/Select";
+
+export const LanguageSelector = () => {
+ const { i18n } = useTranslation();
+ const theme = useTheme();
+ const dispatch = useDispatch();
+ const language = useSelector((state: any) => state.ui.language);
+ const languages = Object.keys(i18n.options.resources || {});
+ const languageMap: Record = {
+ cs: "cz",
+ ja: "jp",
+ uk: "ua",
+ vi: "vn",
+ };
+
+ const handleChange = (event: SelectChangeEvent) => {
+ const newLang = event.target.value;
+ dispatch(setLanguage(newLang));
+ };
+
+ const languagesForDisplay = languages.map((l) => {
+ let formattedLanguage = l === "en" ? "gb" : l;
+ formattedLanguage = formattedLanguage.includes("-")
+ ? formattedLanguage.split("-")[1].toLowerCase()
+ : formattedLanguage;
+ formattedLanguage = languageMap[formattedLanguage] || formattedLanguage;
+ const flag = formattedLanguage ? `fi fi-${formattedLanguage}` : null;
+
+ return (
+
+ );
+ });
+
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/Inputs/RadioInput.tsx b/client/src/Components/v2/Inputs/RadioInput.tsx
new file mode 100644
index 000000000..d903a7b58
--- /dev/null
+++ b/client/src/Components/v2/Inputs/RadioInput.tsx
@@ -0,0 +1,66 @@
+import Radio from "@mui/material/Radio";
+import type { RadioProps } from "@mui/material/Radio";
+import { useTheme } from "@mui/material/styles";
+import RadioChecked from "@/assets/icons/radio-checked.svg?react";
+import FormControlLabel from "@mui/material/FormControlLabel";
+import Typography from "@mui/material/Typography";
+
+interface RadioInputProps extends RadioProps {}
+
+export const RadioInput: React.FC = ({ ...props }) => {
+ const theme = useTheme();
+ return (
+ }
+ sx={{
+ color: "transparent",
+ boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`,
+ "&:not(.Mui-checked)": {
+ boxShadow: `inset 0 0 0 1px ${theme.palette.primary.contrastText}70`, // Use theme text color for the outline
+ },
+ mt: theme.spacing(0.5),
+ padding: 0,
+ "& .MuiSvgIcon-root": {
+ fontSize: 16,
+ },
+ }}
+ />
+ );
+};
+
+export const RadioWithDescription: React.FC<
+ RadioInputProps & { label: string; description: string }
+> = ({ label, description, ...props }) => {
+ const theme = useTheme();
+ return (
+ }
+ label={
+ <>
+ {label}
+
+ {description}
+
+ >
+ }
+ sx={{
+ alignItems: "flex-start",
+ p: theme.spacing(2.5),
+ m: theme.spacing(-2.5),
+ borderRadius: theme.shape.borderRadius,
+
+ "&:hover": {
+ backgroundColor: theme.palette.tertiary.main,
+ },
+ "& .MuiButtonBase-root": {
+ p: 0,
+ mr: theme.spacing(6),
+ },
+ }}
+ />
+ );
+};
diff --git a/client/src/Components/v2/Inputs/Select.tsx b/client/src/Components/v2/Inputs/Select.tsx
new file mode 100644
index 000000000..7d47286da
--- /dev/null
+++ b/client/src/Components/v2/Inputs/Select.tsx
@@ -0,0 +1,22 @@
+import Select from "@mui/material/Select";
+import type { SelectProps } from "@mui/material/Select";
+import { useTheme } from "@mui/material/styles";
+
+export const SelectInput: React.FC = ({ ...props }) => {
+ const theme = useTheme();
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/Inputs/TextInput.tsx b/client/src/Components/v2/Inputs/TextInput.tsx
new file mode 100644
index 000000000..4cd792059
--- /dev/null
+++ b/client/src/Components/v2/Inputs/TextInput.tsx
@@ -0,0 +1,22 @@
+import { forwardRef } from "react";
+import TextField from "@mui/material/TextField";
+import type { TextFieldProps } from "@mui/material";
+import { typographyLevels } from "@/Utils/Theme/v2/palette";
+
+export const TextInput = forwardRef(
+ function TextInput(props, ref) {
+ return (
+
+ );
+ }
+);
+TextInput.displayName = "TextInput";
diff --git a/client/src/Components/v2/Inputs/TextLink.tsx b/client/src/Components/v2/Inputs/TextLink.tsx
new file mode 100644
index 000000000..78525f837
--- /dev/null
+++ b/client/src/Components/v2/Inputs/TextLink.tsx
@@ -0,0 +1,37 @@
+import Typography from "@mui/material/Typography";
+import Stack from "@mui/material/Stack";
+import Link from "@mui/material/Link";
+import { Link as RouterLink } from "react-router-dom";
+
+import { useTheme } from "@mui/material/styles";
+
+export const TextLink = ({
+ text,
+ linkText,
+ href,
+ target = "_self",
+}: {
+ text: string;
+ linkText: string;
+ href: string;
+ target?: string;
+}) => {
+ const theme = useTheme();
+
+ return (
+
+ {text}
+
+ {linkText}
+
+
+ );
+};
diff --git a/client/src/Components/v2/Inputs/ThemeSwitch.tsx b/client/src/Components/v2/Inputs/ThemeSwitch.tsx
new file mode 100644
index 000000000..fb4bb6d81
--- /dev/null
+++ b/client/src/Components/v2/Inputs/ThemeSwitch.tsx
@@ -0,0 +1,135 @@
+import { useTheme } from "@mui/material/styles";
+import { useDispatch, useSelector } from "react-redux";
+import { setMode } from "@/Features/UI/uiSlice.js";
+import { useTranslation } from "react-i18next";
+import IconButton from "@mui/material/IconButton";
+
+const SunAndMoonIcon = () => {
+ const theme = useTheme();
+
+ return (
+
+ );
+};
+
+export const ThemeSwitch = ({
+ width = 48,
+ height = 48,
+}: {
+ width?: number;
+ height?: number;
+}) => {
+ const mode = useSelector((state: any) => state.ui.mode);
+ const dispatch = useDispatch();
+ const { t } = useTranslation();
+
+ const handleChange = () => {
+ dispatch(setMode(mode === "light" ? "dark" : "light"));
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Inputs/index.tsx b/client/src/Components/v2/Inputs/index.tsx
new file mode 100644
index 000000000..ccf17f630
--- /dev/null
+++ b/client/src/Components/v2/Inputs/index.tsx
@@ -0,0 +1,7 @@
+export { ButtonInput as Button } from "./Button";
+export { ButtonGroupInput as ButtonGroup } from "./ButtonGroup";
+export { TextInput } from "./TextInput";
+export { SelectInput as Select } from "./Select";
+export { LanguageSelector } from "./LanguageSelector";
+export { ThemeSwitch } from "./ThemeSwitch";
+export { TextLink } from "./TextLink";
diff --git a/client/src/Components/v2/Layouts/RootLayout.tsx b/client/src/Components/v2/Layouts/RootLayout.tsx
new file mode 100644
index 000000000..4237494b1
--- /dev/null
+++ b/client/src/Components/v2/Layouts/RootLayout.tsx
@@ -0,0 +1,23 @@
+import { Outlet } from "react-router";
+import Stack from "@mui/material/Stack";
+import { SideBar } from "@/Components/v2/Layouts/Sidebar";
+import { useTheme } from "@mui/material/styles";
+const RootLayout = () => {
+ const theme = useTheme();
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default RootLayout;
diff --git a/client/src/Components/v2/Layouts/Sidebar/BottomControls.tsx b/client/src/Components/v2/Layouts/Sidebar/BottomControls.tsx
new file mode 100644
index 000000000..a07ccde6c
--- /dev/null
+++ b/client/src/Components/v2/Layouts/Sidebar/BottomControls.tsx
@@ -0,0 +1,16 @@
+import { useTheme } from "@mui/material/styles";
+
+import Stack from "@mui/material/Stack";
+
+export const BottomControls = ({}) => {
+ const theme = useTheme();
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/Layouts/Sidebar/CollapseButton.tsx b/client/src/Components/v2/Layouts/Sidebar/CollapseButton.tsx
new file mode 100644
index 000000000..564c41591
--- /dev/null
+++ b/client/src/Components/v2/Layouts/Sidebar/CollapseButton.tsx
@@ -0,0 +1,50 @@
+import IconButton from "@mui/material/IconButton";
+import { ArrowRight } from "@/Components/v2/Arrows/ArrowRight";
+import { ArrowLeft } from "@/Components/v2/Arrows/ArrowLeft";
+import { useTheme } from "@mui/material/styles";
+import { useDispatch } from "react-redux";
+import { toggleSidebar } from "../../../../Features/UI/uiSlice.js";
+
+export const CollapseButton = ({ collapsed }: { collapsed: boolean }) => {
+ const theme = useTheme();
+ const dispatch = useDispatch();
+ const arrowIcon = collapsed ? (
+
+ ) : (
+
+ );
+
+ return (
+ {
+ dispatch(toggleSidebar());
+ }}
+ >
+ {arrowIcon}
+
+ );
+};
diff --git a/client/src/Components/v2/Layouts/Sidebar/Logo.tsx b/client/src/Components/v2/Layouts/Sidebar/Logo.tsx
new file mode 100644
index 000000000..6d190b99d
--- /dev/null
+++ b/client/src/Components/v2/Layouts/Sidebar/Logo.tsx
@@ -0,0 +1,59 @@
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import { useTheme } from "@mui/material/styles";
+import { useNavigate } from "react-router";
+import { useTranslation } from "react-i18next";
+
+export const Logo = ({ collapsed }: { collapsed: boolean }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const navigate = useNavigate();
+
+ return (
+ navigate("/")}
+ sx={{ cursor: "pointer" }}
+ >
+
+ C
+
+
+ {" "}
+
+ {t("common.appName")}
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Layouts/Sidebar/Menu.tsx b/client/src/Components/v2/Layouts/Sidebar/Menu.tsx
new file mode 100644
index 000000000..f0f99f33a
--- /dev/null
+++ b/client/src/Components/v2/Layouts/Sidebar/Menu.tsx
@@ -0,0 +1,58 @@
+// import Notifications from "@/assets/icons/notifications.svg?react";
+import Monitors from "@/assets/icons/monitors.svg?react";
+// import PageSpeed from "@/assets/icons/page-speed.svg?react";
+// import Integrations from "@/assets/icons/integrations.svg?react";
+// import Incidents from "@/assets/icons/incidents.svg?react";
+// import StatusPages from "@/assets/icons/status-pages.svg?react";
+// import Maintenance from "@/assets/icons/maintenance.svg?react";
+// import Logs from "@/assets/icons/logs.svg?react";
+// import Settings from "@/assets/icons/settings.svg?react";
+import Support from "@/assets/icons/support.svg?react";
+import Discussions from "@/assets/icons/discussions.svg?react";
+import Docs from "@/assets/icons/docs.svg?react";
+import ChangeLog from "@/assets/icons/changeLog.svg?react";
+
+export const getMenu = (t: Function) => [
+ { name: t("menu.uptime"), path: "v2/uptime", icon: },
+ // { name: t("menu.pagespeed"), path: "pagespeed", icon: },
+
+ // { name: t("menu.infrastructure"), path: "infrastructure", icon: },
+ // {
+ // name: t("menu.notifications"),
+ // path: "notifications",
+ // icon: ,
+ // },
+ // { name: t("menu.incidents"), path: "incidents", icon: },
+
+ // { name: t("menu.statusPages"), path: "status", icon: },
+ // { name: t("menu.maintenance"), path: "maintenance", icon: },
+ // { name: t("menu.logs"), path: "logs", icon: },
+
+ // {
+ // name: t("menu.settings"),
+ // icon: ,
+ // path: "settings",
+ // },
+];
+
+export const getBottomMenu = (t: Function) => [
+ { name: t("menu.support"), path: "support", icon: , url: "invite" },
+ {
+ name: t("menu.discussions"),
+ path: "discussions",
+ icon: ,
+ url: "https://github.com/bluewave-labs/checkmate/discussions",
+ },
+ {
+ name: t("menu.docs"),
+ path: "docs",
+ icon: ,
+ url: "https://bluewavelabs.gitbook.io/checkmate",
+ },
+ {
+ name: t("menu.changelog"),
+ path: "changelog",
+ icon: ,
+ url: "https://github.com/bluewave-labs/checkmate/releases",
+ },
+];
diff --git a/client/src/Components/v2/Layouts/Sidebar/NavItem.tsx b/client/src/Components/v2/Layouts/Sidebar/NavItem.tsx
new file mode 100644
index 000000000..8f7f5d68c
--- /dev/null
+++ b/client/src/Components/v2/Layouts/Sidebar/NavItem.tsx
@@ -0,0 +1,103 @@
+import Tooltip from "@mui/material/Tooltip";
+import ListItemButton from "@mui/material/ListItemButton";
+import ListItemIcon from "@mui/material/ListItemIcon";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import { useTheme } from "@mui/material/styles";
+
+export interface NavData {
+ name: string;
+ icon: JSX.Element;
+}
+
+export const NavItem = ({
+ item,
+ collapsed,
+ selected,
+ onClick,
+}: {
+ item: NavData;
+ collapsed: boolean;
+ selected: boolean;
+ onClick: (event: React.MouseEvent) => void;
+}) => {
+ const theme = useTheme();
+ const iconStroke = selected
+ ? theme.palette.primary.contrastText
+ : theme.palette.primary.contrastTextTertiary;
+
+ const buttonBgColor = selected ? theme.palette.secondary.main : "transparent";
+ const buttonBgHoverColor = selected
+ ? theme.palette.secondary.main
+ : theme.palette.tertiary.main;
+ const fontWeight = selected ? 600 : 400;
+ return (
+
+
+
+ {item.icon}
+
+
+
+ {item.name}
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Layouts/Sidebar/index.tsx b/client/src/Components/v2/Layouts/Sidebar/index.tsx
new file mode 100644
index 000000000..a17b9fa13
--- /dev/null
+++ b/client/src/Components/v2/Layouts/Sidebar/index.tsx
@@ -0,0 +1,106 @@
+import { useEffect } from "react";
+import { useSelector, useDispatch } from "react-redux";
+import { setCollapsed } from "@/Features/UI/uiSlice";
+import useMediaQuery from "@mui/material/useMediaQuery";
+import { useTheme } from "@mui/material/styles";
+import { useTranslation } from "react-i18next";
+import { useNavigate, useLocation } from "react-router-dom";
+
+import { CollapseButton } from "@/Components/v2/Layouts/Sidebar/CollapseButton";
+import Stack from "@mui/material/Stack";
+import List from "@mui/material/List";
+import Divider from "@mui/material/Divider";
+import { Logo } from "@/Components/v2/Layouts/Sidebar/Logo";
+import { getMenu, getBottomMenu } from "@/Components/v2/Layouts/Sidebar/Menu";
+import { NavItem } from "@/Components/v2/Layouts/Sidebar/NavItem";
+import { BottomControls } from "@/Components/v2/Layouts/Sidebar/BottomControls";
+
+export const COLLAPSED_WIDTH = 64;
+export const EXPANDED_WIDTH = 250;
+
+export const SideBar = () => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ const dispatch = useDispatch();
+ const collapsed = useSelector((state: any) => state.ui.sidebar.collapsed);
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const menu = getMenu(t);
+ const bottomMenu = getBottomMenu(t);
+
+ useEffect(() => {
+ dispatch(setCollapsed({ collapsed: isSmall }));
+ }, [isSmall]);
+
+ return (
+
+
+
+
+ {menu.map((item) => {
+ const selected = location.pathname.startsWith(`/${item.path}`);
+ return (
+ navigate(`/${item.path}`)}
+ />
+ );
+ })}
+
+
+ {bottomMenu.map((item) => {
+ const selected = location.pathname.startsWith(`/${item.path}`);
+
+ return (
+ {
+ if (item.url) {
+ window.open(item.url, "_blank", "noreferrer");
+ } else {
+ navigate(`/${item.path}`);
+ }
+ }}
+ />
+ );
+ })}
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/ChartAvgResponse.tsx b/client/src/Components/v2/Monitors/ChartAvgResponse.tsx
new file mode 100644
index 000000000..10938f359
--- /dev/null
+++ b/client/src/Components/v2/Monitors/ChartAvgResponse.tsx
@@ -0,0 +1,84 @@
+import { BaseChart } from "./HistogramStatus";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import AverageResponseIcon from "@/assets/icons/average-response-icon.svg?react";
+import { Cell, RadialBarChart, RadialBar, ResponsiveContainer } from "recharts";
+
+import { getResponseTimeColor } from "@/Utils/MonitorUtils";
+import { useTheme } from "@mui/material/styles";
+
+export const ChartAvgResponse = ({ avg, max }: { avg: number; max: number }) => {
+ const theme = useTheme();
+ const chartData = [
+ { name: "max", value: max - avg, color: "transparent" },
+ { name: "avg", value: avg, color: "red" },
+ ];
+
+ const palette = getResponseTimeColor(avg);
+ const msg: Record = {
+ success: "Excellent",
+ warning: "Average",
+ danger: "Poor",
+ };
+
+ return (
+ }
+ title={"Average response time"}
+ >
+
+
+
+
+ |
+ |
+
+
+
+
+ Low
+ High
+
+
+
+ {msg[palette]}
+
+ {`${avg?.toFixed()}ms`}
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/ChartResponseTime.tsx b/client/src/Components/v2/Monitors/ChartResponseTime.tsx
new file mode 100644
index 000000000..28f40f254
--- /dev/null
+++ b/client/src/Components/v2/Monitors/ChartResponseTime.tsx
@@ -0,0 +1,163 @@
+import { BaseChart } from "./HistogramStatus";
+import { BaseBox } from "../DesignElements";
+import ResponseTimeIcon from "@/assets/icons/response-time-icon.svg?react";
+import { normalizeResponseTimes } from "@/Utils/DataUtils";
+import {
+ AreaChart,
+ Area,
+ XAxis,
+ Tooltip,
+ CartesianGrid,
+ ResponsiveContainer,
+ Text,
+} from "recharts";
+import Typography from "@mui/material/Typography";
+
+import {
+ formatDateWithTz,
+ tickDateFormatLookup,
+ tooltipDateFormatLookup,
+} from "@/Utils/TimeUtils";
+import { useTheme } from "@mui/material/styles";
+import type { GroupedCheck } from "@/Types/Check";
+import { useSelector } from "react-redux";
+
+type XTickProps = {
+ x: number;
+ y: number;
+ payload: { value: any };
+ range: string;
+};
+
+const XTick: React.FC = ({ x, y, payload, range }) => {
+ const format = tickDateFormatLookup(range);
+ const theme = useTheme();
+ const uiTimezone = useSelector((state: any) => state.ui.timezone);
+ return (
+
+ {formatDateWithTz(payload?.value, format, uiTimezone)}
+
+ );
+};
+
+type ResponseTimeToolTipProps = {
+ active?: boolean | undefined;
+ payload?: any[];
+ label?: string;
+ range: string;
+ theme: any;
+ uiTimezone: string;
+};
+
+const ResponseTimeToolTip: React.FC = ({
+ active,
+ payload,
+ label,
+ range,
+ theme,
+ uiTimezone,
+}) => {
+ if (!label) return null;
+ if (!payload) return null;
+ if (!active) return null;
+
+ const format = tooltipDateFormatLookup(range);
+ const responseTime = Math.floor(payload?.[0]?.payload?.avgResponseTime || 0);
+ return (
+
+ {formatDateWithTz(label, format, uiTimezone)}
+ Response time: {responseTime} ms
+
+ );
+};
+
+export const ChartResponseTime = ({
+ checks,
+ range,
+}: {
+ checks: GroupedCheck[];
+ range: string;
+}) => {
+ const theme = useTheme();
+ const uiTimezone = useSelector((state: any) => state.ui.timezone);
+ const normalized = normalizeResponseTimes(
+ checks,
+ "avgResponseTime"
+ );
+ return (
+ }
+ title="Response times"
+ >
+
+
+
+
+
+
+
+
+
+ (
+
+ )}
+ />
+
+ (
+
+ )}
+ />
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/HeaderControls.tsx b/client/src/Components/v2/Monitors/HeaderControls.tsx
new file mode 100644
index 000000000..c6bfa7f81
--- /dev/null
+++ b/client/src/Components/v2/Monitors/HeaderControls.tsx
@@ -0,0 +1,67 @@
+import Stack from "@mui/material/Stack";
+import { MonitorStatus } from "@/Components/v2/Monitors/MonitorStatus";
+import { ButtonGroup, Button } from "@/Components/v2/Inputs";
+import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
+import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
+import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
+import EmailIcon from "@mui/icons-material/Email";
+import BugReportOutlinedIcon from "@mui/icons-material/BugReportOutlined";
+
+import { useMediaQuery } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import { useTheme } from "@mui/material/styles";
+
+import type { IMonitor } from "@/Types/Monitor";
+
+export const HeaderControls = ({
+ monitor,
+ patch,
+ isPatching,
+ refetch,
+}: {
+ monitor: IMonitor;
+ patch: Function;
+ isPatching: boolean;
+ refetch: Function;
+}) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+
+ return (
+
+
+
+
+ }>{t("sendTestNotifications")}
+ }>{t("menu.incidents")}
+
+ }>{t("configure")}
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/HeaderCreate.tsx b/client/src/Components/v2/Monitors/HeaderCreate.tsx
new file mode 100644
index 000000000..46b17c8a5
--- /dev/null
+++ b/client/src/Components/v2/Monitors/HeaderCreate.tsx
@@ -0,0 +1,35 @@
+import Stack from "@mui/material/Stack";
+import { Button } from "@/Components/v2/Inputs";
+import { useTheme } from "@mui/material/styles";
+import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router";
+export const HeaderCreate = ({
+ label,
+ isLoading,
+ path,
+}: {
+ label?: string;
+ isLoading: boolean;
+ path: string;
+}) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ return (
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/HeaderRange.tsx b/client/src/Components/v2/Monitors/HeaderRange.tsx
new file mode 100644
index 000000000..21ce01fe2
--- /dev/null
+++ b/client/src/Components/v2/Monitors/HeaderRange.tsx
@@ -0,0 +1,62 @@
+import Stack from "@mui/material/Stack";
+import { ButtonGroup, Button } from "@/Components/v2/Inputs";
+import { useTheme } from "@mui/material/styles";
+import Typography from "@mui/material/Typography";
+import { useMediaQuery } from "@mui/material";
+export const HeaderRange = ({
+ range,
+ setRange,
+ loading,
+}: {
+ range: string;
+ setRange: Function;
+ loading: boolean;
+}) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ return (
+
+ {`Showing statistics for past ${range}`}
+
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/HistogramResponseTime.tsx b/client/src/Components/v2/Monitors/HistogramResponseTime.tsx
new file mode 100644
index 000000000..1925ac93c
--- /dev/null
+++ b/client/src/Components/v2/Monitors/HistogramResponseTime.tsx
@@ -0,0 +1,87 @@
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import { useTheme } from "@mui/material/styles";
+import type { Check } from "@/Types/Check";
+import { HistogramResponseTimeTooltip } from "@/Components/v2/Monitors/HistogramResponseTimeTooltip";
+import { normalizeResponseTimes } from "@/Utils/DataUtils";
+
+export const HistogramResponseTime = ({ checks }: { checks: Check[] }) => {
+ const normalChecks = normalizeResponseTimes(checks, "responseTime");
+ let data = Array();
+
+ if (!normalChecks || normalChecks.length === 0) {
+ return null;
+ }
+ if (normalChecks.length !== 25) {
+ const placeholders = Array(25 - normalChecks.length).fill("placeholder");
+ data = [...normalChecks, ...placeholders];
+ } else {
+ data = normalChecks;
+ }
+
+ const theme = useTheme();
+
+ return (
+ event.stopPropagation()}
+ sx={{
+ cursor: "default",
+ }}
+ >
+ {data.map((check, index) => {
+ if (check === "placeholder") {
+ return (
+
+ );
+ } else {
+ return (
+
+
+
+
+
+ );
+ }
+ })}
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/HistogramResponseTimeTooltip.tsx b/client/src/Components/v2/Monitors/HistogramResponseTimeTooltip.tsx
new file mode 100644
index 000000000..d704cf5d4
--- /dev/null
+++ b/client/src/Components/v2/Monitors/HistogramResponseTimeTooltip.tsx
@@ -0,0 +1,29 @@
+import Stack from "@mui/material/Stack";
+import Tooltip from "@mui/material/Tooltip";
+import Typography from "@mui/material/Typography";
+import { formatDateWithTz } from "@/Utils/TimeUtils";
+import { useSelector } from "react-redux";
+import type { LatestCheck } from "@/Types/Check";
+
+export const HistogramResponseTimeTooltip: React.FC<{
+ children: React.ReactElement;
+ check: LatestCheck;
+}> = ({ children, check }) => {
+ const uiTimezone = useSelector((state: any) => state.ui.timezone);
+ return (
+
+
+ {formatDateWithTz(check?.checkedAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
+
+ {check?.responseTime && (
+ Response Time: {check.responseTime} ms
+ )}
+
+ }
+ >
+ {children}
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/HistogramStatus.tsx b/client/src/Components/v2/Monitors/HistogramStatus.tsx
new file mode 100644
index 000000000..2e10a8042
--- /dev/null
+++ b/client/src/Components/v2/Monitors/HistogramStatus.tsx
@@ -0,0 +1,218 @@
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import { BaseBox } from "@/Components/v2/DesignElements";
+import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
+import UptimeIcon from "@/assets/icons/uptime-icon.svg?react";
+import IncidentsIcon from "@/assets/icons/incidents.svg?react";
+
+import type { GroupedCheck } from "@/Types/Check";
+import type { MonitorStatus } from "@/Types/Monitor";
+
+import { normalizeResponseTimes } from "@/Utils/DataUtils";
+import { useState } from "react";
+import { formatDateWithTz } from "@/Utils/TimeUtils";
+import { useSelector } from "react-redux";
+import { useTheme } from "@mui/material/styles";
+import { getResponseTimeColor } from "@/Utils/MonitorUtils";
+
+const XLabel = ({
+ p1,
+ p2,
+ range,
+}: {
+ p1: GroupedCheck;
+ p2: GroupedCheck;
+ range: string;
+}) => {
+ const theme = useTheme();
+ const uiTimezone = useSelector((state: any) => state.ui.timezone);
+ const dateFormat = range === "day" ? "MMM D, h:mm A" : "MMM D";
+ return (
+ <>
+
+ {formatDateWithTz(p1._id, dateFormat, uiTimezone)}
+
+
+ {formatDateWithTz(p2._id, dateFormat, uiTimezone)}
+
+ >
+ );
+};
+
+type BaseChartProps = React.PropsWithChildren<{
+ icon: React.ReactNode;
+ title: string;
+}>;
+
+export const BaseChart: React.FC = ({ children, icon, title }) => {
+ const theme = useTheme();
+
+ return (
+
+
+
+
+ {icon}
+
+ {title}
+
+ {children}
+
+
+ );
+};
+
+export const HistogramStatus = ({
+ checks,
+ status,
+ range,
+ title,
+}: {
+ checks: GroupedCheck[];
+ status: MonitorStatus;
+ range: string;
+ title: string;
+}) => {
+ const uiTimezone = useSelector((state: any) => state.ui.timezone);
+
+ const icon = status === "up" ? : ;
+ const theme = useTheme();
+ const [idx, setIdx] = useState(null);
+ const dateFormat = range === "1d" || range === "2h" ? "MMM D, h A" : "MMM D";
+ const normalChecks = normalizeResponseTimes(checks, "avgResponseTime");
+
+ if (normalChecks.length === 0) {
+ return (
+
+
+
+ {status === "up" ? "No checks yet" : "Great, no downtime yet!"}
+
+
+
+ );
+ }
+
+ const totalChecks = normalChecks.reduce((count, check) => {
+ return count + check.count;
+ }, 0);
+
+ return (
+
+
+
+
+ Total checks
+ {idx ? (
+
+ {normalChecks[idx].count}
+
+ {formatDateWithTz(normalChecks[idx]._id, dateFormat, uiTimezone)}
+
+
+ ) : (
+ {totalChecks}
+ )}
+
+
+
+
+
+ }
+ />
+
+ {normalChecks?.map((groupedCheck, idx) => {
+ const fillColor = getResponseTimeColor(groupedCheck.normalResponseTime);
+ return (
+ setIdx(idx)}
+ onMouseLeave={() => setIdx(null)}
+ key={groupedCheck._id}
+ fill={theme.palette[fillColor].main}
+ />
+ );
+ })}
+ |
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/MonitorStatus.tsx b/client/src/Components/v2/Monitors/MonitorStatus.tsx
new file mode 100644
index 000000000..ff4d83352
--- /dev/null
+++ b/client/src/Components/v2/Monitors/MonitorStatus.tsx
@@ -0,0 +1,60 @@
+import type { IMonitor } from "@/Types/Monitor";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { PulseDot } from "@/Components/v2/DesignElements/PulseDot";
+import { Dot } from "@/Components/v2/DesignElements/Dot";
+import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
+import { useTheme } from "@mui/material/styles";
+import prettyMilliseconds from "pretty-ms";
+import { typographyLevels } from "@/Utils/Theme/v2/palette";
+import { useMediaQuery } from "@mui/material";
+export const MonitorStatus = ({ monitor }: { monitor: IMonitor }) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+
+ if (!monitor) {
+ return null;
+ }
+ return (
+
+
+ {monitor.name}
+
+
+
+
+ {formatUrl(monitor?.url)}
+
+ {!isSmall && (
+ <>
+
+
+ Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })}
+
+ >
+ )}
+
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/index.tsx b/client/src/Components/v2/Monitors/index.tsx
new file mode 100644
index 000000000..3b0acbd20
--- /dev/null
+++ b/client/src/Components/v2/Monitors/index.tsx
@@ -0,0 +1 @@
+export { HeaderCreate } from "./HeaderCreate";
diff --git a/client/src/Features/Auth/v2AuthSlice.ts b/client/src/Features/Auth/v2AuthSlice.ts
new file mode 100644
index 000000000..28a18b659
--- /dev/null
+++ b/client/src/Features/Auth/v2AuthSlice.ts
@@ -0,0 +1,19 @@
+import { createSlice } from "@reduxjs/toolkit";
+
+const initialState = {
+ isAuthenticated: false,
+};
+
+const v2AuthSlice = createSlice({
+ name: "v2Auth",
+ initialState,
+ reducers: {
+ setIsAuthenticated: (state, action) => {
+ const { authenticated } = action.payload;
+ state.isAuthenticated = authenticated;
+ },
+ },
+});
+
+export default v2AuthSlice.reducer;
+export const { setIsAuthenticated } = v2AuthSlice.actions;
diff --git a/client/src/Features/UI/uiSlice.js b/client/src/Features/UI/uiSlice.js
index 45ba267b0..63f7ae90e 100644
--- a/client/src/Features/UI/uiSlice.js
+++ b/client/src/Features/UI/uiSlice.js
@@ -47,6 +47,10 @@ const uiSlice = createSlice({
toggleSidebar: (state) => {
state.sidebar.collapsed = !state.sidebar.collapsed;
},
+ setCollapsed: (state, action) => {
+ const { collapsed } = action.payload;
+ state.sidebar.collapsed = collapsed;
+ },
setMode: (state, action) => {
state.mode = action.payload;
},
@@ -73,6 +77,7 @@ export default uiSlice.reducer;
export const {
setRowsPerPage,
toggleSidebar,
+ setCollapsed,
setMode,
setShowURL,
setGreeting,
diff --git a/client/src/Hooks/checkHooks.js b/client/src/Hooks/v1/checkHooks.js
similarity index 97%
rename from client/src/Hooks/checkHooks.js
rename to client/src/Hooks/v1/checkHooks.js
index bdb1a73b7..96fd1fe1c 100644
--- a/client/src/Hooks/checkHooks.js
+++ b/client/src/Hooks/v1/checkHooks.js
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
-import { networkService } from "../main";
-import { createToast } from "../Utils/toastUtils";
+import { networkService } from "../../main.jsx";
+import { createToast } from "../../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
const useFetchChecksTeam = ({
diff --git a/client/src/Hooks/inviteHooks.js b/client/src/Hooks/v1/inviteHooks.js
similarity index 97%
rename from client/src/Hooks/inviteHooks.js
rename to client/src/Hooks/v1/inviteHooks.js
index fb577272a..b6a0851af 100644
--- a/client/src/Hooks/inviteHooks.js
+++ b/client/src/Hooks/v1/inviteHooks.js
@@ -1,5 +1,5 @@
import { useState } from "react";
-import { networkService } from "../main";
+import { networkService } from "../../main.jsx";
import { useTranslation } from "react-i18next";
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST;
diff --git a/client/src/Hooks/logHooks.js b/client/src/Hooks/v1/logHooks.js
similarity index 96%
rename from client/src/Hooks/logHooks.js
rename to client/src/Hooks/v1/logHooks.js
index 989870eea..ff4d13872 100644
--- a/client/src/Hooks/logHooks.js
+++ b/client/src/Hooks/v1/logHooks.js
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
-import { networkService } from "../main";
-import { createToast } from "../Utils/toastUtils";
+import { networkService } from "../../main.jsx";
+import { createToast } from "../../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
const useFetchLogs = () => {
diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/v1/monitorHooks.js
similarity index 98%
rename from client/src/Hooks/monitorHooks.js
rename to client/src/Hooks/v1/monitorHooks.js
index eb1e7acdc..e6b4b5261 100644
--- a/client/src/Hooks/monitorHooks.js
+++ b/client/src/Hooks/v1/monitorHooks.js
@@ -1,8 +1,8 @@
import { useEffect, useState } from "react";
-import { networkService } from "../main";
-import { createToast } from "../Utils/toastUtils";
+import { networkService } from "../../main.jsx";
+import { createToast } from "../../Utils/toastUtils.jsx";
import { useTheme } from "@emotion/react";
-import { useMonitorUtils } from "./useMonitorUtils";
+import { useMonitorUtils } from "./useMonitorUtils.js";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
diff --git a/client/src/Hooks/settingsHooks.js b/client/src/Hooks/v1/settingsHooks.js
similarity index 95%
rename from client/src/Hooks/settingsHooks.js
rename to client/src/Hooks/v1/settingsHooks.js
index 98a21c1fa..787e46ff9 100644
--- a/client/src/Hooks/settingsHooks.js
+++ b/client/src/Hooks/v1/settingsHooks.js
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
-import { networkService } from "../main";
-import { createToast } from "../Utils/toastUtils";
+import { networkService } from "../../main.jsx";
+import { createToast } from "../../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
const useFetchSettings = ({ setSettingsData, setIsApiKeySet, setIsEmailPasswordSet }) => {
diff --git a/client/src/Hooks/useIsAdmin.js b/client/src/Hooks/v1/useIsAdmin.js
similarity index 100%
rename from client/src/Hooks/useIsAdmin.js
rename to client/src/Hooks/v1/useIsAdmin.js
diff --git a/client/src/Hooks/useMonitorUtils.js b/client/src/Hooks/v1/useMonitorUtils.js
similarity index 100%
rename from client/src/Hooks/useMonitorUtils.js
rename to client/src/Hooks/v1/useMonitorUtils.js
diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/v1/useNotifications.js
similarity index 96%
rename from client/src/Hooks/useNotifications.js
rename to client/src/Hooks/v1/useNotifications.js
index 539725958..d1b4a6652 100644
--- a/client/src/Hooks/useNotifications.js
+++ b/client/src/Hooks/v1/useNotifications.js
@@ -1,9 +1,9 @@
import { useState, useEffect, useCallback } from "react";
-import { createToast } from "../Utils/toastUtils";
-import { networkService } from "../main";
+import { createToast } from "../../Utils/toastUtils.jsx";
+import { networkService } from "../../main.jsx";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
-import { NOTIFICATION_TYPES } from "../Pages/Notifications/utils";
+import { NOTIFICATION_TYPES } from "../../Pages/v1/Notifications/utils.js";
const useCreateNotification = () => {
const navigate = useNavigate();
diff --git a/client/src/Hooks/useSendTestEmail.js b/client/src/Hooks/v1/useSendTestEmail.js
similarity index 93%
rename from client/src/Hooks/useSendTestEmail.js
rename to client/src/Hooks/v1/useSendTestEmail.js
index e280b1f99..0be77954a 100644
--- a/client/src/Hooks/useSendTestEmail.js
+++ b/client/src/Hooks/v1/useSendTestEmail.js
@@ -1,7 +1,7 @@
import { useState } from "react";
-import { networkService } from "../main";
+import { networkService } from "../../main.jsx";
import { useSelector } from "react-redux";
-import { createToast } from "../Utils/toastUtils";
+import { createToast } from "../../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
const useSendTestEmail = () => {
diff --git a/client/src/Hooks/userHooks.js b/client/src/Hooks/v1/userHooks.js
similarity index 68%
rename from client/src/Hooks/userHooks.js
rename to client/src/Hooks/v1/userHooks.js
index 8be32a082..f9c454450 100644
--- a/client/src/Hooks/userHooks.js
+++ b/client/src/Hooks/v1/userHooks.js
@@ -1,13 +1,12 @@
import { useEffect, useState, useCallback } from "react";
-import { networkService } from "../main";
-import { createToast } from "../Utils/toastUtils";
+import { networkService } from "../../main.jsx";
+import { createToast } from "../../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
export const useGetUser = (userId) => {
const [user, setUser] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
-
const fetchUser = useCallback(async () => {
try {
setIsLoading(true);
@@ -54,8 +53,29 @@ export const useEditUser = (userId) => {
setIsLoading(false);
}
},
- [userId]
+ [userId, t]
);
- return [editUser, isLoading, error];
+ const changePassword = useCallback(
+ async (passwordForm) => {
+ try {
+ setIsLoading(true);
+
+ await networkService.changePasswordByAdmin({ userId, passwordForm });
+ createToast({
+ body: t("teamPanel.changeTeamPassword.success"),
+ });
+ } catch (error) {
+ createToast({
+ body: error.message,
+ });
+ setError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [userId, t]
+ );
+
+ return [editUser, isLoading, error, changePassword];
};
diff --git a/client/src/Hooks/v2/UseApi.tsx b/client/src/Hooks/v2/UseApi.tsx
new file mode 100644
index 000000000..d3001c87b
--- /dev/null
+++ b/client/src/Hooks/v2/UseApi.tsx
@@ -0,0 +1,90 @@
+import { useState } from "react";
+import useSWR from "swr";
+import type { SWRConfiguration } from "swr";
+import type { AxiosRequestConfig } from "axios";
+import { get, post, patch } from "@/Utils/ApiClient"; // your axios wrapper
+
+export type ApiResponse = {
+ message: string;
+ data: any;
+};
+
+// Generic fetcher for GET requests
+const fetcher = async (url: string, config?: AxiosRequestConfig) => {
+ const res = await get(url, config);
+ return res.data;
+};
+
+export const useGet = (
+ url: string,
+ axiosConfig?: AxiosRequestConfig,
+ swrConfig?: SWRConfiguration
+) => {
+ const { data, error, isLoading, isValidating, mutate } = useSWR(
+ url,
+ (url) => fetcher(url, axiosConfig),
+ swrConfig
+ );
+
+ return {
+ response: data ?? null,
+ loading: isLoading,
+ isValidating,
+ error: error?.message ?? null,
+ refetch: mutate,
+ };
+};
+
+export const usePost = () => {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const postFn = async (
+ endpoint: string,
+ body: B,
+ config?: AxiosRequestConfig
+ ): Promise => {
+ setLoading(true);
+ setError(null);
+
+ try {
+ const res = await post(endpoint, body, config);
+ return res.data;
+ } catch (err: any) {
+ const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
+ setError(errMsg);
+ return null;
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return { post: postFn, loading, error };
+};
+
+export const usePatch = () => {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const patchFn = async (
+ endpoint: string,
+ body?: B,
+ config?: AxiosRequestConfig
+ ): Promise => {
+ setLoading(true);
+ setError(null);
+
+ try {
+ const res = await patch(endpoint, body, config);
+ return res.data;
+ } catch (err: any) {
+ const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
+ setError(errMsg);
+ return null;
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return { patch: patchFn, loading, error };
+};
diff --git a/client/src/Pages/Account/components/ChangePasswordModal/index.jsx b/client/src/Pages/Account/components/ChangePasswordModal/index.jsx
new file mode 100644
index 000000000..cc284a675
--- /dev/null
+++ b/client/src/Pages/Account/components/ChangePasswordModal/index.jsx
@@ -0,0 +1,149 @@
+import { useState } from "react";
+import { Button, Stack } from "@mui/material";
+import { GenericDialog } from "@/Components/v1/Dialog/genericDialog";
+import TextInput from "@/Components/v1/Inputs/TextInput";
+import PasswordTooltip from "@/Pages/v1/Auth/components/PasswordTooltip";
+import { useTheme } from "@emotion/react";
+import { useTranslation } from "react-i18next";
+import { createToast } from "../../../../Utils/toastUtils";
+import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments";
+import usePasswordFeedback from "@/Pages/v1/Auth/hooks/usePasswordFeedback";
+import PropTypes from "prop-types";
+
+const ChangePasswordModal = ({ isSaving, isLoading, changePassword }) => {
+ const INITIAL_FORM_STATE = {
+ password: "",
+ confirm: "",
+ };
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const { feedback, handlePasswordFeedback } = usePasswordFeedback();
+ const [form, setForm] = useState(INITIAL_FORM_STATE);
+ const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
+ const [errors, setErrors] = useState({});
+ const [isLoadingSubmit, setIsLoadingSubmit] = useState(false);
+ const closeChangePasswordModal = () => {
+ setIsChangePasswordOpen(false);
+ setForm(INITIAL_FORM_STATE);
+ };
+
+ const onChange = (e) => {
+ let { name, value } = e.target;
+ const updatedForm = { ...form, [name]: value };
+ setForm(updatedForm);
+
+ handlePasswordFeedback(updatedForm, name, value, form, errors, setErrors);
+ };
+ const isFormValid =
+ form.password.length > 1 &&
+ form.confirm.length > 1 &&
+ !errors.password &&
+ !errors.confirm;
+ const onsubmitChangePassword = async (event) => {
+ event.preventDefault();
+ if (!isFormValid) return;
+ const newPasswordForm = {
+ password: form.password,
+ };
+ try {
+ setIsLoadingSubmit(true);
+ await changePassword(newPasswordForm);
+ closeChangePasswordModal();
+ } catch (error) {
+ const errorMsg = error.response?.data?.msg || error.message || "unknownError";
+ createToast({
+ type: "error",
+ body: t(errorMsg),
+ });
+ } finally {
+ setIsLoadingSubmit(false);
+ }
+ };
+
+ return (
+ <>
+
+
+
+ }
+ sx={{ mb: theme.spacing(5) }}
+ />
+
+ }
+ sx={{ mb: theme.spacing(5) }}
+ />
+
+
+
+
+
+
+ >
+ );
+};
+
+ChangePasswordModal.propTypes = {
+ isSaving: PropTypes.bool.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ changePassword: PropTypes.func.isRequired,
+};
+
+export default ChangePasswordModal;
diff --git a/client/src/Pages/Uptime/Create/index.css b/client/src/Pages/Uptime/Create/index.css
deleted file mode 100644
index e69de29bb..000000000
diff --git a/client/src/Pages/About/index.jsx b/client/src/Pages/v1/About/index.jsx
similarity index 68%
rename from client/src/Pages/About/index.jsx
rename to client/src/Pages/v1/About/index.jsx
index 97040b2e3..ff7bb6c6b 100644
--- a/client/src/Pages/About/index.jsx
+++ b/client/src/Pages/v1/About/index.jsx
@@ -1,6 +1,6 @@
import React from "react";
-import AppBar from "../../Components/Common/AppBar";
-import Footer from "../../Components/Common/Footer";
+import AppBar from "@/Components/v1/Common/AppBar.jsx";
+import Footer from "@/Components/v1/Common/Footer.jsx";
import { useTranslation } from "react-i18next";
const AboutUs = () => {
diff --git a/client/src/Pages/Account/EditUser/hooks/editUser.js b/client/src/Pages/v1/Account/EditUser/hooks/editUser.js
similarity index 91%
rename from client/src/Pages/Account/EditUser/hooks/editUser.js
rename to client/src/Pages/v1/Account/EditUser/hooks/editUser.js
index 2ffa914a0..00660aead 100644
--- a/client/src/Pages/Account/EditUser/hooks/editUser.js
+++ b/client/src/Pages/v1/Account/EditUser/hooks/editUser.js
@@ -1,8 +1,8 @@
import { useState, useEffect } from "react";
-import { ROLES } from "../../../../Utils/roleUtils";
-import { editUserValidation } from "../../../../Validation/validation";
+import { ROLES } from "../../../../../Utils/roleUtils.js";
+import { editUserValidation } from "../../../../../Validation/validation.js";
import Joi from "joi";
-import { createToast } from "../../../../Utils/toastUtils";
+import { createToast } from "../../../../../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
export const useEditUserForm = (user) => {
const [searchInput, setSearchInput] = useState("");
diff --git a/client/src/Pages/Account/EditUser/index.jsx b/client/src/Pages/v1/Account/EditUser/index.jsx
similarity index 70%
rename from client/src/Pages/Account/EditUser/index.jsx
rename to client/src/Pages/v1/Account/EditUser/index.jsx
index e1eeead9d..674a522b8 100644
--- a/client/src/Pages/Account/EditUser/index.jsx
+++ b/client/src/Pages/v1/Account/EditUser/index.jsx
@@ -1,23 +1,25 @@
// Components
import Stack from "@mui/material/Stack";
-import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import TextInput from "../../../Components/Inputs/TextInput";
-import Search from "../../../Components/Inputs/Search";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
+import Search from "@/Components/v1/Inputs/Search/index.jsx";
import Button from "@mui/material/Button";
-import RoleTable from "../components/RoleTable";
-
+import RoleTable from "../components/RoleTable/index.jsx";
+import ChangePasswordModal from "@/Pages/Account/components/ChangePasswordModal/index.jsx";
// Utils
import { useParams } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
-import { useGetUser, useEditUser } from "../../../Hooks/userHooks";
-import { EDITABLE_ROLES, ROLES } from "../../../Utils/roleUtils";
-import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser";
+import { useGetUser, useEditUser } from "../../../../Hooks/v1/userHooks.js";
+import { EDITABLE_ROLES, ROLES } from "../../../../Utils/roleUtils.js";
+import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser.js";
+import { useSelector } from "react-redux";
const EditUser = () => {
+ const { user } = useSelector((state) => state.auth);
const { userId } = useParams();
+ const isSameUser = user?._id === userId;
const theme = useTheme();
const { t } = useTranslation();
const BREADCRUMBS = [
@@ -25,8 +27,8 @@ const EditUser = () => {
{ name: t("editUserPage.title"), path: "" },
];
- const [user, isLoading, error] = useGetUser(userId);
- const [editUser, isSaving, saveError] = useEditUser(userId);
+ const [userToEdit, isLoading, error] = useGetUser(userId);
+ const [editUser, isSaving, saveError, changePassword] = useEditUser(userId);
const [
form,
setForm,
@@ -34,7 +36,7 @@ const EditUser = () => {
handleDeleteRole,
searchInput,
handleSearchInput,
- ] = useEditUserForm(user);
+ ] = useEditUserForm(userToEdit);
const [errors, validateForm, validateField] = useValidateEditUserForm();
const onChange = (e) => {
@@ -104,7 +106,12 @@ const EditUser = () => {
roles={form?.role}
handleDeleteRole={handleDeleteRole}
/>
-
+
-
+ {!isSameUser && (
+
+ )}
+
);
diff --git a/client/src/Pages/Account/components/AddMemberMenu/index.jsx b/client/src/Pages/v1/Account/components/AddMemberMenu/index.jsx
similarity index 100%
rename from client/src/Pages/Account/components/AddMemberMenu/index.jsx
rename to client/src/Pages/v1/Account/components/AddMemberMenu/index.jsx
diff --git a/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx b/client/src/Pages/v1/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx
similarity index 93%
rename from client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx
rename to client/src/Pages/v1/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx
index e42c504b2..58db019a3 100644
--- a/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx
+++ b/client/src/Pages/v1/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx
@@ -1,5 +1,5 @@
import { useState } from "react";
-import { newOrChangedCredentials } from "../../../../../Validation/validation";
+import { newOrChangedCredentials } from "../../../../../../Validation/validation.js";
import { useTranslation } from "react-i18next";
const useAddTeamMember = () => {
const { t } = useTranslation();
diff --git a/client/src/Pages/Account/components/AddTeamMember/index.jsx b/client/src/Pages/v1/Account/components/AddTeamMember/index.jsx
similarity index 92%
rename from client/src/Pages/Account/components/AddTeamMember/index.jsx
rename to client/src/Pages/v1/Account/components/AddTeamMember/index.jsx
index 2df8fb226..219658b63 100644
--- a/client/src/Pages/Account/components/AddTeamMember/index.jsx
+++ b/client/src/Pages/v1/Account/components/AddTeamMember/index.jsx
@@ -1,16 +1,16 @@
import { Button, Stack } from "@mui/material";
-import { GenericDialog } from "../../../../Components/Dialog/genericDialog";
-import TextInput from "../../../../Components/Inputs/TextInput";
-import Select from "../../../../Components/Inputs/Select";
-import { useGetInviteToken } from "../../../../Hooks/inviteHooks";
+import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
+import Select from "@/Components/v1/Inputs/Select/index.jsx";
+import { useGetInviteToken } from "../../../../../Hooks/v1/inviteHooks.js";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
-import { createToast } from "../../../../Utils/toastUtils";
+import { createToast } from "../../../../../Utils/toastUtils.jsx";
import { useState } from "react";
-import PasswordTooltip from "../../../Auth/components/PasswordTooltip";
-import useAddTeamMember from "./hooks/useAddTeamMember";
-import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback";
-import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments";
+import PasswordTooltip from "../../../Auth/components/PasswordTooltip.jsx";
+import useAddTeamMember from "./hooks/useAddTeamMember.jsx";
+import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback.jsx";
+import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
import PropTypes from "prop-types";
const INITIAL_FORM_STATE = {
diff --git a/client/src/Pages/Account/components/PasswordPanel.jsx b/client/src/Pages/v1/Account/components/PasswordPanel.jsx
similarity index 92%
rename from client/src/Pages/Account/components/PasswordPanel.jsx
rename to client/src/Pages/v1/Account/components/PasswordPanel.jsx
index dabd67706..ca2af9205 100644
--- a/client/src/Pages/Account/components/PasswordPanel.jsx
+++ b/client/src/Pages/v1/Account/components/PasswordPanel.jsx
@@ -2,14 +2,14 @@ import TabPanel from "@mui/lab/TabPanel";
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography, Button } from "@mui/material";
-import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments";
-import TextInput from "../../../Components/Inputs/TextInput";
-import { newOrChangedCredentials } from "../../../Validation/validation";
-import Alert from "../../../Components/Alert";
-import { update } from "../../../Features/Auth/authSlice";
+import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
+import { newOrChangedCredentials } from "../../../../Validation/validation.js";
+import Alert from "@/Components/v1/Alert/index.jsx";
+import { update } from "../../../../Features/Auth/authSlice.js";
import { useDispatch, useSelector } from "react-redux";
-import { createToast } from "../../../Utils/toastUtils";
-import { getTouchedFieldErrors } from "../../../Validation/error";
+import { createToast } from "../../../../Utils/toastUtils.jsx";
+import { getTouchedFieldErrors } from "../../../../Validation/error.js";
import { useTranslation } from "react-i18next";
const defaultPasswordsState = {
diff --git a/client/src/Pages/Account/components/ProfilePanel.jsx b/client/src/Pages/v1/Account/components/ProfilePanel.jsx
similarity index 94%
rename from client/src/Pages/Account/components/ProfilePanel.jsx
rename to client/src/Pages/v1/Account/components/ProfilePanel.jsx
index 7c73e7aa3..2035861a2 100644
--- a/client/src/Pages/Account/components/ProfilePanel.jsx
+++ b/client/src/Pages/v1/Account/components/ProfilePanel.jsx
@@ -2,16 +2,20 @@ import { useTheme } from "@emotion/react";
import { useState } from "react";
import TabPanel from "@mui/lab/TabPanel";
import { Box, Button, Divider, Stack, Typography } from "@mui/material";
-import Avatar from "../../../Components/Avatar";
-import TextInput from "../../../Components/Inputs/TextInput";
-import ImageUpload from "../../../Components/Inputs/ImageUpload";
-import { newOrChangedCredentials } from "../../../Validation/validation";
+import Avatar from "@/Components/v1/Avatar/index.jsx";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
+import ImageUpload from "@/Components/v1/Inputs/ImageUpload/index.jsx";
+import { newOrChangedCredentials } from "../../../../Validation/validation.js";
import { useDispatch, useSelector } from "react-redux";
-import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice";
-import { createToast } from "../../../Utils/toastUtils";
-import { logger } from "../../../Utils/Logger";
-import { GenericDialog } from "../../../Components/Dialog/genericDialog";
-import Dialog from "../../../Components/Dialog";
+import {
+ clearAuthState,
+ deleteUser,
+ update,
+} from "../../../../Features/Auth/authSlice.js";
+import { createToast } from "../../../../Utils/toastUtils.jsx";
+import { logger } from "../../../../Utils/Logger.js";
+import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx";
+import Dialog from "@/Components/v1/Dialog/index.jsx";
import { useTranslation } from "react-i18next";
/**
diff --git a/client/src/Pages/Account/components/RoleTable/index.jsx b/client/src/Pages/v1/Account/components/RoleTable/index.jsx
similarity index 88%
rename from client/src/Pages/Account/components/RoleTable/index.jsx
rename to client/src/Pages/v1/Account/components/RoleTable/index.jsx
index 9d6ba3a16..02dcaaf0d 100644
--- a/client/src/Pages/Account/components/RoleTable/index.jsx
+++ b/client/src/Pages/v1/Account/components/RoleTable/index.jsx
@@ -1,9 +1,9 @@
import Typography from "@mui/material/Typography";
-import DataTable from "../../../../Components/Table";
+import DataTable from "@/Components/v1/Table/index.jsx";
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
import { useTranslation } from "react-i18next";
-import { ROLES } from "../../../../Utils/roleUtils";
+import { ROLES } from "../../../../../Utils/roleUtils.js";
const RoleTable = ({ roles, handleDeleteRole }) => {
const { t } = useTranslation();
diff --git a/client/src/Pages/Account/components/TeamPanel.jsx b/client/src/Pages/v1/Account/components/TeamPanel.jsx
similarity index 92%
rename from client/src/Pages/Account/components/TeamPanel.jsx
rename to client/src/Pages/v1/Account/components/TeamPanel.jsx
index fc1768642..02918eb30 100644
--- a/client/src/Pages/Account/components/TeamPanel.jsx
+++ b/client/src/Pages/v1/Account/components/TeamPanel.jsx
@@ -3,18 +3,18 @@ import TabPanel from "@mui/lab/TabPanel";
import { Button, ButtonGroup, Stack, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
-import TextInput from "../../../Components/Inputs/TextInput";
-import { newOrChangedCredentials } from "../../../Validation/validation";
-import { networkService } from "../../../main";
-import { createToast } from "../../../Utils/toastUtils";
-import Select from "../../../Components/Inputs/Select";
-import { GenericDialog } from "../../../Components/Dialog/genericDialog";
-import AddTeamMember from "../components/AddTeamMember";
-import DataTable from "../../../Components/Table";
-import { useGetInviteToken } from "../../../Hooks/inviteHooks";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
+import { newOrChangedCredentials } from "../../../../Validation/validation.js";
+import { networkService } from "../../../../main.jsx";
+import { createToast } from "../../../../Utils/toastUtils.jsx";
+import Select from "@/Components/v1/Inputs/Select/index.jsx";
+import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx";
+import AddTeamMember from "./AddTeamMember/index.jsx";
+import DataTable from "@/Components/v1/Table/index.jsx";
+import { useGetInviteToken } from "../../../../Hooks/v1/inviteHooks.js";
import { useNavigate } from "react-router-dom";
-import { useIsSuperAdmin } from "../../../Hooks/useIsAdmin";
-import AddMemberMenu from "./AddMemberMenu";
+import { useIsSuperAdmin } from "../../../../Hooks/v1/useIsAdmin.js";
+import AddMemberMenu from "./AddMemberMenu/index.jsx";
/**
* TeamPanel component manages the organization and team members,
* providing functionalities like renaming the organization, managing team members,
diff --git a/client/src/Pages/Account/index.jsx b/client/src/Pages/v1/Account/index.jsx
similarity index 91%
rename from client/src/Pages/Account/index.jsx
rename to client/src/Pages/v1/Account/index.jsx
index 37c64aca6..ae16ff9d7 100644
--- a/client/src/Pages/Account/index.jsx
+++ b/client/src/Pages/v1/Account/index.jsx
@@ -3,11 +3,11 @@ import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { Box, Tab, useTheme } from "@mui/material";
-import CustomTabList from "../../Components/Tab";
+import CustomTabList from "@/Components/v1/Tab/index.jsx";
import TabContext from "@mui/lab/TabContext";
-import ProfilePanel from "./components/ProfilePanel";
-import PasswordPanel from "./components/PasswordPanel";
-import TeamPanel from "./components/TeamPanel";
+import ProfilePanel from "./components/ProfilePanel.jsx";
+import PasswordPanel from "./components/PasswordPanel.jsx";
+import TeamPanel from "./components/TeamPanel.jsx";
import { useTranslation } from "react-i18next";
/**
diff --git a/client/src/Pages/Auth/CheckEmail.jsx b/client/src/Pages/v1/Auth/CheckEmail.jsx
similarity index 93%
rename from client/src/Pages/Auth/CheckEmail.jsx
rename to client/src/Pages/v1/Auth/CheckEmail.jsx
index e1f5e43ed..c12a84804 100644
--- a/client/src/Pages/Auth/CheckEmail.jsx
+++ b/client/src/Pages/v1/Auth/CheckEmail.jsx
@@ -3,13 +3,13 @@ import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
-import { createToast } from "../../Utils/toastUtils";
-import { forgotPassword } from "../../Features/Auth/authSlice";
+import { createToast } from "../../../Utils/toastUtils.jsx";
+import { forgotPassword } from "../../../Features/Auth/authSlice.js";
import { Trans, useTranslation } from "react-i18next";
-import Background from "../../assets/Images/background-grid.svg?react";
-import EmailIcon from "../../assets/icons/email.svg?react";
-import Logo from "../../assets/icons/checkmate-icon.svg?react";
-import IconBox from "../../Components/IconBox";
+import Background from "../../../assets/Images/background-grid.svg?react";
+import EmailIcon from "../../../assets/icons/email.svg?react";
+import Logo from "../../../assets/icons/checkmate-icon.svg?react";
+import IconBox from "@/Components/v1/IconBox/index.jsx";
import "./index.css";
const CheckEmail = () => {
diff --git a/client/src/Pages/Auth/ForgotPassword.jsx b/client/src/Pages/v1/Auth/ForgotPassword.jsx
similarity index 91%
rename from client/src/Pages/Auth/ForgotPassword.jsx
rename to client/src/Pages/v1/Auth/ForgotPassword.jsx
index 2a887756e..8cbd85d8a 100644
--- a/client/src/Pages/Auth/ForgotPassword.jsx
+++ b/client/src/Pages/v1/Auth/ForgotPassword.jsx
@@ -1,16 +1,16 @@
import { Box, Stack, Typography, Button } from "@mui/material";
import { useTheme } from "@emotion/react";
-import { createToast } from "../../Utils/toastUtils";
+import { createToast } from "../../../Utils/toastUtils.jsx";
import { useDispatch, useSelector } from "react-redux";
-import { forgotPassword } from "../../Features/Auth/authSlice";
+import { forgotPassword } from "../../../Features/Auth/authSlice.js";
import { useEffect, useState } from "react";
-import { newOrChangedCredentials } from "../../Validation/validation";
+import { newOrChangedCredentials } from "../../../Validation/validation.js";
import { useNavigate } from "react-router-dom";
-import TextInput from "../../Components/Inputs/TextInput";
-import Logo from "../../assets/icons/checkmate-icon.svg?react";
-import Key from "../../assets/icons/key.svg?react";
-import Background from "../../assets/Images/background-grid.svg?react";
-import IconBox from "../../Components/IconBox";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
+import Logo from "../../../assets/icons/checkmate-icon.svg?react";
+import Key from "../../../assets/icons/key.svg?react";
+import Background from "../../../assets/Images/background-grid.svg?react";
+import IconBox from "@/Components/v1/IconBox/index.jsx";
import { Trans, useTranslation } from "react-i18next";
import "./index.css";
diff --git a/client/src/Pages/Auth/Login/hooks/useLoadingSubmit.jsx b/client/src/Pages/v1/Auth/Login/hooks/useLoadingSubmit.jsx
similarity index 100%
rename from client/src/Pages/Auth/Login/hooks/useLoadingSubmit.jsx
rename to client/src/Pages/v1/Auth/Login/hooks/useLoadingSubmit.jsx
diff --git a/client/src/Pages/Auth/Login/hooks/useLoginForm.jsx b/client/src/Pages/v1/Auth/Login/hooks/useLoginForm.jsx
similarity index 100%
rename from client/src/Pages/Auth/Login/hooks/useLoginForm.jsx
rename to client/src/Pages/v1/Auth/Login/hooks/useLoginForm.jsx
diff --git a/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx b/client/src/Pages/v1/Auth/Login/hooks/useLoginSubmit.jsx
similarity index 87%
rename from client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx
rename to client/src/Pages/v1/Auth/Login/hooks/useLoginSubmit.jsx
index 6bc5716ff..0eed9bd07 100644
--- a/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx
+++ b/client/src/Pages/v1/Auth/Login/hooks/useLoginSubmit.jsx
@@ -1,7 +1,7 @@
import { useDispatch } from "react-redux";
-import { login } from "../../../../Features/Auth/authSlice";
+import { login } from "../../../../../Features/Auth/authSlice.js";
import { useNavigate } from "react-router-dom";
-import { createToast } from "../../../../Utils/toastUtils";
+import { createToast } from "../../../../../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
const useLoginSubmit = () => {
diff --git a/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx b/client/src/Pages/v1/Auth/Login/hooks/useValidateLoginForm.jsx
similarity index 90%
rename from client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx
rename to client/src/Pages/v1/Auth/Login/hooks/useValidateLoginForm.jsx
index e5d045be8..ca9bef879 100644
--- a/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx
+++ b/client/src/Pages/v1/Auth/Login/hooks/useValidateLoginForm.jsx
@@ -1,5 +1,5 @@
import { useState } from "react";
-import { loginCredentials } from "../../../../Validation/validation";
+import { loginCredentials } from "../../../../../Validation/validation.js";
const useValidateLoginForm = () => {
const [errors, setErrors] = useState({
diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/v1/Auth/Login/index.jsx
similarity index 82%
rename from client/src/Pages/Auth/Login/index.jsx
rename to client/src/Pages/v1/Auth/Login/index.jsx
index d06ecef6d..7f5dcb5e4 100644
--- a/client/src/Pages/Auth/Login/index.jsx
+++ b/client/src/Pages/v1/Auth/Login/index.jsx
@@ -1,17 +1,17 @@
// Components
import Stack from "@mui/material/Stack";
import Button from "@mui/material/Button";
-import TextInput from "../../../Components/Inputs/TextInput";
-import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments";
-import TextLink from "../../../Components/TextLink";
-import AuthPageWrapper from "../components/AuthPageWrapper";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
+import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
+import TextLink from "@/Components/v1/TextLink/index.jsx";
+import AuthPageWrapper from "../components/AuthPageWrapper.jsx";
// Utils
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
-import useLoginForm from "./hooks/useLoginForm";
-import useValidateLoginForm from "./hooks/useValidateLoginForm";
-import useLoginSubmit from "./hooks/useLoginSubmit";
-import useLoadingSubmit from "./hooks/useLoadingSubmit";
+import useLoginForm from "./hooks/useLoginForm.jsx";
+import useValidateLoginForm from "./hooks/useValidateLoginForm.jsx";
+import useLoginSubmit from "./hooks/useLoginSubmit.jsx";
+import useLoadingSubmit from "./hooks/useLoadingSubmit.jsx";
const Login = () => {
// Hooks
diff --git a/client/src/Pages/Auth/NewPasswordConfirmed.jsx b/client/src/Pages/v1/Auth/NewPasswordConfirmed.jsx
similarity index 90%
rename from client/src/Pages/Auth/NewPasswordConfirmed.jsx
rename to client/src/Pages/v1/Auth/NewPasswordConfirmed.jsx
index 7ace77abc..629494098 100644
--- a/client/src/Pages/Auth/NewPasswordConfirmed.jsx
+++ b/client/src/Pages/v1/Auth/NewPasswordConfirmed.jsx
@@ -2,11 +2,11 @@ import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
-import { clearAuthState } from "../../Features/Auth/authSlice";
-import Background from "../../assets/Images/background-grid.svg?react";
-import ConfirmIcon from "../../assets/icons/check-outlined.svg?react";
-import Logo from "../../assets/icons/checkmate-icon.svg?react";
-import IconBox from "../../Components/IconBox";
+import { clearAuthState } from "../../../Features/Auth/authSlice.js";
+import Background from "../../../assets/Images/background-grid.svg?react";
+import ConfirmIcon from "../../../assets/icons/check-outlined.svg?react";
+import Logo from "../../../assets/icons/checkmate-icon.svg?react";
+import IconBox from "@/Components/v1/IconBox/index.jsx";
import { Trans, useTranslation } from "react-i18next";
import "./index.css";
diff --git a/client/src/Pages/Auth/Register/index.jsx b/client/src/Pages/v1/Auth/Register/index.jsx
similarity index 94%
rename from client/src/Pages/Auth/Register/index.jsx
rename to client/src/Pages/v1/Auth/Register/index.jsx
index 2b45d2d15..d6515b85c 100644
--- a/client/src/Pages/Auth/Register/index.jsx
+++ b/client/src/Pages/v1/Auth/Register/index.jsx
@@ -1,10 +1,10 @@
// Components
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
-import TextInput from "../../../Components/Inputs/TextInput";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
-import PasswordTooltip from "../components/PasswordTooltip";
+import PasswordTooltip from "../components/PasswordTooltip.jsx";
// Utils
import { useTheme } from "@emotion/react";
@@ -13,11 +13,11 @@ import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { useParams } from "react-router-dom";
-import { networkService } from "../../../main";
-import { newOrChangedCredentials } from "../../../Validation/validation";
-import { register } from "../../../Features/Auth/authSlice";
-import AuthPageWrapper from "../components/AuthPageWrapper";
-import { createToast } from "../../../Utils/toastUtils";
+import { networkService } from "../../../../main.jsx";
+import { newOrChangedCredentials } from "../../../../Validation/validation.js";
+import { register } from "../../../../Features/Auth/authSlice.js";
+import AuthPageWrapper from "../components/AuthPageWrapper.jsx";
+import { createToast } from "../../../../Utils/toastUtils.jsx";
import PropTypes from "prop-types";
const getFeedbackStatus = (form, errors, field, criteria) => {
diff --git a/client/src/Pages/Auth/SetNewPassword.jsx b/client/src/Pages/v1/Auth/SetNewPassword.jsx
similarity index 91%
rename from client/src/Pages/Auth/SetNewPassword.jsx
rename to client/src/Pages/v1/Auth/SetNewPassword.jsx
index b4899f47c..45f3351f3 100644
--- a/client/src/Pages/Auth/SetNewPassword.jsx
+++ b/client/src/Pages/v1/Auth/SetNewPassword.jsx
@@ -4,18 +4,18 @@ import { useNavigate } from "react-router-dom";
import { useParams } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography, Button } from "@mui/material";
-import { setNewPassword } from "../../Features/Auth/authSlice";
-import { createToast } from "../../Utils/toastUtils";
-import { newOrChangedCredentials } from "../../Validation/validation";
-import Check from "../../Components/Check/Check";
-import TextInput from "../../Components/Inputs/TextInput";
-import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
-import IconBox from "../../Components/IconBox";
-import LockIcon from "../../assets/icons/lock.svg?react";
-import Logo from "../../assets/icons/checkmate-icon.svg?react";
-import Background from "../../assets/Images/background-grid.svg?react";
+import { setNewPassword } from "../../../Features/Auth/authSlice.js";
+import { createToast } from "../../../Utils/toastUtils.jsx";
+import { newOrChangedCredentials } from "../../../Validation/validation.js";
+import Check from "@/Components/v1/Check/Check.jsx";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
+import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
+import IconBox from "@/Components/v1/IconBox/index.jsx";
+import LockIcon from "../../../assets/icons/lock.svg?react";
+import Logo from "../../../assets/icons/checkmate-icon.svg?react";
+import Background from "../../../assets/Images/background-grid.svg?react";
import "./index.css";
-import { useValidatePassword } from "./hooks/useValidatePassword";
+import { useValidatePassword } from "./hooks/useValidatePassword.jsx";
import { Trans, useTranslation } from "react-i18next";
const SetNewPassword = () => {
diff --git a/client/src/Pages/Auth/components/AuthHeader.jsx b/client/src/Pages/v1/Auth/components/AuthHeader.jsx
similarity index 82%
rename from client/src/Pages/Auth/components/AuthHeader.jsx
rename to client/src/Pages/v1/Auth/components/AuthHeader.jsx
index ce454c065..a34e83d6a 100644
--- a/client/src/Pages/Auth/components/AuthHeader.jsx
+++ b/client/src/Pages/v1/Auth/components/AuthHeader.jsx
@@ -1,9 +1,9 @@
// Components
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
-import Logo from "../../../assets/icons/checkmate-icon.svg?react";
-import LanguageSelector from "../../../Components/LanguageSelector";
-import ThemeSwitch from "../../../Components/ThemeSwitch";
+import Logo from "../../../../assets/icons/checkmate-icon.svg?react";
+import LanguageSelector from "../../../../Components/LanguageSelector.jsx";
+import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx";
// Utils
import { useTheme } from "@mui/material/styles";
diff --git a/client/src/Pages/Auth/components/AuthPageWrapper.jsx b/client/src/Pages/v1/Auth/components/AuthPageWrapper.jsx
similarity index 92%
rename from client/src/Pages/Auth/components/AuthPageWrapper.jsx
rename to client/src/Pages/v1/Auth/components/AuthPageWrapper.jsx
index b83be19c5..7e358e6ab 100644
--- a/client/src/Pages/Auth/components/AuthPageWrapper.jsx
+++ b/client/src/Pages/v1/Auth/components/AuthPageWrapper.jsx
@@ -1,10 +1,10 @@
-import Background from "../../../assets/Images/background-grid.svg?react";
+import Background from "../../../../assets/Images/background-grid.svg?react";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
-import AuthHeader from "../components/AuthHeader";
+import AuthHeader from "./AuthHeader.jsx";
import { useTheme } from "@mui/material/styles";
-import Logo from "../../../assets/icons/checkmate-icon.svg?react";
+import Logo from "../../../../assets/icons/checkmate-icon.svg?react";
import PropTypes from "prop-types";
const AuthPageWrapper = ({ children, heading, welcome }) => {
diff --git a/client/src/Pages/Auth/components/PasswordTooltip.jsx b/client/src/Pages/v1/Auth/components/PasswordTooltip.jsx
similarity index 98%
rename from client/src/Pages/Auth/components/PasswordTooltip.jsx
rename to client/src/Pages/v1/Auth/components/PasswordTooltip.jsx
index bc2f28ec7..2881eb001 100644
--- a/client/src/Pages/Auth/components/PasswordTooltip.jsx
+++ b/client/src/Pages/v1/Auth/components/PasswordTooltip.jsx
@@ -1,4 +1,4 @@
-import Check from "../../../Components/Check/Check";
+import Check from "@/Components/v1/Check/Check.jsx";
import Stack from "@mui/material/Stack";
import { Tooltip, useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
diff --git a/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx b/client/src/Pages/v1/Auth/hooks/usePasswordFeedback.jsx
similarity index 96%
rename from client/src/Pages/Auth/hooks/usePasswordFeedback.jsx
rename to client/src/Pages/v1/Auth/hooks/usePasswordFeedback.jsx
index 4159e6166..3c199e25f 100644
--- a/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx
+++ b/client/src/Pages/v1/Auth/hooks/usePasswordFeedback.jsx
@@ -1,5 +1,5 @@
import { useState } from "react";
-import { newOrChangedCredentials } from "../../../Validation/validation";
+import { newOrChangedCredentials } from "../../../../Validation/validation.js";
const usePasswordFeedback = () => {
const [feedback, setFeedback] = useState({});
diff --git a/client/src/Pages/Auth/hooks/useValidatePassword.jsx b/client/src/Pages/v1/Auth/hooks/useValidatePassword.jsx
similarity index 96%
rename from client/src/Pages/Auth/hooks/useValidatePassword.jsx
rename to client/src/Pages/v1/Auth/hooks/useValidatePassword.jsx
index 102d8d73e..231b8eeb8 100644
--- a/client/src/Pages/Auth/hooks/useValidatePassword.jsx
+++ b/client/src/Pages/v1/Auth/hooks/useValidatePassword.jsx
@@ -1,5 +1,5 @@
import { useMemo, useState } from "react";
-import { newOrChangedCredentials } from "../../../Validation/validation";
+import { newOrChangedCredentials } from "../../../../Validation/validation.js";
const getFeedbackStatus = (form, errors, field, criteria) => {
const fieldErrors = errors[field];
diff --git a/client/src/Pages/Auth/index.css b/client/src/Pages/v1/Auth/index.css
similarity index 100%
rename from client/src/Pages/Auth/index.css
rename to client/src/Pages/v1/Auth/index.css
diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/v1/Incidents/Components/IncidentTable/index.jsx
similarity index 89%
rename from client/src/Pages/Incidents/Components/IncidentTable/index.jsx
rename to client/src/Pages/v1/Incidents/Components/IncidentTable/index.jsx
index df9174290..df457c0e2 100644
--- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx
+++ b/client/src/Pages/v1/Incidents/Components/IncidentTable/index.jsx
@@ -1,26 +1,26 @@
//Components
import Stack from "@mui/material/Stack";
-import DataTable from "../../../../Components/Table";
+import DataTable from "@/Components/v1/Table/index.jsx";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableRow from "@mui/material/TableRow";
import TableCell from "@mui/material/TableCell";
-import TableSkeleton from "../../../../Components/Table/skeleton";
-import Pagination from "../../../../Components/Table/TablePagination";
-import { StatusLabel } from "../../../../Components/Label";
-import { HttpStatusLabel } from "../../../../Components/HttpStatusLabel";
-import GenericFallback from "../../../../Components/GenericFallback";
-import NetworkError from "../../../../Components/GenericFallback/NetworkError";
+import TableSkeleton from "@/Components/v1/Table/skeleton.jsx";
+import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
+import { StatusLabel } from "@/Components/v1/Label/index.jsx";
+import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx";
+import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
+import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx";
//Utils
-import { formatDateWithTz } from "../../../../Utils/timeUtils";
+import { formatDateWithTz } from "../../../../../Utils/timeUtils.js";
import { useSelector } from "react-redux";
import { useState } from "react";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
-import { useFetchChecksTeam } from "../../../../Hooks/checkHooks";
-import { useFetchChecksByMonitor } from "../../../../Hooks/checkHooks";
-import { useResolveIncident } from "../../../../Hooks/checkHooks";
+import { useFetchChecksTeam } from "../../../../../Hooks/v1/checkHooks.js";
+import { useFetchChecksByMonitor } from "../../../../../Hooks/v1/checkHooks.js";
+import { useResolveIncident } from "../../../../../Hooks/v1/checkHooks.js";
import { Button, Typography, useTheme } from "@mui/material";
import { lighten } from "@mui/material/styles";
diff --git a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx b/client/src/Pages/v1/Incidents/Components/OptionsHeader/index.jsx
similarity index 97%
rename from client/src/Pages/Incidents/Components/OptionsHeader/index.jsx
rename to client/src/Pages/v1/Incidents/Components/OptionsHeader/index.jsx
index 259880223..c16f54f18 100644
--- a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx
+++ b/client/src/Pages/v1/Incidents/Components/OptionsHeader/index.jsx
@@ -1,11 +1,11 @@
// Components
import { Stack, Typography, Button, ButtonGroup } from "@mui/material";
-import Select from "../../../../Components/Inputs/Select";
+import Select from "@/Components/v1/Inputs/Select/index.jsx";
import PropTypes from "prop-types";
//Utils
import { useTheme } from "@emotion/react";
-import SkeletonLayout from "./skeleton";
+import SkeletonLayout from "./skeleton.jsx";
import { useTranslation } from "react-i18next";
const OptionsHeader = ({
diff --git a/client/src/Pages/Incidents/Components/OptionsHeader/skeleton.jsx b/client/src/Pages/v1/Incidents/Components/OptionsHeader/skeleton.jsx
similarity index 100%
rename from client/src/Pages/Incidents/Components/OptionsHeader/skeleton.jsx
rename to client/src/Pages/v1/Incidents/Components/OptionsHeader/skeleton.jsx
diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx b/client/src/Pages/v1/Incidents/Components/StatusBoxes/StatusBox.jsx
similarity index 87%
rename from client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx
rename to client/src/Pages/v1/Incidents/Components/StatusBoxes/StatusBox.jsx
index 7b20e3d51..cd7078e6c 100644
--- a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx
+++ b/client/src/Pages/v1/Incidents/Components/StatusBoxes/StatusBox.jsx
@@ -1,11 +1,11 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
-import Background from "../../../../assets/Images/background-grid.svg?react";
-import AlertIcon from "../../../../assets/icons/alert-icon.svg?react";
-import CheckIcon from "../../../../assets/icons/check-icon.svg?react";
-import CloseIcon from "../../../../assets/icons/close-icon.svg?react";
-import WarningIcon from "../../../../assets/icons/warning-icon.svg?react";
+import Background from "../../../../../assets/Images/background-grid.svg?react";
+import AlertIcon from "../../../../../assets/icons/alert-icon.svg?react";
+import CheckIcon from "../../../../../assets/icons/check-icon.svg?react";
+import CloseIcon from "../../../../../assets/icons/close-icon.svg?react";
+import WarningIcon from "../../../../../assets/icons/warning-icon.svg?react";
const StatusBox = ({ title, value, status }) => {
const theme = useTheme();
diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx b/client/src/Pages/v1/Incidents/Components/StatusBoxes/index.jsx
similarity index 87%
rename from client/src/Pages/Incidents/Components/StatusBoxes/index.jsx
rename to client/src/Pages/v1/Incidents/Components/StatusBoxes/index.jsx
index 6ba2f1820..bb993111a 100644
--- a/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx
+++ b/client/src/Pages/v1/Incidents/Components/StatusBoxes/index.jsx
@@ -1,9 +1,9 @@
import PropTypes from "prop-types";
import { Stack } from "@mui/material";
-import StatusBox from "./StatusBox";
+import StatusBox from "./StatusBox.jsx";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
-import SkeletonLayout from "./skeleton";
+import SkeletonLayout from "./skeleton.jsx";
const StatusBoxes = ({ isLoading, summary }) => {
const theme = useTheme();
@@ -17,7 +17,7 @@ const StatusBoxes = ({ isLoading, summary }) => {
>
{
diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx b/client/src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx
similarity index 92%
rename from client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx
rename to client/src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx
index 2ed48bf64..570f38a7e 100644
--- a/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx
+++ b/client/src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx
@@ -1,8 +1,8 @@
import { Box, Stack, Tooltip, Typography } from "@mui/material";
-import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils";
+import { useMonitorUtils } from "../../../../../Hooks/v1/useMonitorUtils.js";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
-import PulseDot from "../../../../Components/Animated/PulseDot";
+import PulseDot from "@/Components/v1/Animated/PulseDot.jsx";
import PropTypes from "prop-types";
const MonitorStatusHeader = ({ monitor, infrastructureMonitor }) => {
const theme = useTheme();
diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx b/client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx
similarity index 82%
rename from client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx
rename to client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx
index fa8a6ebe9..f0a701517 100644
--- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx
+++ b/client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useCallback } from "react";
const useInfrastructureMonitorForm = () => {
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
url: "",
@@ -6,6 +6,8 @@ const useInfrastructureMonitorForm = () => {
notifications: [],
notify_email: false,
interval: 0.25,
+ statusWindowSize: 5,
+ statusWindowThreshold: 60,
cpu: false,
usage_cpu: "",
memory: false,
@@ -29,9 +31,10 @@ const useInfrastructureMonitorForm = () => {
[event.target.name]: event.target.checked,
});
};
- const initializeInfrastructureMonitorForCreate = (globalSettings) => {
+ const initializeInfrastructureMonitorForCreate = useCallback((globalSettings) => {
const gt = globalSettings?.data?.settings?.globalThresholds || {};
- setInfrastructureMonitor({
+ setInfrastructureMonitor((prev) => ({
+ ...prev,
url: "",
name: "",
notifications: [],
@@ -45,17 +48,20 @@ const useInfrastructureMonitorForm = () => {
temperature: gt.temperature !== undefined,
usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "",
secret: "",
- });
- };
+ }));
+ }, []);
- const initializeInfrastructureMonitorForUpdate = (monitor) => {
+ const initializeInfrastructureMonitorForUpdate = useCallback((monitor) => {
const MS_PER_MINUTE = 60000;
const { thresholds = {} } = monitor;
- setInfrastructureMonitor({
+ setInfrastructureMonitor((prev) => ({
+ ...prev,
url: monitor.url.replace(/^https?:\/\//, ""),
name: monitor.name || "",
notifications: monitor.notifications || [],
interval: monitor.interval / MS_PER_MINUTE,
+ statusWindowSize: monitor.statusWindowSize,
+ statusWindowThreshold: monitor.statusWindowThreshold,
cpu: thresholds.usage_cpu !== undefined,
usage_cpu:
thresholds.usage_cpu !== undefined ? (thresholds.usage_cpu * 100).toString() : "",
@@ -75,8 +81,8 @@ const useInfrastructureMonitorForm = () => {
? (thresholds.usage_temperature * 100).toString()
: "",
secret: monitor.secret || "",
- });
- };
+ }));
+ }, []);
return {
infrastructureMonitor,
setInfrastructureMonitor,
diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx b/client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx
similarity index 90%
rename from client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx
rename to client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx
index da564ae7c..8a97486c7 100644
--- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx
+++ b/client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx
@@ -1,4 +1,7 @@
-import { useCreateMonitor, useUpdateMonitor } from "../../../../Hooks/monitorHooks";
+import {
+ useCreateMonitor,
+ useUpdateMonitor,
+} from "../../../../../Hooks/v1/monitorHooks.js";
const useInfrastructureSubmit = () => {
const [createMonitor, isCreating] = useCreateMonitor();
const [updateMonitor, isUpdating] = useUpdateMonitor();
@@ -12,6 +15,8 @@ const useInfrastructureSubmit = () => {
? infrastructureMonitor.url
: infrastructureMonitor.name,
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
+ statusWindowSize: infrastructureMonitor.statusWindowSize,
+ statusWindowThreshold: infrastructureMonitor.statusWindowThreshold,
cpu: infrastructureMonitor.cpu,
...(infrastructureMonitor.cpu
? { usage_cpu: infrastructureMonitor.usage_cpu }
diff --git a/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx b/client/src/Pages/v1/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx
similarity index 85%
rename from client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx
rename to client/src/Pages/v1/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx
index 080f441c4..73097d0d8 100644
--- a/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx
+++ b/client/src/Pages/v1/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx
@@ -1,6 +1,6 @@
import { useState } from "react";
-import { infrastructureMonitorValidation } from "../../../../Validation/validation";
-import { createToast } from "../../../../Utils/toastUtils";
+import { infrastructureMonitorValidation } from "../../../../../Validation/validation.js";
+import { createToast } from "../../../../../Utils/toastUtils.jsx";
const useValidateInfrastructureForm = () => {
const [errors, setErrors] = useState({});
diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/v1/Infrastructure/Create/index.jsx
similarity index 81%
rename from client/src/Pages/Infrastructure/Create/index.jsx
rename to client/src/Pages/v1/Infrastructure/Create/index.jsx
index 28007c538..d4fd859ec 100644
--- a/client/src/Pages/Infrastructure/Create/index.jsx
+++ b/client/src/Pages/v1/Infrastructure/Create/index.jsx
@@ -1,18 +1,18 @@
//Components
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import ConfigBox from "../../../Components/ConfigBox";
-import FieldWrapper from "../../../Components/Inputs/FieldWrapper";
-import Link from "../../../Components/Link";
-import Select from "../../../Components/Inputs/Select";
-import TextInput from "../../../Components/Inputs/TextInput";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
+import FieldWrapper from "@/Components/v1/Inputs/FieldWrapper/index.jsx";
+import Link from "@/Components/v1/Link/index.jsx";
+import Select from "@/Components/v1/Inputs/Select/index.jsx";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
-import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
-import MonitorStatusHeader from "./Components/MonitorStatusHeader";
-import MonitorActionButtons from "./Components/MonitorActionButtons";
-import CustomAlertsSection from "./Components/CustomAlertsSection";
+import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
+import MonitorStatusHeader from "./Components/MonitorStatusHeader.jsx";
+import MonitorActionButtons from "./Components/MonitorActionButtons.jsx";
+import CustomAlertsSection from "./Components/CustomAlertsSection.jsx";
// Utils
-import NotificationsConfig from "../../../Components/NotificationConfig";
-import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
+import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx";
+import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js";
import { useParams } from "react-router-dom";
import { useState, useEffect } from "react";
import { useTheme } from "@emotion/react";
@@ -22,10 +22,10 @@ import {
useFetchGlobalSettings,
useFetchHardwareMonitorById,
usePauseMonitor,
-} from "../../../Hooks/monitorHooks";
-import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm";
-import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm";
-import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit";
+} from "../../../../Hooks/v1/monitorHooks.js";
+import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm.jsx";
+import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm.jsx";
+import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit.jsx";
const CreateInfrastructureMonitor = () => {
const { monitorId } = useParams();
@@ -86,7 +86,14 @@ const CreateInfrastructureMonitor = () => {
setHttps(monitor.url.startsWith("https"));
initializeInfrastructureMonitorForUpdate(monitor);
}
- }, [isCreate, monitor, globalSettings, globalSettingsLoading]);
+ }, [
+ isCreate,
+ monitor,
+ globalSettings,
+ globalSettingsLoading,
+ initializeInfrastructureMonitorForCreate,
+ initializeInfrastructureMonitorForUpdate,
+ ]);
// Handlers
const onSubmit = async (event) => {
@@ -281,6 +288,39 @@ const CreateInfrastructureMonitor = () => {
setNotifications={infrastructureMonitor.notifications}
/>
+
+
+
+ {t("createMonitorPage.incidentConfigTitle")}
+
+
+ {t("createMonitorPage.incidentConfigDescription")}
+
+
+
+
+
+
+
{
const theme = useTheme();
const { getDimensions } = useHardwareUtils();
diff --git a/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx
similarity index 93%
rename from client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx
rename to client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx
index 0eb3aa36d..3465683d9 100644
--- a/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx
+++ b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx
@@ -1,7 +1,7 @@
// Components
import { Stack } from "@mui/material";
-import InfraAreaChart from "./InfraAreaChart";
-import SkeletonLayout from "./skeleton";
+import InfraAreaChart from "./InfraAreaChart.jsx";
+import SkeletonLayout from "./skeleton.jsx";
// Utils
import {
@@ -9,9 +9,9 @@ import {
TzTick,
InfrastructureTooltip,
TemperatureTooltip,
-} from "../../../../../Components/Charts/Utils/chartUtils";
+} from "@/Components/v1/Charts/Utils/chartUtils.jsx";
import { useTheme } from "@emotion/react";
-import { useHardwareUtils } from "../../Hooks/useHardwareUtils";
+import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
import { useTranslation } from "react-i18next";
const AreaChartBoxes = ({ shouldRender, monitor, dateRange }) => {
const theme = useTheme();
diff --git a/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx
similarity index 100%
rename from client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx
rename to client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx
diff --git a/client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/BaseContainer/index.jsx
similarity index 94%
rename from client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx
rename to client/src/Pages/v1/Infrastructure/Details/Components/BaseContainer/index.jsx
index 246769751..dd73d1a65 100644
--- a/client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx
+++ b/client/src/Pages/v1/Infrastructure/Details/Components/BaseContainer/index.jsx
@@ -11,7 +11,7 @@ import { Box } from "@mui/material";
// Utils
import { useTheme } from "@emotion/react";
-import { useHardwareUtils } from "../../Hooks/useHardwareUtils";
+import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
import PropTypes from "prop-types";
const BaseContainer = ({ children, sx = {} }) => {
diff --git a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx
similarity index 93%
rename from client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx
rename to client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx
index 70d36bd00..cd61b7109 100644
--- a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx
+++ b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx
@@ -1,6 +1,6 @@
// Components
-import CustomGauge from "../../../../../Components/Charts/CustomGauge";
-import BaseContainer from "../BaseContainer";
+import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx";
+import BaseContainer from "../BaseContainer/index.jsx";
import { Stack, Typography, Box } from "@mui/material";
// Utils
import { useTheme } from "@emotion/react";
diff --git a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx
similarity index 93%
rename from client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx
rename to client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx
index 3c1c68f8b..432881e7e 100644
--- a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx
+++ b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx
@@ -1,11 +1,11 @@
// Components
import { Stack } from "@mui/material";
-import Gauge from "./Gauge";
-import SkeletonLayout from "./skeleton";
+import Gauge from "./Gauge.jsx";
+import SkeletonLayout from "./skeleton.jsx";
import PropTypes from "prop-types";
// Utils
-import { useHardwareUtils } from "../../Hooks/useHardwareUtils";
+import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
diff --git a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx
similarity index 100%
rename from client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx
rename to client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx
diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx
similarity index 93%
rename from client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx
rename to client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx
index 9157a3432..d9f93311c 100644
--- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx
+++ b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx
@@ -1,15 +1,15 @@
import PropTypes from "prop-types";
import { Stack, Typography } from "@mui/material";
-import InfraAreaChart from "../../../../../Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart";
+import InfraAreaChart from "../AreaChartBoxes/InfraAreaChart.jsx";
import {
TzTick,
InfrastructureTooltip,
NetworkTick,
-} from "../../../../../Components/Charts/Utils/chartUtils";
+} from "@/Components/v1/Charts/Utils/chartUtils.jsx";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
-import { useHardwareUtils } from "../../Hooks/useHardwareUtils";
+import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
const NetworkCharts = ({ ethernetData, dateRange }) => {
const theme = useTheme();
diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx
similarity index 91%
rename from client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx
rename to client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx
index 98ba90dae..0747dfcd1 100644
--- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx
+++ b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx
@@ -1,9 +1,9 @@
import PropTypes from "prop-types";
-import StatusBoxes from "../../../../../Components/StatusBoxes";
-import StatBox from "../../../../../Components/StatBox";
+import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
+import StatBox from "@/Components/v1/StatBox/index.jsx";
import { Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
-import { useHardwareUtils } from "../../Hooks/useHardwareUtils";
+import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
function formatNumber(num) {
return num != null ? num.toLocaleString() : "0";
diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx
similarity index 91%
rename from client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx
rename to client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx
index 41d2b0a87..78db333e6 100644
--- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx
+++ b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx
@@ -3,10 +3,10 @@ import { useState, useEffect } from "react";
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";
+import Select from "@/Components/v1/Inputs/Select/index.jsx";
+import NetworkStatBoxes from "./NetworkStatBoxes.jsx";
+import NetworkCharts from "./NetworkCharts.jsx";
+import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx";
const getAvailableInterfaces = (net) => {
return (net || []).map((iface) => iface.name).filter(Boolean);
diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/skeleton.jsx
similarity index 100%
rename from client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx
rename to client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/skeleton.jsx
diff --git a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx
similarity index 90%
rename from client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx
rename to client/src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx
index 6c11e7c82..7f09e82d5 100644
--- a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx
+++ b/client/src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx
@@ -1,11 +1,11 @@
// Components
import { Stack, Typography } from "@mui/material";
-import StatusBoxes from "../../../../../Components/StatusBoxes";
-import StatBox from "../../../../../Components/StatBox";
+import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
+import StatBox from "@/Components/v1/StatBox/index.jsx";
//Utils
-import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
-import { useHardwareUtils } from "../../Hooks/useHardwareUtils";
+import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js";
+import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx";
import { useTranslation } from "react-i18next";
const InfraStatBoxes = ({ shouldRender, monitor }) => {
diff --git a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx b/client/src/Pages/v1/Infrastructure/Details/Hooks/useHardwareUtils.jsx
similarity index 100%
rename from client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx
rename to client/src/Pages/v1/Infrastructure/Details/Hooks/useHardwareUtils.jsx
diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/v1/Infrastructure/Details/index.jsx
similarity index 79%
rename from client/src/Pages/Infrastructure/Details/index.jsx
rename to client/src/Pages/v1/Infrastructure/Details/index.jsx
index a16f3906b..f9c51c27d 100644
--- a/client/src/Pages/Infrastructure/Details/index.jsx
+++ b/client/src/Pages/v1/Infrastructure/Details/index.jsx
@@ -1,20 +1,20 @@
// Components
import { Stack, Typography, Tab } from "@mui/material";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader";
-import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
-import StatusBoxes from "./Components/StatusBoxes";
-import GaugeBoxes from "./Components/GaugeBoxes";
-import AreaChartBoxes from "./Components/AreaChartBoxes";
-import GenericFallback from "../../../Components/GenericFallback";
-import NetworkStats from "./Components/NetworkStats";
-import CustomTabList from "../../../Components/Tab";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx";
+import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx";
+import StatusBoxes from "./Components/StatusBoxes/index.jsx";
+import GaugeBoxes from "./Components/GaugeBoxes/index.jsx";
+import AreaChartBoxes from "./Components/AreaChartBoxes/index.jsx";
+import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
+import NetworkStats from "./Components/NetworkStats/index.jsx";
+import CustomTabList from "@/Components/v1/Tab/index.jsx";
import TabContext from "@mui/lab/TabContext";
// Utils
import { useTheme } from "@emotion/react";
-import { useIsAdmin } from "../../../Hooks/useIsAdmin";
-import { useFetchHardwareMonitorById } from "../../../Hooks/monitorHooks";
+import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js";
+import { useFetchHardwareMonitorById } from "../../../../Hooks/v1/monitorHooks.js";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
diff --git a/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx b/client/src/Pages/v1/Infrastructure/Monitors/Components/Filters/index.jsx
similarity index 97%
rename from client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx
rename to client/src/Pages/v1/Infrastructure/Monitors/Components/Filters/index.jsx
index 3c2511c2f..70bf502af 100644
--- a/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx
+++ b/client/src/Pages/v1/Infrastructure/Monitors/Components/Filters/index.jsx
@@ -1,6 +1,6 @@
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
-import FilterHeader from "../../../../../Components/FilterHeader";
+import FilterHeader from "@/Components/v1/FilterHeader/index.jsx";
import { useMemo } from "react";
import { Box, Button } from "@mui/material";
import ClearIcon from "@mui/icons-material/Clear";
diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx b/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx
similarity index 87%
rename from client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx
rename to client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx
index 7ee603ef7..cc2cd5f98 100644
--- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx
+++ b/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx
@@ -1,18 +1,18 @@
// Components
import { Box } from "@mui/material";
-import DataTable from "../../../../../Components/Table";
-import Host from "../../../../../Components/Host";
-import { StatusLabel } from "../../../../../Components/Label";
+import DataTable from "@/Components/v1/Table/index.jsx";
+import Host from "@/Components/v1/Host/index.jsx";
+import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import { Stack } from "@mui/material";
-import { InfrastructureMenu } from "../MonitorsTableMenu";
-import LoadingSpinner from "../../../../Uptime/Monitors/Components/LoadingSpinner";
+import { InfrastructureMenu } from "../MonitorsTableMenu/index.jsx";
+import LoadingSpinner from "../../../../Uptime/Monitors/Components/LoadingSpinner/index.jsx";
// Assets
-import CPUChipIcon from "../../../../../assets/icons/cpu-chip.svg?react";
-import CustomGauge from "../../../../../Components/Charts/CustomGauge";
+import CPUChipIcon from "../../../../../../assets/icons/cpu-chip.svg?react";
+import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
-import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
+import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx b/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx
similarity index 93%
rename from client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx
rename to client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx
index 147f1c297..d3af34d00 100644
--- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx
+++ b/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx
@@ -3,13 +3,13 @@
import { useRef, useState } from "react";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
-import { createToast } from "../../../../../Utils/toastUtils";
+import { createToast } from "../../../../../../Utils/toastUtils.jsx";
import { IconButton, Menu, MenuItem } from "@mui/material";
-import Settings from "../../../../../assets/icons/settings-bold.svg?react";
+import Settings from "../../../../../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
-import Dialog from "../../../../../Components/Dialog";
-import { networkService } from "../../../../../Utils/NetworkService.js";
-import { usePauseMonitor } from "../../../../../Hooks/monitorHooks";
+import Dialog from "@/Components/v1/Dialog/index.jsx";
+import { networkService } from "../../../../../../Utils/NetworkService.js";
+import { usePauseMonitor } from "../../../../../../Hooks/v1/monitorHooks.js";
import { useTranslation } from "react-i18next";
/**
diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/v1/Infrastructure/Monitors/index.jsx
similarity index 81%
rename from client/src/Pages/Infrastructure/Monitors/index.jsx
rename to client/src/Pages/v1/Infrastructure/Monitors/index.jsx
index bfd2270fe..6c3a657ab 100644
--- a/client/src/Pages/Infrastructure/Monitors/index.jsx
+++ b/client/src/Pages/v1/Infrastructure/Monitors/index.jsx
@@ -1,21 +1,21 @@
// Components
import { Stack } from "@mui/material";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import MonitorCountHeader from "../../../Components/MonitorCountHeader";
-import MonitorCreateHeader from "../../../Components/MonitorCreateHeader";
-import MonitorsTable from "./Components/MonitorsTable";
-import Pagination from "../../..//Components/Table/TablePagination";
-import PageStateWrapper from "../../../Components/PageStateWrapper";
-import Filter from "./Components/Filters";
-import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx";
+import MonitorCreateHeader from "@/Components/v1/MonitorCreateHeader/index.jsx";
+import MonitorsTable from "./Components/MonitorsTable/index.jsx";
+import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
+import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
+import Filter from "./Components/Filters/index.jsx";
+import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { useEffect, useState } from "react";
-import { useIsAdmin } from "../../../Hooks/useIsAdmin";
+import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js";
import { useTranslation } from "react-i18next";
-import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks";
+import { useFetchMonitorsByTeamId } from "../../../../Hooks/v1/monitorHooks.js";
import { useDispatch, useSelector } from "react-redux";
-import { setRowsPerPage } from "../../../Features/UI/uiSlice";
+import { setRowsPerPage } from "../../../../Features/UI/uiSlice.js";
// Constants
const TYPES = ["hardware"];
const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }];
diff --git a/client/src/Pages/Integrations/index.css b/client/src/Pages/v1/Integrations/index.css
similarity index 100%
rename from client/src/Pages/Integrations/index.css
rename to client/src/Pages/v1/Integrations/index.css
diff --git a/client/src/Pages/Integrations/index.jsx b/client/src/Pages/v1/Integrations/index.jsx
similarity index 94%
rename from client/src/Pages/Integrations/index.jsx
rename to client/src/Pages/v1/Integrations/index.jsx
index fe0faa5cf..dde6b52ee 100644
--- a/client/src/Pages/Integrations/index.jsx
+++ b/client/src/Pages/v1/Integrations/index.jsx
@@ -1,9 +1,9 @@
import PropTypes from "prop-types";
import { Stack, Typography, Grid, Button } from "@mui/material";
import { useTheme } from "@emotion/react";
-import Discord from "../../assets/icons/discord-icon.svg?react";
-import Slack from "../../assets/icons/slack-icon.svg?react";
-import Zapier from "../../assets/icons/zapier-icon.svg?react";
+import Discord from "../../../assets/icons/discord-icon.svg?react";
+import Slack from "../../../assets/icons/slack-icon.svg?react";
+import Zapier from "../../../assets/icons/zapier-icon.svg?react";
import { useTranslation } from "react-i18next";
import "./index.css";
diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/v1/Logs/Diagnostics/components/gauges/index.jsx
similarity index 96%
rename from client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx
rename to client/src/Pages/v1/Logs/Diagnostics/components/gauges/index.jsx
index 5bb227b7c..b38837cf2 100644
--- a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx
+++ b/client/src/Pages/v1/Logs/Diagnostics/components/gauges/index.jsx
@@ -1,11 +1,11 @@
import Stack from "@mui/material/Stack";
-import CustomGauge from "../../../../../Components/Charts/CustomGauge";
+import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx";
import Typography from "@mui/material/Typography";
// Utils
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
-import { getPercentage, formatBytes } from "../../utils/utils";
+import { getPercentage, formatBytes } from "../../utils/utils.js";
import { useTranslation } from "react-i18next";
import { Box } from "@mui/material";
diff --git a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx b/client/src/Pages/v1/Logs/Diagnostics/components/stats/index.jsx
similarity index 94%
rename from client/src/Pages/Logs/Diagnostics/components/stats/index.jsx
rename to client/src/Pages/v1/Logs/Diagnostics/components/stats/index.jsx
index 103d1c698..b99f34506 100644
--- a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx
+++ b/client/src/Pages/v1/Logs/Diagnostics/components/stats/index.jsx
@@ -7,8 +7,8 @@ import CircularProgress from "@mui/material/CircularProgress";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
-import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
-import { formatBytes } from "../../utils/utils";
+import { getHumanReadableDuration } from "../../../../../../Utils/timeUtils.js";
+import { formatBytes } from "../../utils/utils.js";
import { useTranslation } from "react-i18next";
const StatsCard = ({ title, value, unit = "", isLoading }) => {
diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/v1/Logs/Diagnostics/index.jsx
similarity index 83%
rename from client/src/Pages/Logs/Diagnostics/index.jsx
rename to client/src/Pages/v1/Logs/Diagnostics/index.jsx
index 72ee47f48..54feee516 100644
--- a/client/src/Pages/Logs/Diagnostics/index.jsx
+++ b/client/src/Pages/v1/Logs/Diagnostics/index.jsx
@@ -1,14 +1,14 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
-import Gauges from "./components/gauges";
+import Gauges from "./components/gauges/index.jsx";
import Button from "@mui/material/Button";
-import StatBox from "../../../Components/StatBox";
-import StatusBoxes from "../../../Components/StatusBoxes";
+import StatBox from "@/Components/v1/StatBox/index.jsx";
+import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
-import { useFetchDiagnostics } from "../../../Hooks/logHooks";
-import { getHumanReadableDuration } from "../../../Utils/timeUtils";
-import { formatBytes, getPercentage } from "./utils/utils";
+import { useFetchDiagnostics } from "../../../../Hooks/v1/logHooks.js";
+import { getHumanReadableDuration } from "../../../../Utils/timeUtils.js";
+import { formatBytes, getPercentage } from "./utils/utils.js";
const Diagnostics = () => {
// Local state
diff --git a/client/src/Pages/Logs/Diagnostics/utils/utils.js b/client/src/Pages/v1/Logs/Diagnostics/utils/utils.js
similarity index 100%
rename from client/src/Pages/Logs/Diagnostics/utils/utils.js
rename to client/src/Pages/v1/Logs/Diagnostics/utils/utils.js
diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/v1/Logs/Logs/index.jsx
similarity index 95%
rename from client/src/Pages/Logs/Logs/index.jsx
rename to client/src/Pages/v1/Logs/Logs/index.jsx
index b44b3fd1a..64c4cb97a 100644
--- a/client/src/Pages/Logs/Logs/index.jsx
+++ b/client/src/Pages/v1/Logs/Logs/index.jsx
@@ -1,10 +1,10 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
-import Select from "../../../Components/Inputs/Select";
+import Select from "@/Components/v1/Inputs/Select/index.jsx";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";
-import { useFetchLogs } from "../../../Hooks/logHooks";
+import { useFetchLogs } from "../../../../Hooks/v1/logHooks.js";
import { useTheme } from "@emotion/react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
diff --git a/client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx b/client/src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx
similarity index 93%
rename from client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx
rename to client/src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx
index 94fc2f5c3..743f52639 100644
--- a/client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx
+++ b/client/src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx
@@ -1,10 +1,10 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
-import DataTable from "../../../../../Components/Table";
+import DataTable from "@/Components/v1/Table/index.jsx";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
-import { TypeToPathMap } from "../../../../../Utils/monitorUtils";
+import { TypeToPathMap } from "../../../../../../Utils/monitorUtils.js";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
diff --git a/client/src/Pages/Logs/Queue/components/JobTable/index.jsx b/client/src/Pages/v1/Logs/Queue/components/JobTable/index.jsx
similarity index 93%
rename from client/src/Pages/Logs/Queue/components/JobTable/index.jsx
rename to client/src/Pages/v1/Logs/Queue/components/JobTable/index.jsx
index fab85ec5d..7d309e845 100644
--- a/client/src/Pages/Logs/Queue/components/JobTable/index.jsx
+++ b/client/src/Pages/v1/Logs/Queue/components/JobTable/index.jsx
@@ -1,13 +1,13 @@
import Stack from "@mui/material/Stack";
-import DataTable from "../../../../../Components/Table";
+import DataTable from "@/Components/v1/Table/index.jsx";
import Typography from "@mui/material/Typography";
// Utils
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
-import { TypeToPathMap } from "../../../../../Utils/monitorUtils";
+import { TypeToPathMap } from "../../../../../../Utils/monitorUtils.js";
import { useTranslation } from "react-i18next";
-import { createHeaderFactory } from "../../../../../Components/Table/TableUtils";
+import { createHeaderFactory } from "@/Components/v1/Table/TableUtils.js";
const JobTable = ({ jobs = [] }) => {
const theme = useTheme();
diff --git a/client/src/Pages/Logs/Queue/components/Metrics/index.jsx b/client/src/Pages/v1/Logs/Queue/components/Metrics/index.jsx
similarity index 86%
rename from client/src/Pages/Logs/Queue/components/Metrics/index.jsx
rename to client/src/Pages/v1/Logs/Queue/components/Metrics/index.jsx
index b0112fa99..c4f4389c3 100644
--- a/client/src/Pages/Logs/Queue/components/Metrics/index.jsx
+++ b/client/src/Pages/v1/Logs/Queue/components/Metrics/index.jsx
@@ -1,6 +1,6 @@
import Stack from "@mui/material/Stack";
-import StatBox from "../../../../../Components/StatBox";
-import StatusBoxes from "../../../../../Components/StatusBoxes";
+import StatBox from "@/Components/v1/StatBox/index.jsx";
+import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
diff --git a/client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx b/client/src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx
similarity index 95%
rename from client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx
rename to client/src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx
index 204b90035..d96f37a20 100644
--- a/client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx
+++ b/client/src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx
@@ -1,6 +1,6 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
-import DataTable from "../../../../../Components/Table";
+import DataTable from "@/Components/v1/Table/index.jsx";
// Utils
import { useTranslation } from "react-i18next";
diff --git a/client/src/Pages/Logs/Queue/index.jsx b/client/src/Pages/v1/Logs/Queue/index.jsx
similarity index 86%
rename from client/src/Pages/Logs/Queue/index.jsx
rename to client/src/Pages/v1/Logs/Queue/index.jsx
index fde9073fc..10eca4d3b 100644
--- a/client/src/Pages/Logs/Queue/index.jsx
+++ b/client/src/Pages/v1/Logs/Queue/index.jsx
@@ -1,8 +1,8 @@
// Components
import Stack from "@mui/material/Stack";
-import JobTable from "./components/JobTable";
-import Metrics from "./components/Metrics";
-import FailedJobTable from "./components/FailedJobTable";
+import JobTable from "./components/JobTable/index.jsx";
+import Metrics from "./components/Metrics/index.jsx";
+import FailedJobTable from "./components/FailedJobTable/index.jsx";
import ButtonGroup from "@mui/material/ButtonGroup";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
@@ -10,7 +10,7 @@ import Divider from "@mui/material/Divider";
// Utils
import { useState } from "react";
-import { useFetchQueueData, useFlushQueue } from "../../../Hooks/logHooks";
+import { useFetchQueueData, useFlushQueue } from "../../../../Hooks/v1/logHooks.js";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
diff --git a/client/src/Pages/Logs/index.jsx b/client/src/Pages/v1/Logs/index.jsx
similarity index 84%
rename from client/src/Pages/Logs/index.jsx
rename to client/src/Pages/v1/Logs/index.jsx
index 79c89f282..e5bdf623d 100644
--- a/client/src/Pages/Logs/index.jsx
+++ b/client/src/Pages/v1/Logs/index.jsx
@@ -1,10 +1,10 @@
import Stack from "@mui/material/Stack";
-import Breadcrumbs from "../../Components/Breadcrumbs";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
-import Queue from "./Queue";
-import LogsComponent from "./Logs";
-import Diagnostics from "./Diagnostics";
+import Queue from "./Queue/index.jsx";
+import LogsComponent from "./Logs/index.jsx";
+import Diagnostics from "./Diagnostics/index.jsx";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
diff --git a/client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx
similarity index 93%
rename from client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx
rename to client/src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx
index 1627ef3d5..8d4c0ae53 100644
--- a/client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx
+++ b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx
@@ -1,4 +1,4 @@
-import Select from "../../../../../Components/Inputs/Select";
+import Select from "@/Components/v1/Inputs/Select/index.jsx";
import PropTypes from "prop-types";
const ConfigSelect = ({ configSelection, valueSelect, onChange, ...props }) => {
const getValueById = (config, id) => {
diff --git a/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx
similarity index 100%
rename from client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx
rename to client/src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx
diff --git a/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx
similarity index 94%
rename from client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx
rename to client/src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx
index e792734ff..14920d81c 100644
--- a/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx
+++ b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx
@@ -1,7 +1,7 @@
import PropTypes from "prop-types";
-import Search from "../../../../../Components/Inputs/Search";
+import Search from "@/Components/v1/Inputs/Search/index.jsx";
import { useState } from "react";
-import ConfigRow from "../../../../../Components/ConfigRow";
+import ConfigRow from "@/Components/v1/ConfigRow/index.jsx";
/**
* MonitorsConfig is a component that allows users to select and apply a maintenance window to a list of monitors.
diff --git a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx
similarity index 93%
rename from client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx
rename to client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx
index 84333a14f..a75cbe206 100644
--- a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx
+++ b/client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx
@@ -1,4 +1,4 @@
-import { networkService } from "../../../../main";
+import { networkService } from "../../../../../main.jsx";
import dayjs from "dayjs";
import {
MS_PER_SECOND,
@@ -6,7 +6,7 @@ import {
MS_PER_HOUR,
MS_PER_DAY,
MS_PER_WEEK,
-} from "../../../../Utils/timeUtils";
+} from "../../../../../Utils/timeUtils.js";
const useMaintenanceActions = () => {
const MS_LOOKUP = {
diff --git a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx
similarity index 95%
rename from client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx
rename to client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx
index 8ba9b5e80..d21906e41 100644
--- a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx
+++ b/client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx
@@ -1,4 +1,4 @@
-import { networkService } from "../../../../main";
+import { networkService } from "../../../../../main.jsx";
import dayjs from "dayjs";
import {
MS_PER_SECOND,
@@ -6,7 +6,7 @@ import {
MS_PER_HOUR,
MS_PER_DAY,
MS_PER_WEEK,
-} from "../../../../Utils/timeUtils";
+} from "../../../../../Utils/timeUtils.js";
const useMaintenanceData = () => {
const REVERSE_REPEAT_LOOKUP = {
0: "none",
diff --git a/client/src/Pages/Maintenance/CreateMaintenance/index.css b/client/src/Pages/v1/Maintenance/CreateMaintenance/index.css
similarity index 100%
rename from client/src/Pages/Maintenance/CreateMaintenance/index.css
rename to client/src/Pages/v1/Maintenance/CreateMaintenance/index.css
diff --git a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx
similarity index 93%
rename from client/src/Pages/Maintenance/CreateMaintenance/index.jsx
rename to client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx
index 9dbf9d4aa..d0e7b03cd 100644
--- a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx
+++ b/client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx
@@ -2,27 +2,27 @@ import { Box, Button, Stack, Typography } from "@mui/material";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useEffect, useState } from "react";
-import ConfigBox from "../../../Components/ConfigBox";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
-import { maintenanceWindowValidation } from "../../../Validation/validation";
-import { createToast } from "../../../Utils/toastUtils";
-import MonitorList from "./Components/MonitorList";
-import ConfigSelect from "./Components/ConfigSelect";
-import useMaintenanceData from "./hooks/useMaintenanceData";
-import useMaintenanceActions from "./hooks/useMaintenanceActions";
+import { maintenanceWindowValidation } from "../../../../Validation/validation.js";
+import { createToast } from "../../../../Utils/toastUtils.jsx";
+import MonitorList from "./Components/MonitorList/index.jsx";
+import ConfigSelect from "./Components/ConfigSelect/index.jsx";
+import useMaintenanceData from "./hooks/useMaintenanceData.jsx";
+import useMaintenanceActions from "./hooks/useMaintenanceActions.jsx";
-import TextInput from "../../../Components/Inputs/TextInput";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import CalendarIcon from "../../../assets/icons/calendar.svg?react";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import CalendarIcon from "../../../../assets/icons/calendar.svg?react";
import "./index.css";
-import Search from "../../../Components/Inputs/Search";
-import { logger } from "../../../Utils/Logger";
+import Search from "@/Components/v1/Inputs/Search/index.jsx";
+import { logger } from "../../../../Utils/Logger.js";
import { useNavigate, useParams } from "react-router-dom";
-import { buildErrors, hasValidationErrors } from "../../../Validation/error";
+import { buildErrors, hasValidationErrors } from "../../../../Validation/error.js";
import { useTranslation } from "react-i18next";
import dayjs from "dayjs";
diff --git a/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx b/client/src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx
similarity index 92%
rename from client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx
rename to client/src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx
index aa1c48433..b2654da49 100644
--- a/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx
+++ b/client/src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx
@@ -3,14 +3,14 @@ import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { IconButton, Menu, MenuItem } from "@mui/material";
-import { logger } from "../../../../Utils/Logger";
-import Settings from "../../../../assets/icons/settings-bold.svg?react";
+import { logger } from "../../../../../Utils/Logger.js";
+import Settings from "../../../../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
-import { networkService } from "../../../../main";
-import { createToast } from "../../../../Utils/toastUtils";
+import { networkService } from "../../../../../main.jsx";
+import { createToast } from "../../../../../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
-import Dialog from "../../../../Components/Dialog";
+import Dialog from "@/Components/v1/Dialog/index.jsx";
const ActionsMenu = ({ /* isAdmin, */ maintenanceWindow, updateCallback }) => {
maintenanceWindow;
diff --git a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx b/client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx
similarity index 93%
rename from client/src/Pages/Maintenance/MaintenanceTable/index.jsx
rename to client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx
index bb5842e9f..f1d44f944 100644
--- a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx
+++ b/client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx
@@ -1,15 +1,15 @@
import PropTypes from "prop-types";
import { Box } from "@mui/material";
-import DataTable from "../../../Components/Table";
-import Pagination from "../../../Components/Table/TablePagination";
+import DataTable from "@/Components/v1/Table/index.jsx";
+import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
-import ActionsMenu from "./ActionsMenu";
+import ActionsMenu from "./ActionsMenu/index.jsx";
import { memo } from "react";
import { useDispatch, useSelector } from "react-redux";
-import { formatDurationRounded } from "../../../Utils/timeUtils";
-import { StatusLabel } from "../../../Components/Label";
-import { setRowsPerPage } from "../../../Features/UI/uiSlice";
+import { formatDurationRounded } from "../../../../Utils/timeUtils.js";
+import { StatusLabel } from "@/Components/v1/Label/index.jsx";
+import { setRowsPerPage } from "../../../../Features/UI/uiSlice.js";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
diff --git a/client/src/Components/ProgressStepper/index.css b/client/src/Pages/v1/Maintenance/index.css
similarity index 100%
rename from client/src/Components/ProgressStepper/index.css
rename to client/src/Pages/v1/Maintenance/index.css
diff --git a/client/src/Pages/Maintenance/index.jsx b/client/src/Pages/v1/Maintenance/index.jsx
similarity index 91%
rename from client/src/Pages/Maintenance/index.jsx
rename to client/src/Pages/v1/Maintenance/index.jsx
index ee142f524..cba0e9f18 100644
--- a/client/src/Pages/Maintenance/index.jsx
+++ b/client/src/Pages/v1/Maintenance/index.jsx
@@ -2,13 +2,13 @@ import { Stack, Button } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useState, useEffect } from "react";
import "./index.css";
-import MaintenanceTable from "./MaintenanceTable";
+import MaintenanceTable from "./MaintenanceTable/index.jsx";
import { useSelector } from "react-redux";
-import { networkService } from "../../main";
-import Breadcrumbs from "../../Components/Breadcrumbs";
+import { networkService } from "../../../main.jsx";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
-import PageStateWrapper from "../../Components/PageStateWrapper";
+import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
const Maintenance = () => {
const theme = useTheme();
diff --git a/client/src/Pages/NotFound/index.jsx b/client/src/Pages/v1/NotFound/index.jsx
similarity index 96%
rename from client/src/Pages/NotFound/index.jsx
rename to client/src/Pages/v1/NotFound/index.jsx
index fdae92cb5..48825c9f6 100644
--- a/client/src/Pages/NotFound/index.jsx
+++ b/client/src/Pages/v1/NotFound/index.jsx
@@ -1,5 +1,5 @@
import PropTypes from "prop-types";
-import NotFoundSvg from "../../../src/assets/Images/sushi_404.svg";
+import NotFoundSvg from "../../../assets/Images/sushi_404.svg";
import { Button, Stack, Typography } from "@mui/material";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
diff --git a/client/src/Pages/Notifications/components/ActionMenu.jsx b/client/src/Pages/v1/Notifications/components/ActionMenu.jsx
similarity index 100%
rename from client/src/Pages/Notifications/components/ActionMenu.jsx
rename to client/src/Pages/v1/Notifications/components/ActionMenu.jsx
diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/v1/Notifications/create/index.jsx
similarity index 92%
rename from client/src/Pages/Notifications/create/index.jsx
rename to client/src/Pages/v1/Notifications/create/index.jsx
index 6d8afe665..eb2b04aa6 100644
--- a/client/src/Pages/Notifications/create/index.jsx
+++ b/client/src/Pages/v1/Notifications/create/index.jsx
@@ -1,13 +1,13 @@
// Components
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import Button from "@mui/material/Button";
-import ConfigBox from "../../../Components/ConfigBox";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
import Box from "@mui/material/Box";
-import Select from "../../../Components/Inputs/Select";
-import TextInput from "../../../Components/Inputs/TextInput";
-import Dialog from "../../../Components/Dialog";
+import Select from "@/Components/v1/Inputs/Select/index.jsx";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
+import Dialog from "@/Components/v1/Dialog/index.jsx";
// Utils
import { useState } from "react";
@@ -18,9 +18,9 @@ import {
useEditNotification,
useTestNotification,
useDeleteNotification,
-} from "../../../Hooks/useNotifications";
-import { notificationValidation } from "../../../Validation/validation";
-import { createToast } from "../../../Utils/toastUtils";
+} from "../../../../Hooks/v1/useNotifications.js";
+import { notificationValidation } from "../../../../Validation/validation.js";
+import { createToast } from "../../../../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
import { useParams, useNavigate } from "react-router-dom";
import {
@@ -29,7 +29,7 @@ import {
DESCRIPTION_MAP,
LABEL_MAP,
PLACEHOLDER_MAP,
-} from "../utils";
+} from "../utils.js";
// Setup
diff --git a/client/src/Pages/Notifications/index.jsx b/client/src/Pages/v1/Notifications/index.jsx
similarity index 89%
rename from client/src/Pages/Notifications/index.jsx
rename to client/src/Pages/v1/Notifications/index.jsx
index 910fbf1fc..78ec6e9bb 100644
--- a/client/src/Pages/Notifications/index.jsx
+++ b/client/src/Pages/v1/Notifications/index.jsx
@@ -1,11 +1,11 @@
// Components
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
-import Breadcrumbs from "../../Components/Breadcrumbs";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import Button from "@mui/material/Button";
-import DataTable from "../../Components/Table";
-import ActionMenu from "./components/ActionMenu";
-import PageStateWrapper from "../../Components/PageStateWrapper";
+import DataTable from "@/Components/v1/Table/index.jsx";
+import ActionMenu from "./components/ActionMenu.jsx";
+import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
// Utils
import { useState } from "react";
@@ -14,7 +14,7 @@ import { useNavigate } from "react-router-dom";
import {
useGetNotificationsByTeamId,
useDeleteNotification,
-} from "../../Hooks/useNotifications";
+} from "../../../Hooks/v1/useNotifications.js";
import { useTranslation } from "react-i18next";
const Notifications = () => {
diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/v1/Notifications/utils.js
similarity index 100%
rename from client/src/Pages/Notifications/utils.js
rename to client/src/Pages/v1/Notifications/utils.js
diff --git a/client/src/Pages/Maintenance/index.css b/client/src/Pages/v1/PageSpeed/Create/index.css
similarity index 100%
rename from client/src/Pages/Maintenance/index.css
rename to client/src/Pages/v1/PageSpeed/Create/index.css
diff --git a/client/src/Pages/PageSpeed/Create/index.jsx b/client/src/Pages/v1/PageSpeed/Create/index.jsx
similarity index 92%
rename from client/src/Pages/PageSpeed/Create/index.jsx
rename to client/src/Pages/v1/PageSpeed/Create/index.jsx
index 46bc7f977..cb856bd37 100644
--- a/client/src/Pages/PageSpeed/Create/index.jsx
+++ b/client/src/Pages/v1/PageSpeed/Create/index.jsx
@@ -1,37 +1,37 @@
// Components
import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } from "@mui/material";
-import ConfigBox from "../../../Components/ConfigBox";
-import Select from "../../../Components/Inputs/Select";
-import TextInput from "../../../Components/Inputs/TextInput";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
+import Select from "@/Components/v1/Inputs/Select/index.jsx";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import PulseDot from "../../../Components/Animated/PulseDot";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import PulseDot from "@/Components/v1/Animated/PulseDot.jsx";
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
-import SkeletonLayout from "./skeleton";
-import NotificationsConfig from "../../../Components/NotificationConfig";
-import Dialog from "../../../Components/Dialog";
-import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
-import Radio from "../../../Components/Inputs/Radio";
+import SkeletonLayout from "./skeleton.jsx";
+import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx";
+import Dialog from "@/Components/v1/Dialog/index.jsx";
+import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
+import Radio from "@/Components/v1/Inputs/Radio/index.jsx";
// Utils
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
-import { monitorValidation } from "../../../Validation/validation";
-import { parseDomainName } from "../../../Utils/monitorUtils";
+import { monitorValidation } from "../../../../Validation/validation.js";
+import { parseDomainName } from "../../../../Utils/monitorUtils.js";
import { useTranslation } from "react-i18next";
-import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
+import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js";
import { useTheme } from "@emotion/react";
-import { createToast } from "../../../Utils/toastUtils";
+import { createToast } from "../../../../Utils/toastUtils.jsx";
import { useParams } from "react-router";
-import { useMonitorUtils } from "../../../Hooks/useMonitorUtils";
+import { useMonitorUtils } from "../../../../Hooks/v1/useMonitorUtils.js";
import {
useCreateMonitor,
useFetchMonitorById,
useDeleteMonitor,
useUpdateMonitor,
usePauseMonitor,
-} from "../../../Hooks/monitorHooks";
+} from "../../../../Hooks/v1/monitorHooks.js";
const PageSpeedSetup = () => {
const { monitorId } = useParams();
diff --git a/client/src/Pages/PageSpeed/Create/skeleton.jsx b/client/src/Pages/v1/PageSpeed/Create/skeleton.jsx
similarity index 100%
rename from client/src/Pages/PageSpeed/Create/skeleton.jsx
rename to client/src/Pages/v1/PageSpeed/Create/skeleton.jsx
diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChart.jsx
similarity index 99%
rename from client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx
rename to client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChart.jsx
index 40fec0ab6..5c79cc06c 100644
--- a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx
+++ b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChart.jsx
@@ -11,7 +11,7 @@ import {
import { useTheme } from "@emotion/react";
import { useMemo, useState } from "react";
import { Box, Stack, Typography } from "@mui/material";
-import { formatDateWithTz } from "../../../../../Utils/timeUtils";
+import { formatDateWithTz } from "../../../../../../Utils/timeUtils.js";
import { useSelector } from "react-redux";
const config = {
diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx
similarity index 85%
rename from client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx
rename to client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx
index 72811fa2a..c0da1600f 100644
--- a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx
+++ b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx
@@ -1,7 +1,7 @@
import { Box, Typography, Divider } from "@mui/material";
-import Checkbox from "../../../../../Components/Inputs/Checkbox";
-import MetricsIcon from "../../../../../assets/icons/ruler-icon.svg?react";
-import LegendBox from "../../../../../Components/Charts/LegendBox";
+import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx";
+import MetricsIcon from "../../../../../../assets/icons/ruler-icon.svg?react";
+import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChart.jsx
similarity index 100%
rename from client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx
rename to client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChart.jsx
diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx
similarity index 93%
rename from client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx
rename to client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx
index 18ca0733c..1297bd699 100644
--- a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx
+++ b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx
@@ -1,7 +1,7 @@
import { Stack, Box, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
-import LegendBox from "../../../../../Components/Charts/LegendBox";
-import SpeedometerIcon from "../../../../../assets/icons/speedometer-icon.svg?react";
+import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx";
+import SpeedometerIcon from "../../../../../../assets/icons/speedometer-icon.svg?react";
import PropTypes from "prop-types";
const PieChartLegend = ({ audits }) => {
diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx
similarity index 76%
rename from client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx
rename to client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx
index 582648e23..1281a396e 100644
--- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx
+++ b/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx
@@ -1,8 +1,8 @@
-import ChartBox from "../../../../../Components/Charts/ChartBox";
-import AreaChart from "../Charts/AreaChart";
-import AreaChartLegend from "../Charts/AreaChartLegend";
-import SkeletonLayout from "./skeleton";
-import ScoreIcon from "../../../../../assets/icons/monitor-graph-line.svg?react";
+import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
+import AreaChart from "../Charts/AreaChart.jsx";
+import AreaChartLegend from "../Charts/AreaChartLegend.jsx";
+import SkeletonLayout from "./skeleton.jsx";
+import ScoreIcon from "../../../../../../assets/icons/monitor-graph-line.svg?react";
import { Stack } from "@mui/material";
import PropTypes from "prop-types";
diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx
similarity index 100%
rename from client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx
rename to client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx
diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx
similarity index 79%
rename from client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx
rename to client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx
index cb79198e9..ff265317c 100644
--- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx
+++ b/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx
@@ -1,7 +1,7 @@
-import StatusBoxes from "../../../../../Components/StatusBoxes";
-import StatBox from "../../../../../Components/StatBox";
+import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
+import StatBox from "@/Components/v1/StatBox/index.jsx";
import { Typography } from "@mui/material";
-import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
+import { getHumanReadableDuration } from "../../../../../../Utils/timeUtils.js";
import { useTranslation } from "react-i18next";
const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => {
diff --git a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx
similarity index 78%
rename from client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx
rename to client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx
index 2f809ebbb..27f9e885b 100644
--- a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx
+++ b/client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx
@@ -1,10 +1,10 @@
-import ChartBox from "../../../../../Components/Charts/ChartBox";
-import PerformanceIcon from "../../../../../assets/icons/performance-report.svg?react";
-import PieChart from "../Charts/PieChart";
+import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
+import PerformanceIcon from "../../../../../../assets/icons/performance-report.svg?react";
+import PieChart from "../Charts/PieChart.jsx";
import { Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
-import PieChartLegend from "../Charts/PieChartLegend";
-import SkeletonLayout from "./skeleton";
+import PieChartLegend from "../Charts/PieChartLegend.jsx";
+import SkeletonLayout from "./skeleton.jsx";
import { useTranslation } from "react-i18next";
const PerformanceReport = ({ shouldRender, audits }) => {
diff --git a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx
similarity index 100%
rename from client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx
rename to client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx
diff --git a/client/src/Pages/PageSpeed/Details/index.jsx b/client/src/Pages/v1/PageSpeed/Details/index.jsx
similarity index 81%
rename from client/src/Pages/PageSpeed/Details/index.jsx
rename to client/src/Pages/v1/PageSpeed/Details/index.jsx
index cfc799b36..836f0ca37 100644
--- a/client/src/Pages/PageSpeed/Details/index.jsx
+++ b/client/src/Pages/v1/PageSpeed/Details/index.jsx
@@ -1,17 +1,17 @@
// Components
import { Stack, Typography } from "@mui/material";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
-import PageSpeedStatusBoxes from "./Components/PageSpeedStatusBoxes";
-import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader";
-import PageSpeedAreaChart from "./Components/PageSpeedAreaChart";
-import PerformanceReport from "./Components/PerformanceReport";
-import GenericFallback from "../../../Components/GenericFallback";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx";
+import PageSpeedStatusBoxes from "./Components/PageSpeedStatusBoxes/index.jsx";
+import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx";
+import PageSpeedAreaChart from "./Components/PageSpeedAreaChart/index.jsx";
+import PerformanceReport from "./Components/PerformanceReport/index.jsx";
+import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
-import { useIsAdmin } from "../../../Hooks/useIsAdmin";
+import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js";
import { useParams } from "react-router-dom";
-import { useFetchStatsByMonitorId } from "../../../Hooks/monitorHooks";
+import { useFetchStatsByMonitorId } from "../../../../Hooks/v1/monitorHooks.js";
import { useState } from "react";
import { useTranslation } from "react-i18next";
// Constants
diff --git a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx b/client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx
similarity index 95%
rename from client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx
rename to client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx
index 94d6b137b..4e551526b 100644
--- a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx
+++ b/client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx
@@ -1,17 +1,20 @@
import PropTypes from "prop-types";
// import PageSpeedIcon from "../../../../assets/icons/page-speed.svg?react";
-import PageSpeedIcon from "../../../../../assets/icons/page-speed.svg?react";
-import { StatusLabel } from "../../../../../Components/Label";
+import PageSpeedIcon from "../../../../../../assets/icons/page-speed.svg?react";
+import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import { Box, Grid, Stack, Typography } from "@mui/material";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
import { useSelector } from "react-redux";
-import { formatDateWithTz, formatDurationSplit } from "../../../../../Utils/timeUtils";
-import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
+import {
+ formatDateWithTz,
+ formatDurationSplit,
+} from "../../../../../../Utils/timeUtils.js";
+import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js";
import { useState } from "react";
import { useTranslation } from "react-i18next";
-import IconBox from "../../../../../Components/IconBox";
+import IconBox from "@/Components/v1/IconBox/index.jsx";
/**
* CustomToolTip displays a tooltip with formatted date and score information.
* @param {Object} props
diff --git a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx b/client/src/Pages/v1/PageSpeed/Monitors/Components/MonitorGrid/index.jsx
similarity index 88%
rename from client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx
rename to client/src/Pages/v1/PageSpeed/Monitors/Components/MonitorGrid/index.jsx
index 6266ebdc9..6b0a800fb 100644
--- a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx
+++ b/client/src/Pages/v1/PageSpeed/Monitors/Components/MonitorGrid/index.jsx
@@ -1,5 +1,5 @@
import { Grid, Grid2 } from "@mui/material";
-import Card from "../Card";
+import Card from "../Card/index.jsx";
const MonitorGrid = ({ shouldRender, monitors }) => {
return (
diff --git a/client/src/Pages/PageSpeed/Monitors/index.jsx b/client/src/Pages/v1/PageSpeed/Monitors/index.jsx
similarity index 70%
rename from client/src/Pages/PageSpeed/Monitors/index.jsx
rename to client/src/Pages/v1/PageSpeed/Monitors/index.jsx
index b5b13ba6e..7485a72c9 100644
--- a/client/src/Pages/PageSpeed/Monitors/index.jsx
+++ b/client/src/Pages/v1/PageSpeed/Monitors/index.jsx
@@ -1,18 +1,18 @@
// Components
import { useState } from "react";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import { Stack } from "@mui/material";
-import CreateMonitorHeader from "../../../Components/MonitorCreateHeader";
-import MonitorCountHeader from "../../../Components/MonitorCountHeader";
-import MonitorGrid from "./Components/MonitorGrid";
-import PageStateWrapper from "../../../Components/PageStateWrapper";
-import FallbackPageSpeedWarning from "../../../Components/Fallback/FallbackPageSpeedWarning";
+import CreateMonitorHeader from "@/Components/v1/MonitorCreateHeader/index.jsx";
+import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx";
+import MonitorGrid from "./Components/MonitorGrid/index.jsx";
+import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
+import FallbackPageSpeedWarning from "@/Components/v1/Fallback/FallbackPageSpeedWarning.jsx";
// Utils
import { useTheme } from "@emotion/react";
-import { useIsAdmin } from "../../../Hooks/useIsAdmin";
-import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks";
-import { useFetchSettings } from "../../../Hooks/settingsHooks";
+import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js";
+import { useFetchMonitorsByTeamId } from "../../../../Hooks/v1/monitorHooks.js";
+import { useFetchSettings } from "../../../../Hooks/v1/settingsHooks.js";
// Constants
const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }];
const TYPES = ["pagespeed"];
diff --git a/client/src/Pages/ServerUnreachable.jsx b/client/src/Pages/v1/ServerUnreachable.jsx
similarity index 90%
rename from client/src/Pages/ServerUnreachable.jsx
rename to client/src/Pages/v1/ServerUnreachable.jsx
index 466633144..8230f473b 100644
--- a/client/src/Pages/ServerUnreachable.jsx
+++ b/client/src/Pages/v1/ServerUnreachable.jsx
@@ -2,14 +2,14 @@ import React, { useState } from "react";
import { Box, Typography, Button, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
-import { networkService } from "../Utils/NetworkService";
-import Alert from "../Components/Alert";
-import { createToast } from "../Utils/toastUtils";
+import { networkService } from "../../Utils/NetworkService.js";
+import Alert from "@/Components/v1/Alert/index.jsx";
+import { createToast } from "../../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
-import Background from "../assets/Images/background-grid.svg?react";
-import Logo from "../assets/icons/checkmate-icon.svg?react";
-import ThemeSwitch from "../Components/ThemeSwitch";
-import LanguageSelector from "../Components/LanguageSelector";
+import Background from "../../assets/Images/background-grid.svg?react";
+import Logo from "../../assets/icons/checkmate-icon.svg?react";
+import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx";
+import LanguageSelector from "../../Components/LanguageSelector.jsx";
const ServerUnreachable = () => {
const theme = useTheme();
diff --git a/client/src/Pages/Settings/SettingsAbout.jsx b/client/src/Pages/v1/Settings/SettingsAbout.jsx
similarity index 89%
rename from client/src/Pages/Settings/SettingsAbout.jsx
rename to client/src/Pages/v1/Settings/SettingsAbout.jsx
index a80351393..6a8125af3 100644
--- a/client/src/Pages/Settings/SettingsAbout.jsx
+++ b/client/src/Pages/v1/Settings/SettingsAbout.jsx
@@ -1,10 +1,10 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
-import ConfigBox from "../../Components/ConfigBox";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
-import Link from "../../Components/Link";
+import Link from "@/Components/v1/Link/index.jsx";
const SettingsAbout = () => {
const theme = useTheme();
diff --git a/client/src/Pages/Settings/SettingsDemoMonitors.jsx b/client/src/Pages/v1/Settings/SettingsDemoMonitors.jsx
similarity index 95%
rename from client/src/Pages/Settings/SettingsDemoMonitors.jsx
rename to client/src/Pages/v1/Settings/SettingsDemoMonitors.jsx
index 13e93421e..47e699851 100644
--- a/client/src/Pages/Settings/SettingsDemoMonitors.jsx
+++ b/client/src/Pages/v1/Settings/SettingsDemoMonitors.jsx
@@ -1,12 +1,12 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
-import ConfigBox from "../../Components/ConfigBox";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
-import Dialog from "../../Components/Dialog";
+import Dialog from "@/Components/v1/Dialog/index.jsx";
import { useState } from "react";
const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) => {
diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/v1/Settings/SettingsEmail.jsx
similarity index 95%
rename from client/src/Pages/Settings/SettingsEmail.jsx
rename to client/src/Pages/v1/Settings/SettingsEmail.jsx
index 1f86b1984..7b5ad175e 100644
--- a/client/src/Pages/Settings/SettingsEmail.jsx
+++ b/client/src/Pages/v1/Settings/SettingsEmail.jsx
@@ -1,19 +1,19 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
-import ConfigBox from "../../Components/ConfigBox";
-import TextInput from "../../Components/Inputs/TextInput";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import { Switch } from "@mui/material";
-import TextLink from "../../Components/TextLink";
+import TextLink from "@/Components/v1/TextLink/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useState } from "react";
import { useTranslation } from "react-i18next";
-import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
-import { useSendTestEmail } from "../../Hooks/useSendTestEmail";
-import { createToast } from "../../Utils/toastUtils";
+import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
+import { useSendTestEmail } from "../../../Hooks/v1/useSendTestEmail.js";
+import { createToast } from "../../../Utils/toastUtils.jsx";
const SettingsEmail = ({
isAdmin,
diff --git a/client/src/Pages/Settings/SettingsGlobalThresholds.jsx b/client/src/Pages/v1/Settings/SettingsGlobalThresholds.jsx
similarity index 94%
rename from client/src/Pages/Settings/SettingsGlobalThresholds.jsx
rename to client/src/Pages/v1/Settings/SettingsGlobalThresholds.jsx
index acc101ab4..ea58478e2 100644
--- a/client/src/Pages/Settings/SettingsGlobalThresholds.jsx
+++ b/client/src/Pages/v1/Settings/SettingsGlobalThresholds.jsx
@@ -1,8 +1,8 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
-import ConfigBox from "../../Components/ConfigBox";
-import TextInput from "../../Components/Inputs/TextInput";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
diff --git a/client/src/Pages/Settings/SettingsPagespeed.jsx b/client/src/Pages/v1/Settings/SettingsPagespeed.jsx
similarity index 91%
rename from client/src/Pages/Settings/SettingsPagespeed.jsx
rename to client/src/Pages/v1/Settings/SettingsPagespeed.jsx
index 157c75dac..b0d5d6441 100644
--- a/client/src/Pages/Settings/SettingsPagespeed.jsx
+++ b/client/src/Pages/v1/Settings/SettingsPagespeed.jsx
@@ -2,9 +2,9 @@ import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
-import ConfigBox from "../../Components/ConfigBox";
-import TextInput from "../../Components/Inputs/TextInput";
-import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
+import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
diff --git a/client/src/Pages/Settings/SettingsStats.jsx b/client/src/Pages/v1/Settings/SettingsStats.jsx
similarity index 92%
rename from client/src/Pages/Settings/SettingsStats.jsx
rename to client/src/Pages/v1/Settings/SettingsStats.jsx
index 740ac3a37..b68006ab2 100644
--- a/client/src/Pages/Settings/SettingsStats.jsx
+++ b/client/src/Pages/v1/Settings/SettingsStats.jsx
@@ -2,9 +2,9 @@ import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
-import ConfigBox from "../../Components/ConfigBox";
-import TextInput from "../../Components/Inputs/TextInput";
-import Dialog from "../../Components/Dialog";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
+import Dialog from "@/Components/v1/Dialog/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
diff --git a/client/src/Pages/Settings/SettingsTimeZone.jsx b/client/src/Pages/v1/Settings/SettingsTimeZone.jsx
similarity index 90%
rename from client/src/Pages/Settings/SettingsTimeZone.jsx
rename to client/src/Pages/v1/Settings/SettingsTimeZone.jsx
index ee5cf48de..f684c871d 100644
--- a/client/src/Pages/Settings/SettingsTimeZone.jsx
+++ b/client/src/Pages/v1/Settings/SettingsTimeZone.jsx
@@ -1,9 +1,9 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
-import ConfigBox from "../../Components/ConfigBox";
-import Search from "../../Components/Inputs/Search";
-import timezones from "../../Utils/timezones.json";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
+import Search from "@/Components/v1/Inputs/Search/index.jsx";
+import timezones from "../../../Utils/timezones.json";
// Utils
import { useTheme } from "@emotion/react";
diff --git a/client/src/Pages/Settings/SettingsUI.jsx b/client/src/Pages/v1/Settings/SettingsUI.jsx
similarity index 92%
rename from client/src/Pages/Settings/SettingsUI.jsx
rename to client/src/Pages/v1/Settings/SettingsUI.jsx
index aab5f863d..7129e775b 100644
--- a/client/src/Pages/Settings/SettingsUI.jsx
+++ b/client/src/Pages/v1/Settings/SettingsUI.jsx
@@ -1,8 +1,8 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
-import ConfigBox from "../../Components/ConfigBox";
-import Select from "../../Components/Inputs/Select";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
+import Select from "@/Components/v1/Inputs/Select/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
diff --git a/client/src/Pages/Settings/SettingsURL.jsx b/client/src/Pages/v1/Settings/SettingsURL.jsx
similarity index 90%
rename from client/src/Pages/Settings/SettingsURL.jsx
rename to client/src/Pages/v1/Settings/SettingsURL.jsx
index 24c49cad1..c52dcf317 100644
--- a/client/src/Pages/Settings/SettingsURL.jsx
+++ b/client/src/Pages/v1/Settings/SettingsURL.jsx
@@ -1,8 +1,8 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
-import ConfigBox from "../../Components/ConfigBox";
-import Select from "../../Components/Inputs/Select";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
+import Select from "@/Components/v1/Inputs/Select/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx
similarity index 88%
rename from client/src/Pages/Settings/index.jsx
rename to client/src/Pages/v1/Settings/index.jsx
index 92f58b4c0..2732e331a 100644
--- a/client/src/Pages/Settings/index.jsx
+++ b/client/src/Pages/v1/Settings/index.jsx
@@ -1,31 +1,36 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
-import Breadcrumbs from "../../Components/Breadcrumbs";
-import SettingsTimeZone from "./SettingsTimeZone";
-import SettingsUI from "./SettingsUI";
-import SettingsURL from "./SettingsURL";
-import SettingsPagespeed from "./SettingsPagespeed";
-import SettingsDemoMonitors from "./SettingsDemoMonitors";
-import SettingsAbout from "./SettingsAbout";
-import SettingsEmail from "./SettingsEmail";
-import SettingsGlobalThresholds from "./SettingsGlobalThresholds";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import SettingsTimeZone from "./SettingsTimeZone.jsx";
+import SettingsUI from "./SettingsUI.jsx";
+import SettingsURL from "./SettingsURL.jsx";
+import SettingsPagespeed from "./SettingsPagespeed.jsx";
+import SettingsDemoMonitors from "./SettingsDemoMonitors.jsx";
+import SettingsAbout from "./SettingsAbout.jsx";
+import SettingsEmail from "./SettingsEmail.jsx";
+import SettingsGlobalThresholds from "./SettingsGlobalThresholds.jsx";
import Button from "@mui/material/Button";
// Utils
-import { settingsValidation } from "../../Validation/validation";
+import { settingsValidation } from "../../../Validation/validation.js";
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";
-import { setTimezone, setMode, setLanguage, setShowURL } from "../../Features/UI/uiSlice";
-import SettingsStats from "./SettingsStats";
+import {
+ setTimezone,
+ setMode,
+ setLanguage,
+ setShowURL,
+} from "../../../Features/UI/uiSlice.js";
+import SettingsStats from "./SettingsStats.jsx";
-import { useFetchSettings, useSaveSettings } from "../../Hooks/settingsHooks";
-import { useIsAdmin } from "../../Hooks/useIsAdmin";
+import { useFetchSettings, useSaveSettings } from "../../../Hooks/v1/settingsHooks.js";
+import { useIsAdmin } from "../../../Hooks/v1/useIsAdmin.js";
import {
useAddDemoMonitors,
useDeleteAllMonitors,
useDeleteMonitorStats,
-} from "../../Hooks/monitorHooks";
+} from "../../../Hooks/v1/monitorHooks.js";
// Constants
const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }];
diff --git a/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx b/client/src/Pages/v1/StatusPage/Create/Components/MonitorList/index.jsx
similarity index 100%
rename from client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx
rename to client/src/Pages/v1/StatusPage/Create/Components/MonitorList/index.jsx
diff --git a/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx
similarity index 84%
rename from client/src/Pages/StatusPage/Create/Components/Progress/index.jsx
rename to client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx
index 14c4ce0f5..232a12f1d 100644
--- a/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx
+++ b/client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx
@@ -1,8 +1,8 @@
import { Button, Box } from "@mui/material";
-import ProgressUpload from "../../../../../Components/ProgressBars";
+import ProgressUpload from "@/Components/v1/ProgressBars/index.jsx";
import ImageIcon from "@mui/icons-material/Image";
import { useTranslation } from "react-i18next";
-import { formatBytes } from "../../../../../Utils/fileUtils";
+import { formatBytes } from "../../../../../../Utils/fileUtils.js";
const Progress = ({ isLoading, progressValue, logo, logoType, removeLogo, errors }) => {
const { t } = useTranslation();
if (isLoading) {
diff --git a/client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Skeleton/index.jsx
similarity index 100%
rename from client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx
rename to client/src/Pages/v1/StatusPage/Create/Components/Skeleton/index.jsx
diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx
similarity index 93%
rename from client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx
rename to client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx
index e313f0e85..f31c456ef 100644
--- a/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx
+++ b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx
@@ -1,5 +1,5 @@
import { Stack, Typography } from "@mui/material";
-import ConfigBox from "../../../../../Components/ConfigBox";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx
similarity index 91%
rename from client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx
rename to client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx
index 327a2fb0a..d11825e69 100644
--- a/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx
+++ b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx
@@ -1,14 +1,14 @@
// Components
import { Stack, Typography } from "@mui/material";
import { TabPanel } from "@mui/lab";
-import MonitorList from "../MonitorList";
-import Search from "../../../../../Components/Inputs/Search";
-import Checkbox from "../../../../../Components/Inputs/Checkbox";
+import MonitorList from "../MonitorList/index.jsx";
+import Search from "@/Components/v1/Inputs/Search/index.jsx";
+import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx";
// Utils
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
-import ConfigStack from "./ConfigStack";
+import ConfigStack from "./ConfigStack.jsx";
const Content = ({
tabValue,
form,
diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx
similarity index 89%
rename from client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx
rename to client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx
index dd4345158..a668a9db8 100644
--- a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx
+++ b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx
@@ -1,17 +1,17 @@
// Components
import { Stack, Typography } from "@mui/material";
import { TabPanel } from "@mui/lab";
-import ConfigBox from "../../../../../Components/ConfigBox";
-import Checkbox from "../../../../../Components/Inputs/Checkbox";
-import TextInput from "../../../../../Components/Inputs/TextInput";
-import Search from "../../../../../Components/Inputs/Search";
-import ImageUpload from "../../../../../Components/Inputs/ImageUpload";
-import ColorPicker from "../../../../../Components/Inputs/ColorPicker";
-import Progress from "../Progress";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
+import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
+import Search from "@/Components/v1/Inputs/Search/index.jsx";
+import ImageUpload from "@/Components/v1/Inputs/ImageUpload/index.jsx";
+import ColorPicker from "@/Components/v1/Inputs/ColorPicker/index.jsx";
+import Progress from "../Progress/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
-import timezones from "../../../../../Utils/timezones.json";
+import timezones from "../../../../../../Utils/timezones.json";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { useMemo, useState, useCallback } from "react";
diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx
similarity index 93%
rename from client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx
rename to client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx
index 60d86e1c9..e8669c474 100644
--- a/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx
+++ b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx
@@ -1,12 +1,12 @@
// Components
import { TabContext } from "@mui/lab";
import { Tab } from "@mui/material";
-import Settings from "./Settings";
-import Content from "./Content";
+import Settings from "./Settings.jsx";
+import Content from "./Content.jsx";
// Utils
import PropTypes from "prop-types";
-import CustomTabList from "../../../../../Components/Tab";
+import CustomTabList from "@/Components/v1/Tab/index.jsx";
const Tabs = ({
isCreate,
diff --git a/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx b/client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx
similarity index 83%
rename from client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx
rename to client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx
index a2ac5f55d..808598052 100644
--- a/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx
+++ b/client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx
@@ -1,6 +1,6 @@
import { useState } from "react";
-import { networkService } from "../../../../main";
-import { createToast } from "../../../../Utils/toastUtils";
+import { networkService } from "../../../../../main.jsx";
+import { createToast } from "../../../../../Utils/toastUtils.jsx";
const useCreateStatusPage = (isCreate, url) => {
const [isLoading, setIsLoading] = useState(false);
diff --git a/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx b/client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx
similarity index 88%
rename from client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx
rename to client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx
index f380bed9a..2a7db202a 100644
--- a/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx
+++ b/client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
-import { networkService } from "../../../../main";
+import { networkService } from "../../../../../main.jsx";
import { useSelector } from "react-redux";
-import { createToast } from "../../../../Utils/toastUtils";
+import { createToast } from "../../../../../Utils/toastUtils.jsx";
const useMonitorsFetch = () => {
const { user } = useSelector((state) => state.auth);
diff --git a/client/src/Pages/StatusPage/Create/index.jsx b/client/src/Pages/v1/StatusPage/Create/index.jsx
similarity index 93%
rename from client/src/Pages/StatusPage/Create/index.jsx
rename to client/src/Pages/v1/StatusPage/Create/index.jsx
index d4cbc0fd5..d9fafed70 100644
--- a/client/src/Pages/StatusPage/Create/index.jsx
+++ b/client/src/Pages/v1/StatusPage/Create/index.jsx
@@ -1,23 +1,23 @@
// Components
import { Stack, Button, Typography } from "@mui/material";
-import Tabs from "./Components/Tabs";
-import GenericFallback from "../../../Components/GenericFallback";
-import SkeletonLayout from "./Components/Skeleton";
-import Dialog from "../../../Components/Dialog";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
+import Tabs from "./Components/Tabs/index.jsx";
+import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
+import SkeletonLayout from "./Components/Skeleton/index.jsx";
+import Dialog from "@/Components/v1/Dialog/index.jsx";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
//Utils
import { useTheme } from "@emotion/react";
import { useState, useEffect, useRef, useCallback } from "react";
-import { statusPageValidation } from "../../../Validation/validation";
-import { buildErrors } from "../../../Validation/error";
-import { useMonitorsFetch } from "./Hooks/useMonitorsFetch";
-import { useCreateStatusPage } from "./Hooks/useCreateStatusPage";
-import { createToast } from "../../../Utils/toastUtils";
+import { statusPageValidation } from "../../../../Validation/validation.js";
+import { buildErrors } from "../../../../Validation/error.js";
+import { useMonitorsFetch } from "./Hooks/useMonitorsFetch.jsx";
+import { useCreateStatusPage } from "./Hooks/useCreateStatusPage.jsx";
+import { createToast } from "../../../../Utils/toastUtils.jsx";
import { useNavigate } from "react-router-dom";
-import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch";
+import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch.jsx";
import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
-import { useStatusPageDelete } from "../Status/Hooks/useStatusPageDelete";
+import { useStatusPageDelete } from "../Status/Hooks/useStatusPageDelete.jsx";
//Constants
const ERROR_TAB_MAPPING = [
["companyName", "url", "timezone", "color", "isPublished", "logo"],
diff --git a/client/src/Pages/StatusPage/Status/Components/AdminLink/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/AdminLink/index.jsx
similarity index 100%
rename from client/src/Pages/StatusPage/Status/Components/AdminLink/index.jsx
rename to client/src/Pages/v1/StatusPage/Status/Components/AdminLink/index.jsx
diff --git a/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx
similarity index 93%
rename from client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx
rename to client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx
index beac3b0d2..c56aa40cc 100644
--- a/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx
+++ b/client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx
@@ -1,8 +1,8 @@
// Components
import { Box, Stack, Typography, Button } from "@mui/material";
-import Image from "../../../../../Components/Image";
-import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react";
-import ThemeSwitch from "../../../../../Components/ThemeSwitch";
+import Image from "@/Components/v1/Image/index.jsx";
+import SettingsIcon from "../../../../../../assets/icons/settings-bold.svg?react";
+import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx";
import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward";
//Utils
import { useTheme } from "@mui/material/styles";
diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx
similarity index 84%
rename from client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx
rename to client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx
index 545974c57..59ad4d522 100644
--- a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx
+++ b/client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx
@@ -1,12 +1,12 @@
// Components
import { Stack, Box } from "@mui/material";
-import Host from "../../../../../Components/Host";
-import StatusPageBarChart from "../../../../../Components/Charts/StatusPageBarChart";
-import { StatusLabel } from "../../../../../Components/Label";
+import Host from "@/Components/v1/Host/index.jsx";
+import StatusPageBarChart from "@/Components/v1/Charts/StatusPageBarChart/index.jsx";
+import { StatusLabel } from "@/Components/v1/Label/index.jsx";
//Utils
import { useTheme } from "@mui/material/styles";
-import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
+import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js";
import PropTypes from "prop-types";
import { useSelector } from "react-redux";
diff --git a/client/src/Pages/StatusPage/Status/Components/Skeleton/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/Skeleton/index.jsx
similarity index 100%
rename from client/src/Pages/StatusPage/Status/Components/Skeleton/index.jsx
rename to client/src/Pages/v1/StatusPage/Status/Components/Skeleton/index.jsx
diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/StatusBar/index.jsx
similarity index 100%
rename from client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx
rename to client/src/Pages/v1/StatusPage/Status/Components/StatusBar/index.jsx
diff --git a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx b/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageDelete.jsx
similarity index 89%
rename from client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx
rename to client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageDelete.jsx
index 2c39c70b3..038a771ba 100644
--- a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx
+++ b/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageDelete.jsx
@@ -1,6 +1,6 @@
import { useState } from "react";
-import { networkService } from "../../../../Utils/NetworkService";
-import { createToast } from "../../../../Utils/toastUtils";
+import { networkService } from "../../../../../Utils/NetworkService.js";
+import { createToast } from "../../../../../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
/**
diff --git a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx b/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx
similarity index 87%
rename from client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx
rename to client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx
index ae9c7b579..506082887 100644
--- a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx
+++ b/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx
@@ -1,9 +1,9 @@
import { useEffect, useState, useCallback } from "react";
-import { networkService } from "../../../../main";
+import { networkService } from "../../../../../main.jsx";
import { useSelector } from "react-redux";
-import { createToast } from "../../../../Utils/toastUtils";
+import { createToast } from "../../../../../Utils/toastUtils.jsx";
import { useTheme } from "@emotion/react";
-import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils";
+import { useMonitorUtils } from "../../../../../Hooks/v1/useMonitorUtils.js";
const useStatusPageFetch = (isCreate = false, url) => {
const [isLoading, setIsLoading] = useState(true);
diff --git a/client/src/Pages/StatusPage/Status/index.jsx b/client/src/Pages/v1/StatusPage/Status/index.jsx
similarity index 85%
rename from client/src/Pages/StatusPage/Status/index.jsx
rename to client/src/Pages/v1/StatusPage/Status/index.jsx
index a77f2a380..e700bbbd9 100644
--- a/client/src/Pages/StatusPage/Status/index.jsx
+++ b/client/src/Pages/v1/StatusPage/Status/index.jsx
@@ -1,18 +1,18 @@
// Components
import { Typography, Stack } from "@mui/material";
-import GenericFallback from "../../../Components/GenericFallback";
-import AdminLink from "./Components/AdminLink";
-import ControlsHeader from "./Components/ControlsHeader";
-import SkeletonLayout from "./Components/Skeleton";
-import StatusBar from "./Components/StatusBar";
-import MonitorsList from "./Components/MonitorsList";
-import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx";
-import TextLink from "../../../Components/TextLink";
+import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
+import AdminLink from "./Components/AdminLink/index.jsx";
+import ControlsHeader from "./Components/ControlsHeader/index.jsx";
+import SkeletonLayout from "./Components/Skeleton/index.jsx";
+import StatusBar from "./Components/StatusBar/index.jsx";
+import MonitorsList from "./Components/MonitorsList/index.jsx";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import TextLink from "@/Components/v1/TextLink/index.jsx";
// Utils
-import { useStatusPageFetch } from "./Hooks/useStatusPageFetch";
+import { useStatusPageFetch } from "./Hooks/useStatusPageFetch.jsx";
import { useTheme } from "@emotion/react";
-import { useIsAdmin } from "../../../Hooks/useIsAdmin";
+import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js";
import { useLocation } from "react-router-dom";
import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx b/client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx
similarity index 94%
rename from client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx
rename to client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx
index 04d7a6cfb..1afe25409 100644
--- a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx
+++ b/client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx
@@ -1,7 +1,7 @@
-import DataTable from "../../../../../Components/Table";
+import DataTable from "@/Components/v1/Table/index.jsx";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
-import { StatusLabel } from "../../../../../Components/Label";
+import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward";
import { Stack, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
diff --git a/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx b/client/src/Pages/v1/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx
similarity index 86%
rename from client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx
rename to client/src/Pages/v1/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx
index b236368c8..89df1b84d 100644
--- a/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx
+++ b/client/src/Pages/v1/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx
@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
-import { networkService } from "../../../../main";
+import { networkService } from "../../../../../main.jsx";
import { useSelector } from "react-redux";
-import { createToast } from "../../../../Utils/toastUtils";
+import { createToast } from "../../../../../Utils/toastUtils.jsx";
const useStatusPagesFetch = () => {
const { user } = useSelector((state) => state.auth);
diff --git a/client/src/Pages/StatusPage/StatusPages/index.jsx b/client/src/Pages/v1/StatusPage/StatusPages/index.jsx
similarity index 71%
rename from client/src/Pages/StatusPage/StatusPages/index.jsx
rename to client/src/Pages/v1/StatusPage/StatusPages/index.jsx
index fa26c2b81..3c60fe950 100644
--- a/client/src/Pages/StatusPage/StatusPages/index.jsx
+++ b/client/src/Pages/v1/StatusPage/StatusPages/index.jsx
@@ -1,13 +1,13 @@
// Components
import { Stack } from "@mui/material";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import MonitorCreateHeader from "../../../Components/MonitorCreateHeader";
-import StatusPagesTable from "./Components/StatusPagesTable";
-import PageStateWrapper from "../../../Components/PageStateWrapper";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import MonitorCreateHeader from "@/Components/v1/MonitorCreateHeader/index.jsx";
+import StatusPagesTable from "./Components/StatusPagesTable/index.jsx";
+import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
-import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch";
-import { useIsAdmin } from "../../../Hooks/useIsAdmin";
+import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch.jsx";
+import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js";
const BREADCRUMBS = [{ name: `Status Pages`, path: "" }];
const StatusPages = () => {
diff --git a/client/src/Pages/Uptime/BulkImport/Upload.jsx b/client/src/Pages/v1/Uptime/BulkImport/Upload.jsx
similarity index 100%
rename from client/src/Pages/Uptime/BulkImport/Upload.jsx
rename to client/src/Pages/v1/Uptime/BulkImport/Upload.jsx
diff --git a/client/src/Pages/Uptime/BulkImport/index.jsx b/client/src/Pages/v1/Uptime/BulkImport/index.jsx
similarity index 89%
rename from client/src/Pages/Uptime/BulkImport/index.jsx
rename to client/src/Pages/v1/Uptime/BulkImport/index.jsx
index 9490b4eab..9f3efcd67 100644
--- a/client/src/Pages/Uptime/BulkImport/index.jsx
+++ b/client/src/Pages/v1/Uptime/BulkImport/index.jsx
@@ -5,14 +5,14 @@ import { useState } from "react";
import { Box, Stack, Typography, Button, Link } from "@mui/material";
//Components
-import { createToast } from "../../../Utils/toastUtils";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import ConfigBox from "../../../Components/ConfigBox";
-import UploadFile from "./Upload";
+import { createToast } from "../../../../Utils/toastUtils.jsx";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
+import UploadFile from "./Upload.jsx";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
-import { useCreateBulkMonitors } from "../../../Hooks/monitorHooks";
+import { useCreateBulkMonitors } from "../../../../Hooks/v1/monitorHooks.js";
const BulkImport = () => {
const theme = useTheme();
diff --git a/client/src/Pages/PageSpeed/Create/index.css b/client/src/Pages/v1/Uptime/Create/index.css
similarity index 100%
rename from client/src/Pages/PageSpeed/Create/index.css
rename to client/src/Pages/v1/Uptime/Create/index.css
diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/v1/Uptime/Create/index.jsx
similarity index 95%
rename from client/src/Pages/Uptime/Create/index.jsx
rename to client/src/Pages/v1/Uptime/Create/index.jsx
index 3f905fa11..202f64d85 100644
--- a/client/src/Pages/Uptime/Create/index.jsx
+++ b/client/src/Pages/v1/Uptime/Create/index.jsx
@@ -9,31 +9,31 @@ import {
Tooltip,
Typography,
} from "@mui/material";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import TextInput from "../../../Components/Inputs/TextInput";
-import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
-import Radio from "../../../Components/Inputs/Radio";
-import Select from "../../../Components/Inputs/Select";
-import ConfigBox from "../../../Components/ConfigBox";
-import NotificationsConfig from "../../../Components/NotificationConfig";
-import Checkbox from "../../../Components/Inputs/Checkbox";
-import Dialog from "../../../Components/Dialog";
-import PulseDot from "../../../Components/Animated/PulseDot";
-import SkeletonLayout from "./skeleton";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
+import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
+import Radio from "@/Components/v1/Inputs/Radio/index.jsx";
+import Select from "@/Components/v1/Inputs/Select/index.jsx";
+import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
+import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx";
+import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx";
+import Dialog from "@/Components/v1/Dialog/index.jsx";
+import PulseDot from "@/Components/v1/Animated/PulseDot.jsx";
+import SkeletonLayout from "./skeleton.jsx";
// Utils
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
-import { monitorValidation } from "../../../Validation/validation";
-import { createToast } from "../../../Utils/toastUtils";
+import { monitorValidation } from "../../../../Validation/validation.js";
+import { createToast } from "../../../../Utils/toastUtils.jsx";
import {
PauseOutlined as PauseOutlinedIcon,
PlayArrowOutlined as PlayArrowOutlinedIcon,
} from "@mui/icons-material";
-import { useMonitorUtils } from "../../../Hooks/useMonitorUtils";
-import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
+import { useMonitorUtils } from "../../../../Hooks/v1/useMonitorUtils.js";
+import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js";
import { useParams } from "react-router-dom";
import {
useCreateMonitor,
@@ -42,7 +42,7 @@ import {
usePauseMonitor,
useFetchMonitorById,
useFetchMonitorGames,
-} from "../../../Hooks/monitorHooks";
+} from "../../../../Hooks/v1/monitorHooks.js";
/**
* Create page renders monitor creation or configuration views.
diff --git a/client/src/Pages/Uptime/Create/skeleton.jsx b/client/src/Pages/v1/Uptime/Create/skeleton.jsx
similarity index 100%
rename from client/src/Pages/Uptime/Create/skeleton.jsx
rename to client/src/Pages/v1/Uptime/Create/skeleton.jsx
diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx b/client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx
similarity index 86%
rename from client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx
rename to client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx
index b60a4dd04..45f35a42e 100644
--- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx
+++ b/client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx
@@ -1,15 +1,15 @@
// Components
import { Stack, Typography, Box } from "@mui/material";
-import ChartBox from "../../../../../Components/Charts/ChartBox";
-import UptimeIcon from "../../../../../assets/icons/uptime-icon.svg?react";
-import IncidentsIcon from "../../../../../assets/icons/incidents.svg?react";
-import AverageResponseIcon from "../../../../../assets/icons/average-response-icon.svg?react";
-import UpBarChart from "../Charts/UpBarChart";
-import DownBarChart from "../Charts/DownBarChart";
-import ResponseGaugeChart from "../Charts/ResponseGaugeChart";
-import SkeletonLayout from "./skeleton";
+import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
+import UptimeIcon from "../../../../../../assets/icons/uptime-icon.svg?react";
+import IncidentsIcon from "../../../../../../assets/icons/incidents.svg?react";
+import AverageResponseIcon from "../../../../../../assets/icons/average-response-icon.svg?react";
+import UpBarChart from "../Charts/UpBarChart.jsx";
+import DownBarChart from "../Charts/DownBarChart.jsx";
+import ResponseGaugeChart from "../Charts/ResponseGaugeChart.jsx";
+import SkeletonLayout from "./skeleton.jsx";
// Utils
-import { formatDateWithTz } from "../../../../../Utils/timeUtils";
+import { formatDateWithTz } from "../../../../../../Utils/timeUtils.js";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { useState } from "react";
diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx b/client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/skeleton.jsx
similarity index 100%
rename from client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx
rename to client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/skeleton.jsx
diff --git a/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx b/client/src/Pages/v1/Uptime/Details/Components/Charts/CustomLabels.jsx
similarity index 93%
rename from client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx
rename to client/src/Pages/v1/Uptime/Details/Components/Charts/CustomLabels.jsx
index 8df33953f..c9c1ff58d 100644
--- a/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx
+++ b/client/src/Pages/v1/Uptime/Details/Components/Charts/CustomLabels.jsx
@@ -1,6 +1,6 @@
import PropTypes from "prop-types";
import { useSelector } from "react-redux";
-import { formatDateWithTz } from "../../../../../Utils/timeUtils";
+import { formatDateWithTz } from "../../../../../../Utils/timeUtils.js";
const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
diff --git a/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx b/client/src/Pages/v1/Uptime/Details/Components/Charts/DownBarChart.jsx
similarity index 97%
rename from client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx
rename to client/src/Pages/v1/Uptime/Details/Components/Charts/DownBarChart.jsx
index 662abc831..c5e08ecca 100644
--- a/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx
+++ b/client/src/Pages/v1/Uptime/Details/Components/Charts/DownBarChart.jsx
@@ -2,7 +2,7 @@ import { memo, useState } from "react";
import { useTheme } from "@mui/material";
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
import PropTypes from "prop-types";
-import CustomLabels from "./CustomLabels";
+import CustomLabels from "./CustomLabels.jsx";
const DownBarChart = memo(({ groupedDownChecks = [], type, onBarHover }) => {
const theme = useTheme();
diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx b/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx
similarity index 100%
rename from client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx
rename to client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx
diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx b/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx
similarity index 62%
rename from client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx
rename to client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx
index e2d8dea80..daa7b4f53 100644
--- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx
+++ b/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx
@@ -1,7 +1,7 @@
-import ChartBox from "../../../../../Components/Charts/ChartBox";
-import MonitorDetailsAreaChart from "../../../../../Components/Charts/MonitorDetailsAreaChart";
-import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react";
-import SkeletonLayout from "./ResponseTimeChartSkeleton";
+import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
+import MonitorDetailsAreaChart from "@/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx";
+import ResponseTimeIcon from "../../../../../../assets/icons/response-time-icon.svg?react";
+import SkeletonLayout from "./ResponseTimeChartSkeleton.jsx";
import PropTypes from "prop-types";
const ResponseTImeChart = ({ isLoading = false, groupedChecks = [], dateRange }) => {
diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx b/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx
similarity index 100%
rename from client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx
rename to client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx
diff --git a/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx b/client/src/Pages/v1/Uptime/Details/Components/Charts/UpBarChart.jsx
similarity index 98%
rename from client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx
rename to client/src/Pages/v1/Uptime/Details/Components/Charts/UpBarChart.jsx
index 317147b74..8f1af8912 100644
--- a/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx
+++ b/client/src/Pages/v1/Uptime/Details/Components/Charts/UpBarChart.jsx
@@ -2,7 +2,7 @@ import { memo, useState } from "react";
import { useTheme } from "@mui/material";
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts";
import PropTypes from "prop-types";
-import CustomLabels from "./CustomLabels";
+import CustomLabels from "./CustomLabels.jsx";
const getThemeColor = (responseTime) => {
if (responseTime < 200) {
diff --git a/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx b/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx
similarity index 78%
rename from client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx
rename to client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx
index 4d482aa07..948489ac1 100644
--- a/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx
+++ b/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx
@@ -1,12 +1,12 @@
-import ChartBox from "../../../../../Components/Charts/ChartBox";
+import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx";
import PropTypes from "prop-types";
-import HistoryIcon from "../../../../../assets/icons/history-icon.svg?react";
-import Table from "../../../../../Components/Table";
-import TablePagination from "../../../../../Components/Table/TablePagination";
-import { StatusLabel } from "../../../../../Components/Label";
+import HistoryIcon from "../../../../../../assets/icons/history-icon.svg?react";
+import Table from "@/Components/v1/Table/index.jsx";
+import TablePagination from "@/Components/v1/Table/TablePagination/index.jsx";
+import { StatusLabel } from "@/Components/v1/Label/index.jsx";
import { useTranslation } from "react-i18next";
-import { formatDateWithTz } from "../../../../../Utils/timeUtils";
-import SkeletonLayout from "./skeleton";
+import { formatDateWithTz } from "../../../../../../Utils/timeUtils.js";
+import SkeletonLayout from "./skeleton.jsx";
const ResponseTable = ({
isLoading = false,
checks = [],
diff --git a/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx b/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/skeleton.jsx
similarity index 100%
rename from client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx
rename to client/src/Pages/v1/Uptime/Details/Components/ResponseTable/skeleton.jsx
diff --git a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx b/client/src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx
similarity index 86%
rename from client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx
rename to client/src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx
index abeb251fe..a7b971839 100644
--- a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx
+++ b/client/src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx
@@ -1,11 +1,11 @@
-import StatusBoxes from "../../../../../Components/StatusBoxes";
-import StatBox from "../../../../../Components/StatBox";
+import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx";
+import StatBox from "@/Components/v1/StatBox/index.jsx";
import PropTypes from "prop-types";
-import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
+import { getHumanReadableDuration } from "../../../../../../Utils/timeUtils.js";
import { useTheme } from "@mui/material/styles";
import { Typography } from "@mui/material";
-import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
+import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js";
const UptimeStatusBoxes = ({
isLoading = false,
diff --git a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx b/client/src/Pages/v1/Uptime/Details/Hooks/useCertificateFetch.jsx
similarity index 84%
rename from client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx
rename to client/src/Pages/v1/Uptime/Details/Hooks/useCertificateFetch.jsx
index ad0db6f70..f85111f97 100644
--- a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx
+++ b/client/src/Pages/v1/Uptime/Details/Hooks/useCertificateFetch.jsx
@@ -1,7 +1,7 @@
-import { logger } from "../../../../Utils/Logger";
+import { logger } from "../../../../../Utils/Logger.js";
import { useEffect, useState } from "react";
-import { networkService } from "../../../../main";
-import { formatDateWithTz } from "../../../../Utils/timeUtils";
+import { networkService } from "../../../../../main.jsx";
+import { formatDateWithTz } from "../../../../../Utils/timeUtils.js";
const useCertificateFetch = ({
monitor,
diff --git a/client/src/Pages/Uptime/Details/index.jsx b/client/src/Pages/v1/Uptime/Details/index.jsx
similarity index 84%
rename from client/src/Pages/Uptime/Details/index.jsx
rename to client/src/Pages/v1/Uptime/Details/index.jsx
index c72a5756f..f5e283495 100644
--- a/client/src/Pages/Uptime/Details/index.jsx
+++ b/client/src/Pages/v1/Uptime/Details/index.jsx
@@ -1,12 +1,12 @@
// Components
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader";
-import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
-import ChartBoxes from "./Components/ChartBoxes";
-import ResponseTimeChart from "./Components/Charts/ResponseTimeChart";
-import ResponseTable from "./Components/ResponseTable";
-import UptimeStatusBoxes from "./Components/UptimeStatusBoxes";
-import GenericFallback from "../../../Components/GenericFallback";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx";
+import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx";
+import ChartBoxes from "./Components/ChartBoxes/index.jsx";
+import ResponseTimeChart from "./Components/Charts/ResponseTimeChart.jsx";
+import ResponseTable from "./Components/ResponseTable/index.jsx";
+import UptimeStatusBoxes from "./Components/UptimeStatusBoxes/index.jsx";
+import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
@@ -15,10 +15,10 @@ import { useState } from "react";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
-import { useIsAdmin } from "../../../Hooks/useIsAdmin";
-import { useFetchUptimeMonitorById } from "../../../Hooks/monitorHooks";
-import useCertificateFetch from "./Hooks/useCertificateFetch";
-import { useFetchChecksByMonitor } from "../../../Hooks/checkHooks";
+import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js";
+import { useFetchUptimeMonitorById } from "../../../../Hooks/v1/monitorHooks.js";
+import useCertificateFetch from "./Hooks/useCertificateFetch.jsx";
+import { useFetchChecksByMonitor } from "../../../../Hooks/v1/checkHooks.js";
import { useTranslation } from "react-i18next";
// Constants
diff --git a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/Filter/index.jsx
similarity index 98%
rename from client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx
rename to client/src/Pages/v1/Uptime/Monitors/Components/Filter/index.jsx
index 0efb06b6c..b9a6824c1 100644
--- a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx
+++ b/client/src/Pages/v1/Uptime/Monitors/Components/Filter/index.jsx
@@ -1,6 +1,6 @@
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
-import FilterHeader from "../../../../../Components/FilterHeader";
+import FilterHeader from "@/Components/v1/FilterHeader/index.jsx";
import { useMemo } from "react";
import { Box, Button } from "@mui/material";
import ClearIcon from "@mui/icons-material/Clear";
diff --git a/client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/LoadingSpinner/index.jsx
similarity index 100%
rename from client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx
rename to client/src/Pages/v1/Uptime/Monitors/Components/LoadingSpinner/index.jsx
diff --git a/client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/SearchComponent/index.jsx
similarity index 90%
rename from client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx
rename to client/src/Pages/v1/Uptime/Monitors/Components/SearchComponent/index.jsx
index 0c6eec854..64c546a8a 100644
--- a/client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx
+++ b/client/src/Pages/v1/Uptime/Monitors/Components/SearchComponent/index.jsx
@@ -1,7 +1,7 @@
import { useState } from "react";
-import Search from "../../../../../Components/Inputs/Search";
+import Search from "@/Components/v1/Inputs/Search/index.jsx";
import { Box } from "@mui/material";
-import useDebounce from "../../Hooks/useDebounce";
+import useDebounce from "../../Hooks/useDebounce.jsx";
import { useEffect, useRef } from "react";
import PropTypes from "prop-types";
diff --git a/client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/Skeleton/index.jsx
similarity index 100%
rename from client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx
rename to client/src/Pages/v1/Uptime/Monitors/Components/Skeleton/index.jsx
diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/index.jsx
similarity index 91%
rename from client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx
rename to client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/index.jsx
index 9cd29c4ca..5cba9e3b1 100644
--- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx
+++ b/client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/index.jsx
@@ -1,9 +1,9 @@
import PropTypes from "prop-types";
import { Stack } from "@mui/material";
-import StatusBox from "./statusBox";
+import StatusBox from "./statusBox.jsx";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
-import SkeletonLayout from "./skeleton";
+import SkeletonLayout from "./skeleton.jsx";
const StatusBoxes = ({ shouldRender, monitorsSummary }) => {
const theme = useTheme();
diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx
similarity index 100%
rename from client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx
rename to client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx
diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx
similarity index 90%
rename from client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx
rename to client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx
index 78d555409..c745e7935 100644
--- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx
+++ b/client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx
@@ -1,9 +1,9 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
-import Arrow from "../../../../../assets/icons/top-right-arrow.svg?react";
-import Background from "../../../../../assets/Images/background-grid.svg?react";
-import ClockSnooze from "../../../../../assets/icons/clock-snooze.svg?react";
+import Arrow from "../../../../../../assets/icons/top-right-arrow.svg?react";
+import Background from "../../../../../../assets/Images/background-grid.svg?react";
+import ClockSnooze from "../../../../../../assets/icons/clock-snooze.svg?react";
const StatusBox = ({ title, value, status }) => {
const theme = useTheme();
diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx
similarity index 91%
rename from client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx
rename to client/src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx
index 63a40f85a..9cf2d247d 100644
--- a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx
+++ b/client/src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx
@@ -1,19 +1,19 @@
// Components
import { Box, Stack } from "@mui/material";
-import DataTable from "../../../../../Components/Table";
+import DataTable from "@/Components/v1/Table/index.jsx";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
-import Host from "../../../../../Components/Host";
-import { StatusLabel } from "../../../../../Components/Label";
-import BarChart from "../../../../../Components/Charts/BarChart";
-import ActionsMenu from "../../../../../Components/ActionsMenu";
+import Host from "@/Components/v1/Host/index.jsx";
+import { StatusLabel } from "@/Components/v1/Label/index.jsx";
+import BarChart from "@/Components/v1/Charts/BarChart/index.jsx";
+import ActionsMenu from "@/Components/v1/ActionsMenu/index.jsx";
-import LoadingSpinner from "../LoadingSpinner";
-import TableSkeleton from "../../../../../Components/Table/skeleton";
+import LoadingSpinner from "../LoadingSpinner/index.jsx";
+import TableSkeleton from "@/Components/v1/Table/skeleton.jsx";
// Utils
import { useTheme } from "@emotion/react";
-import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
+import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
diff --git a/client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx b/client/src/Pages/v1/Uptime/Monitors/Hooks/useDebounce.jsx
similarity index 100%
rename from client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx
rename to client/src/Pages/v1/Uptime/Monitors/Hooks/useDebounce.jsx
diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/v1/Uptime/Monitors/index.jsx
similarity index 86%
rename from client/src/Pages/Uptime/Monitors/index.jsx
rename to client/src/Pages/v1/Uptime/Monitors/index.jsx
index 7f28c1cd7..cacfc23b4 100644
--- a/client/src/Pages/Uptime/Monitors/index.jsx
+++ b/client/src/Pages/v1/Uptime/Monitors/index.jsx
@@ -4,32 +4,32 @@
// 2a.List of monitors must have the total number of monitors that match.
// Components
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import Greeting from "../../../Utils/greeting";
-import StatusBoxes from "./Components/StatusBoxes";
-import UptimeDataTable from "./Components/UptimeDataTable";
-import Pagination from "../../../Components/Table/TablePagination";
-import CreateMonitorHeader from "../../../Components/MonitorCreateHeader";
-import SearchComponent from "./Components/SearchComponent";
-import Filter from "./Components/Filter";
-import PageStateWrapper from "../../../Components/PageStateWrapper";
+import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
+import Greeting from "../../../../Utils/greeting.jsx";
+import StatusBoxes from "./Components/StatusBoxes/index.jsx";
+import UptimeDataTable from "./Components/UptimeDataTable/index.jsx";
+import Pagination from "@/Components/v1/Table/TablePagination/index.jsx";
+import CreateMonitorHeader from "@/Components/v1/MonitorCreateHeader/index.jsx";
+import SearchComponent from "./Components/SearchComponent/index.jsx";
+import Filter from "./Components/Filter/index.jsx";
+import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx";
-import MonitorCountHeader from "../../../Components/MonitorCountHeader";
+import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx";
// MUI Components
import { Stack, Box, Button } from "@mui/material";
// Utils
import { useState, useCallback, useEffect } from "react";
-import { useIsAdmin } from "../../../Hooks/useIsAdmin";
+import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
-import { setRowsPerPage } from "../../../Features/UI/uiSlice";
+import { setRowsPerPage } from "../../../../Features/UI/uiSlice.js";
import PropTypes from "prop-types";
import {
useFetchMonitorsWithSummary,
useFetchMonitorsWithChecks,
-} from "../../../Hooks/monitorHooks";
+} from "../../../../Hooks/v1/monitorHooks.js";
import { useTranslation } from "react-i18next";
const TYPES = ["http", "ping", "docker", "port", "game"];
diff --git a/client/src/Pages/v2/Auth/Login.tsx b/client/src/Pages/v2/Auth/Login.tsx
new file mode 100644
index 000000000..79ed2329f
--- /dev/null
+++ b/client/src/Pages/v2/Auth/Login.tsx
@@ -0,0 +1,136 @@
+import { AuthBasePage } from "@/Components/v2/Auth";
+import { Button } from "@/Components/v2/Inputs";
+import Stack from "@mui/material/Stack";
+import { TextInput, TextLink } from "@/Components/v2/Inputs";
+
+import type { ApiResponse } from "@/Hooks/v2/UseApi";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { usePost } from "@/Hooks/v2/UseApi";
+import { useNavigate } from "react-router";
+import { useTheme } from "@mui/material/styles";
+import { useDispatch } from "react-redux";
+import { setIsAuthenticated } from "@/Features/Auth/v2AuthSlice";
+import { useTranslation } from "react-i18next";
+import { z } from "zod";
+import { useForm, Controller } from "react-hook-form";
+
+const schema = z.object({
+ email: z.email("Invalid email address"),
+ password: z.string().min(6, "Password must be at least 6 characters"),
+});
+
+type FormData = z.infer;
+
+const Login = () => {
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+ const theme = useTheme();
+ const { post, loading } = usePost();
+ const navigate = useNavigate();
+
+ const {
+ handleSubmit,
+ control,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ email: "",
+ password: "",
+ },
+ });
+
+ const onSubmit = async (data: FormData) => {
+ const result = await post("/auth/login", data);
+ if (result) {
+ dispatch(setIsAuthenticated({ authenticated: true }));
+ navigate("/v2/uptime");
+ } else {
+ dispatch(setIsAuthenticated({ authenticated: false }));
+ }
+ };
+
+ return (
+
+
+
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+
+
+
+
+
+
+ );
+};
+
+export default Login;
diff --git a/client/src/Pages/v2/Auth/Register.tsx b/client/src/Pages/v2/Auth/Register.tsx
new file mode 100644
index 000000000..d4565c66c
--- /dev/null
+++ b/client/src/Pages/v2/Auth/Register.tsx
@@ -0,0 +1,177 @@
+import { AuthBasePage } from "@/Components/v2/Auth";
+import { TextInput } from "@/Components/v2/Inputs";
+import { Button } from "@/Components/v2/Inputs";
+import Typography from "@mui/material/Typography";
+import Stack from "@mui/material/Stack";
+
+import type { ApiResponse } from "@/Hooks/v2/UseApi";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { useTranslation } from "react-i18next";
+import { useForm, Controller } from "react-hook-form";
+import { useTheme } from "@mui/material/styles";
+import { usePost } from "@/Hooks/v2/UseApi";
+import { useNavigate } from "react-router";
+
+const schema = z
+ .object({
+ email: z.email({ message: "Invalid email address" }),
+ firstName: z.string().min(1, { message: "First Name is required" }),
+ lastName: z.string().min(1, { message: "Last Name is required" }),
+ password: z.string().min(6, { message: "Password must be at least 6 characters" }),
+ confirmPassword: z
+ .string()
+ .min(6, { message: "Confirm Password must be at least 6 characters" }),
+ })
+ .refine((data) => data.password === data.confirmPassword, {
+ path: ["confirmPassword"],
+ message: "Passwords must match",
+ });
+
+type FormData = z.infer;
+
+const Register = () => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const navigate = useNavigate();
+ const { post, loading, error } = usePost();
+
+ const {
+ handleSubmit,
+ control,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(schema),
+ defaultValues: {
+ email: "",
+ password: "",
+ },
+ });
+
+ const onSubmit = async (data: FormData) => {
+ const result = await post("/auth/register", data);
+ if (result) {
+ navigate("/v2/uptime");
+ } else {
+ console.error("Login failed:", error);
+ }
+ };
+
+ return (
+
+
+
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+
+ {error && {error}}
+
+
+
+ );
+};
+
+export default Register;
diff --git a/client/src/Pages/v2/Uptime/CheckTable.tsx b/client/src/Pages/v2/Uptime/CheckTable.tsx
new file mode 100644
index 000000000..42da6da12
--- /dev/null
+++ b/client/src/Pages/v2/Uptime/CheckTable.tsx
@@ -0,0 +1,93 @@
+import { Table, Pagination } from "@/Components/v2/DesignElements";
+import { StatusLabel } from "@/Components/v2/DesignElements";
+import Box from "@mui/material/Box";
+
+import type { Header } from "@/Components/v2/DesignElements/Table";
+import type { Check } from "@/Types/Check";
+import type { ApiResponse } from "@/Hooks/v2/UseApi";
+import type { MonitorStatus } from "@/Types/Monitor";
+
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useGet } from "@/Hooks/v2/UseApi";
+import { formatDateWithTz } from "@/Utils/TimeUtils";
+import { useSelector } from "react-redux";
+const getHeaders = (t: Function, uiTimezone: string) => {
+ const headers: Header[] = [
+ {
+ id: "status",
+ content: t("status"),
+ render: (row) => {
+ return ;
+ },
+ },
+ {
+ id: "date",
+ content: t("date&Time"),
+ render: (row) => {
+ return formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone);
+ },
+ },
+ {
+ id: "statusCode",
+ content: t("statusCode"),
+ render: (row) => {
+ return row.httpStatusCode || "N/A";
+ },
+ },
+ ];
+ return headers;
+};
+
+export const CheckTable = ({ monitorId }: { monitorId: string }) => {
+ const [page, setPage] = useState(0);
+ const [rowsPerPage, setRowsPerPage] = useState(5);
+ const { t } = useTranslation();
+ const uiTimezone = useSelector((state: any) => state.ui.timezone);
+ const headers = getHeaders(t, uiTimezone);
+
+ const { response, error } = useGet(
+ `/monitors/${monitorId}/checks?page=${page}&rowsPerPage=${rowsPerPage}`,
+ {},
+ { keepPreviousData: true }
+ );
+
+ const checks = response?.data?.checks || [];
+ const count = response?.data?.count || 0;
+
+ const handlePageChange = (
+ _e: React.MouseEvent | null,
+ newPage: number
+ ) => {
+ setPage(newPage);
+ };
+
+ const handleRowsPerPageChange = (
+ e: React.ChangeEvent
+ ) => {
+ const value = Number(e.target.value);
+ setPage(0);
+ setRowsPerPage(value);
+ };
+
+ if (error) {
+ console.error(error);
+ }
+
+ return (
+
+
+
+
+ );
+};
diff --git a/client/src/Pages/v2/Uptime/Create.tsx b/client/src/Pages/v2/Uptime/Create.tsx
new file mode 100644
index 000000000..38b1511f4
--- /dev/null
+++ b/client/src/Pages/v2/Uptime/Create.tsx
@@ -0,0 +1,262 @@
+import Stack from "@mui/material/Stack";
+import { TextInput } from "@/Components/v2/Inputs/TextInput";
+import { AutoCompleteInput } from "@/Components/v2/Inputs/AutoComplete";
+import { ConfigBox, BasePage } from "@/Components/v2/DesignElements";
+import RadioGroup from "@mui/material/RadioGroup";
+import FormControl from "@mui/material/FormControl";
+import { RadioWithDescription } from "@/Components/v2/Inputs/RadioInput";
+import { Button } from "@/Components/v2/Inputs";
+import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
+import { Typography } from "@mui/material";
+import humanInterval from "human-interval";
+
+import { useTranslation } from "react-i18next";
+import { useForm, Controller, useWatch } from "react-hook-form";
+import { z } from "zod";
+import { monitorSchema } from "@/Validation/v2/zod";
+import { useTheme } from "@mui/material/styles";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useGet, usePost } from "@/Hooks/v2/UseApi";
+import type { ApiResponse } from "@/Hooks/v2/UseApi";
+
+const UptimeCreatePage = () => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ type FormValues = z.infer;
+ type SubmitValues = Omit & { interval: number | undefined };
+
+ const {
+ handleSubmit,
+ control,
+ setValue,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(monitorSchema) as any,
+ defaultValues: {
+ type: "http",
+ url: "",
+ n: 3,
+ notificationChannels: [],
+ name: "",
+ interval: "1 minute",
+ },
+ mode: "onChange",
+ });
+ const { response } = useGet("/notification-channels");
+ const { post, loading, error } = usePost();
+ const selectedType = useWatch({
+ control,
+ name: "type",
+ });
+ const notificationChannels = useWatch({
+ control,
+ name: "notificationChannels",
+ });
+
+ const onSubmit = async (data: FormValues) => {
+ let interval = humanInterval(data.interval);
+ if (!interval) interval = 60000;
+ const submitData = { ...data, interval };
+ const result = await post("/monitors", submitData);
+ if (result) {
+ console.log(result);
+ } else {
+ console.error(error);
+ }
+ };
+
+ const notificationOptions = response?.data ?? [];
+ return (
+
+ );
+};
+
+export default UptimeCreatePage;
diff --git a/client/src/Pages/v2/Uptime/Details.tsx b/client/src/Pages/v2/Uptime/Details.tsx
new file mode 100644
index 000000000..c2069c7e4
--- /dev/null
+++ b/client/src/Pages/v2/Uptime/Details.tsx
@@ -0,0 +1,157 @@
+import { BasePage } from "@/Components/v2/DesignElements";
+import { HeaderControls } from "@/Components/v2/Monitors/HeaderControls";
+import Stack from "@mui/material/Stack";
+import { StatBox } from "@/Components/v2/DesignElements";
+import { HistogramStatus } from "@/Components/v2/Monitors/HistogramStatus";
+import { ChartAvgResponse } from "@/Components/v2/Monitors/ChartAvgResponse";
+import { ChartResponseTime } from "@/Components/v2/Monitors/ChartResponseTime";
+import { HeaderRange } from "@/Components/v2/Monitors/HeaderRange";
+import { CheckTable } from "@/Pages/v2/Uptime/CheckTable";
+
+import type { IMonitor } from "@/Types/Monitor";
+import { useMediaQuery } from "@mui/material";
+import { useTheme } from "@mui/material/styles";
+import { useParams } from "react-router";
+import { useGet, usePatch, type ApiResponse } from "@/Hooks/v2/UseApi";
+import { useState } from "react";
+import { getStatusPalette } from "@/Utils/MonitorUtils";
+import prettyMilliseconds from "pretty-ms";
+
+const UptimeDetailsPage = () => {
+ const { id } = useParams();
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+
+ // Local state
+ const [range, setRange] = useState("2h");
+
+ const { response, isValidating, error, refetch } = useGet(
+ `/monitors/${id}?embedChecks=true&range=${range}`,
+
+ {},
+ { refreshInterval: 30000, keepPreviousData: true }
+ );
+
+ const {
+ response: upResponse,
+ isValidating: upIsValidating,
+ error: upError,
+ } = useGet(
+ `/monitors/${id}?embedChecks=true&range=${range}&status=up`,
+ {},
+ { keepPreviousData: true }
+ );
+
+ const {
+ response: downResponse,
+ error: downError,
+ isValidating: downIsValidating,
+ } = useGet(
+ `/monitors/${id}?embedChecks=true&range=${range}&status=down`,
+ {},
+ { keepPreviousData: true }
+ );
+
+ const { patch, loading: isPatching, error: postError } = usePatch();
+
+ const monitor: IMonitor = response?.data?.monitor;
+
+ if (!monitor) {
+ return null;
+ }
+
+ const stats = response?.data?.stats || null;
+ const avgResponseTime = stats?.avgResponseTime || 0;
+ const maxResponseTime = stats?.maxResponseTime || 0;
+
+ const streakDuration = stats?.currentStreakStartedAt
+ ? Date.now() - stats?.currentStreakStartedAt
+ : 0;
+
+ const lastChecked = stats?.lastCheckTimestamp
+ ? Date.now() - stats?.lastCheckTimestamp
+ : -1;
+
+ const checks = response?.data?.checks || [];
+ const upChecks = upResponse?.data?.checks ? [...upResponse.data.checks].reverse() : [];
+ const downChecks = downResponse?.data?.checks
+ ? [...downResponse.data.checks].reverse()
+ : [];
+
+ const palette = getStatusPalette(monitor?.status);
+
+ if (error || upError || downError || postError) {
+ console.error("Error fetching monitor data:", {
+ error,
+ upError,
+ downError,
+ postError,
+ });
+ }
+
+ return (
+
+
+
+
+ = 0
+ ? `${prettyMilliseconds(lastChecked, { secondsDecimalDigits: 0 })} ago`
+ : "N/A"
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default UptimeDetailsPage;
diff --git a/client/src/Pages/v2/Uptime/MonitorTable.tsx b/client/src/Pages/v2/Uptime/MonitorTable.tsx
new file mode 100644
index 000000000..209784309
--- /dev/null
+++ b/client/src/Pages/v2/Uptime/MonitorTable.tsx
@@ -0,0 +1,158 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { Table } from "@/Components/v2/DesignElements";
+import { HistogramResponseTime } from "@/Components/v2/Monitors/HistogramResponseTime";
+import type { Header } from "@/Components/v2/DesignElements/Table";
+import { ActionsMenu } from "@/Components/v2/ActionsMenu";
+import { StatusLabel } from "@/Components/v2/DesignElements";
+
+import { useTranslation } from "react-i18next";
+import { useMediaQuery } from "@mui/material";
+import { useTheme } from "@mui/material/styles";
+import { useNavigate } from "react-router-dom";
+import { usePatch } from "@/Hooks/v2/UseApi";
+import type { ApiResponse } from "@/Hooks/v2/UseApi";
+
+import type { IMonitor } from "@/Types/Monitor";
+import type { ActionMenuItem } from "@/Components/v2/ActionsMenu";
+
+export const MonitorTable = ({
+ monitors,
+ refetch,
+}: {
+ monitors: IMonitor[];
+ refetch: Function;
+}) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ const navigate = useNavigate();
+ const {
+ patch,
+ // loading: isPatching,
+ // error: postError,
+ } = usePatch();
+
+ const getActions = (monitor: IMonitor): ActionMenuItem[] => {
+ return [
+ {
+ id: 1,
+ label: "Open site",
+ action: () => {
+ window.open(monitor.url, "_blank", "noreferrer");
+ },
+ closeMenu: true,
+ },
+ {
+ id: 2,
+ label: "Details",
+ action: () => {
+ navigate(`${monitor._id}`);
+ },
+ },
+ {
+ id: 3,
+ label: "Incidents",
+ action: () => {
+ navigate(`/v2/incidents/${monitor._id}`);
+ },
+ },
+ {
+ id: 4,
+ label: "Configure",
+ action: () => {
+ console.log("Open configure");
+ },
+ },
+ {
+ id: 5,
+ label: "Clone",
+ action: () => {
+ console.log("Open clone");
+ },
+ },
+ {
+ id: 6,
+ label: monitor.isActive ? "Pause" : "Resume",
+ action: async () => {
+ await patch(`/monitors/${monitor._id}/active`);
+ refetch();
+ },
+ closeMenu: true,
+ },
+ {
+ id: 7,
+ label: Remove,
+ action: () => {
+ console.log("Open delete");
+ },
+ closeMenu: true,
+ },
+ ];
+ };
+
+ const getHeaders = () => {
+ const headers: Header[] = [
+ {
+ id: "name",
+ content: t("host"),
+ render: (row) => {
+ return row.name;
+ },
+ },
+ {
+ id: "status",
+ content: t("status"),
+ render: (row) => {
+ return (
+
+ );
+ },
+ },
+ {
+ id: "histogram",
+ content: t("responseTime"),
+ render: (row) => {
+ return (
+
+
+
+ );
+ },
+ },
+ {
+ id: "type",
+ content: t("type"),
+ render: (row) => {
+ return row.type;
+ },
+ },
+ {
+ id: "actions",
+ content: t("actions"),
+ render: (row) => {
+ return ;
+ },
+ },
+ ];
+ return headers;
+ };
+
+ let headers = getHeaders();
+
+ if (isSmall) {
+ headers = headers.filter((h) => h.id !== "histogram");
+ }
+ return (
+ {
+ navigate(row._id);
+ }}
+ />
+ );
+};
diff --git a/client/src/Pages/v2/Uptime/UptimeMonitors.tsx b/client/src/Pages/v2/Uptime/UptimeMonitors.tsx
new file mode 100644
index 000000000..e5a151eb1
--- /dev/null
+++ b/client/src/Pages/v2/Uptime/UptimeMonitors.tsx
@@ -0,0 +1,74 @@
+import {
+ BasePageWithStates,
+ UpStatusBox,
+ DownStatusBox,
+ PausedStatusBox,
+} from "@/Components/v2/DesignElements";
+import { HeaderCreate } from "@/Components/v2/Monitors";
+import Stack from "@mui/material/Stack";
+import { MonitorTable } from "@/Pages/v2/Uptime/MonitorTable";
+
+import { useTheme } from "@mui/material/styles";
+import { useGet } from "@/Hooks/v2/UseApi";
+import type { ApiResponse } from "@/Hooks/v2/UseApi";
+import type { IMonitor } from "@/Types/Monitor";
+import { useMediaQuery } from "@mui/material";
+
+const UptimeMonitors = () => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+
+ const { response, isValidating, error, refetch } = useGet(
+ "/monitors?embedChecks=true",
+ {},
+ { refreshInterval: 30000, keepPreviousData: true }
+ );
+ const monitors: IMonitor[] = response?.data ?? ([] as IMonitor[]);
+
+ const monitorStatuses = monitors.reduce(
+ (acc, monitor) => {
+ if (monitor.status === "up") {
+ acc.up += 1;
+ } else if (monitor.status === "down") {
+ acc.down += 1;
+ } else if (monitor.isActive === false) {
+ acc.paused += 1;
+ }
+ return acc;
+ },
+ {
+ up: 0,
+ down: 0,
+ paused: 0,
+ }
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default UptimeMonitors;
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 7ddcd7e3c..90dcaa3f5 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -1,62 +1,71 @@
+import { useSelector } from "react-redux";
+import { lightTheme, darkTheme } from "@/Utils/Theme/v2/theme";
import { Navigate, Route, Routes as LibRoutes } from "react-router";
-import HomeLayout from "../Components/Layouts/HomeLayout";
-import NotFound from "../Pages/NotFound";
-
+import HomeLayout from "@/Components/v1/Layouts/HomeLayout";
+import NotFound from "../Pages/v1/NotFound";
// Auth
-import AuthLogin from "../Pages/Auth/Login";
-import AuthRegister from "../Pages/Auth/Register/";
-import AuthForgotPassword from "../Pages/Auth/ForgotPassword";
-import AuthCheckEmail from "../Pages/Auth/CheckEmail";
-import AuthSetNewPassword from "../Pages/Auth/SetNewPassword";
-import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed";
+import AuthLogin from "../Pages/v1/Auth/Login";
+import AuthRegister from "../Pages/v1/Auth/Register/";
+import AuthForgotPassword from "../Pages/v1/Auth/ForgotPassword";
+import AuthCheckEmail from "../Pages/v1/Auth/CheckEmail";
+import AuthSetNewPassword from "../Pages/v1/Auth/SetNewPassword";
+import AuthNewPasswordConfirmed from "../Pages/v1/Auth/NewPasswordConfirmed";
// Uptime
-import Uptime from "../Pages/Uptime/Monitors";
-import UptimeDetails from "../Pages/Uptime/Details";
-import UptimeCreate from "../Pages/Uptime/Create";
+import Uptime from "../Pages/v1/Uptime/Monitors";
+import UptimeDetails from "../Pages/v1/Uptime/Details";
+import UptimeCreate from "../Pages/v1/Uptime/Create";
// PageSpeed
-import PageSpeed from "../Pages/PageSpeed/Monitors";
-import PageSpeedDetails from "../Pages/PageSpeed/Details";
-import PageSpeedCreate from "../Pages/PageSpeed/Create";
+import PageSpeed from "../Pages/v1/PageSpeed/Monitors";
+import PageSpeedDetails from "../Pages/v1/PageSpeed/Details";
+import PageSpeedCreate from "../Pages/v1/PageSpeed/Create";
// Infrastructure
-import Infrastructure from "../Pages/Infrastructure/Monitors";
-import InfrastructureCreate from "../Pages/Infrastructure/Create";
-import InfrastructureDetails from "../Pages/Infrastructure/Details";
+import Infrastructure from "../Pages/v1/Infrastructure/Monitors";
+import InfrastructureCreate from "../Pages/v1/Infrastructure/Create";
+import InfrastructureDetails from "../Pages/v1/Infrastructure/Details";
// Server Status
-import ServerUnreachable from "../Pages/ServerUnreachable";
+import ServerUnreachable from "../Pages/v1/ServerUnreachable.jsx";
// Incidents
-import Incidents from "../Pages/Incidents";
+import Incidents from "../Pages/v1/Incidents";
// Status pages
-import CreateStatus from "../Pages/StatusPage/Create";
-import StatusPages from "../Pages/StatusPage/StatusPages";
-import Status from "../Pages/StatusPage/Status";
+import CreateStatus from "../Pages/v1/StatusPage/Create";
+import StatusPages from "../Pages/v1/StatusPage/StatusPages";
+import Status from "../Pages/v1/StatusPage/Status";
-import Notifications from "../Pages/Notifications";
-import CreateNotifications from "../Pages/Notifications/create";
+import Notifications from "../Pages/v1/Notifications";
+import CreateNotifications from "../Pages/v1/Notifications/create";
// Settings
-import Account from "../Pages/Account";
-import EditUser from "../Pages/Account/EditUser";
-import Settings from "../Pages/Settings";
+import Account from "../Pages/v1/Account";
+import EditUser from "../Pages/v1/Account/EditUser";
+import Settings from "../Pages/v1/Settings";
-import Maintenance from "../Pages/Maintenance";
+import Maintenance from "../Pages/v1/Maintenance";
-import ProtectedRoute from "../Components/ProtectedRoute";
-import RoleProtectedRoute from "../Components/RoleProtectedRoute";
-import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance";
-import withAdminCheck from "../Components/HOC/withAdminCheck";
-import BulkImport from "../Pages/Uptime/BulkImport";
-import Logs from "../Pages/Logs";
+import ProtectedRoute from "../Components/v1/ProtectedRoute";
+import RoleProtectedRoute from "../Components/v1/RoleProtectedRoute";
+import CreateNewMaintenanceWindow from "../Pages/v1/Maintenance/CreateMaintenance";
+import withAdminCheck from "@/Components/v1/HOC/withAdminCheck";
+import BulkImport from "../Pages/v1/Uptime/BulkImport";
+import Logs from "../Pages/v1/Logs";
+
+import V2Routes from "@/Routes/v2router";
const Routes = () => {
+ const mode = useSelector((state) => state.ui.mode);
const AdminCheckedRegister = withAdminCheck(AuthRegister);
return (
+ }
+ />
+
{
+ const v2Theme = mode === "light" ? lightTheme : darkTheme;
+
+ return (
+
+
+ }
+ />
+ }
+ />
+ }
+ >
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+
+
+
+ );
+};
+
+export default V2Routes;
diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts
new file mode 100644
index 000000000..cccc56f3d
--- /dev/null
+++ b/client/src/Types/Check.ts
@@ -0,0 +1,50 @@
+export interface CheckTimingPhases {
+ wait: number;
+ dns: number;
+ tcp: number;
+ tls: number;
+ request: number;
+ firstByte: number;
+ download: number;
+ total: number;
+}
+
+export interface CheckTimings {
+ start: string;
+ socket: string;
+ lookup: string;
+ connect: string;
+ secureConnect: string;
+ response: string;
+ end: string;
+ phases: CheckTimingPhases;
+}
+
+export interface Check {
+ _id: string;
+ monitorId: string;
+ type: string;
+ status: string;
+ message: string;
+ responseTime: number;
+ normalResponseTime?: number;
+ httpStatusCode: number;
+ ack: boolean;
+ expiry: string;
+ createdAt: string;
+ updatedAt: string;
+ timings: CheckTimings;
+}
+
+export interface GroupedCheck {
+ _id: string;
+ avgResponseTime: number;
+ count: number;
+}
+
+export interface LatestCheck {
+ status: string;
+ responseTime: number;
+ checkedAt: string;
+ _id: string;
+}
diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts
new file mode 100644
index 000000000..05f7d7d6f
--- /dev/null
+++ b/client/src/Types/Monitor.ts
@@ -0,0 +1,20 @@
+import type { Check } from "@/Types/Check";
+export type MonitorStatus = "up" | "down" | "initializing";
+
+export interface IMonitor {
+ checks: Check[];
+ createdAt: string;
+ createdBy: string;
+ interval: number;
+ isActive: boolean;
+ latestChecks: Check[];
+ n: number;
+ name: string;
+ status: MonitorStatus;
+ type: string;
+ updatedAt: string;
+ updatedBy: string;
+ url: string;
+ __v: number;
+ _id: string;
+}
diff --git a/client/src/Types/env.d.ts b/client/src/Types/env.d.ts
new file mode 100644
index 000000000..74a2a29e7
--- /dev/null
+++ b/client/src/Types/env.d.ts
@@ -0,0 +1,17 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_APP_API_V2_BASE_URL?: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
+
+declare module "*.svg?react" {
+ import * as React from "react";
+ const ReactComponent: React.FunctionComponent<
+ React.SVGProps & { title?: string }
+ >;
+ export default ReactComponent;
+}
diff --git a/client/src/Types/mui.d.ts b/client/src/Types/mui.d.ts
new file mode 100644
index 000000000..84bcd799f
--- /dev/null
+++ b/client/src/Types/mui.d.ts
@@ -0,0 +1,24 @@
+import "@mui/material/Button";
+
+declare module "@mui/material/styles" {
+ interface Palette {
+ accent: Palette["primary"];
+ tertiary: Palette["primary"];
+ }
+ interface PaletteOptions {
+ accent?: PaletteOptions["primary"];
+ tertiary?: PaletteOptions["primary"];
+ }
+
+ interface PaletteColor {
+ contrastTextSecondary?: string;
+ contrastTextTertiary?: string;
+ lowContrast?: string;
+ }
+}
+
+declare module "@mui/material/Button" {
+ interface ButtonPropsColorOverrides {
+ accent: true;
+ }
+}
diff --git a/client/src/Utils/ApiClient.ts b/client/src/Utils/ApiClient.ts
new file mode 100644
index 000000000..f41407b8f
--- /dev/null
+++ b/client/src/Utils/ApiClient.ts
@@ -0,0 +1,26 @@
+import axios from "axios";
+import type { AxiosRequestConfig, AxiosResponse } from "axios";
+const BASE_URL = import.meta.env.VITE_APP_API_V2_BASE_URL;
+const api = axios.create({
+ baseURL: BASE_URL || "http://localhost:55555/api/v2",
+ withCredentials: true,
+});
+
+export const get = (
+ url: string,
+ config: AxiosRequestConfig = {}
+): Promise> => api.get(url, config);
+
+export const post = (
+ url: string,
+ data: any,
+ config: AxiosRequestConfig = {}
+): Promise> => api.post(url, data, config);
+
+export const patch = (
+ url: string,
+ data: any,
+ config: AxiosRequestConfig = {}
+): Promise> => api.patch(url, data, config);
+
+export default api;
diff --git a/client/src/Utils/DataUtils.ts b/client/src/Utils/DataUtils.ts
new file mode 100644
index 000000000..3878d7d6a
--- /dev/null
+++ b/client/src/Utils/DataUtils.ts
@@ -0,0 +1,37 @@
+const MIN_OUT = 10;
+const MAX_OUT = 100;
+
+export const normalizeResponseTimes = , K extends keyof T>(
+ checks: T[],
+ key: K
+): (T & { normalResponseTime: number })[] => {
+ if (!Array.isArray(checks) || checks.length === 0)
+ return checks as (T & {
+ normalResponseTime: number;
+ })[];
+
+ if (checks.length === 1) {
+ return [
+ {
+ ...checks[0],
+ normalResponseTime: 50,
+ },
+ ];
+ }
+
+ const { min, max } = checks.reduce(
+ (acc, check) => {
+ if (check[key] > acc.max) acc.max = check[key];
+ if (check[key] < acc.min) acc.min = check[key];
+ return acc;
+ },
+ { max: -Infinity, min: Infinity }
+ );
+
+ const range = max - min || 1;
+
+ return checks.map((check) => ({
+ ...check,
+ normalResponseTime: MIN_OUT + ((check[key] - min) * (MAX_OUT - MIN_OUT)) / range,
+ }));
+};
diff --git a/client/src/Utils/MonitorUtils.ts b/client/src/Utils/MonitorUtils.ts
new file mode 100644
index 000000000..dcd202361
--- /dev/null
+++ b/client/src/Utils/MonitorUtils.ts
@@ -0,0 +1,38 @@
+import type { MonitorStatus } from "@/Types/Monitor";
+import type { PaletteKey } from "./Theme/v2/theme";
+export const getStatusPalette = (status: MonitorStatus): PaletteKey => {
+ const paletteMap: Record = {
+ up: "success",
+ down: "error",
+ initializing: "warning",
+ };
+ return paletteMap[status];
+};
+
+export const getStatusColor = (status: MonitorStatus, theme: any): string => {
+ const statusColors: Record = {
+ up: theme.palette.success.lowContrast,
+ down: theme.palette.error.lowContrast,
+ initializing: theme.palette.warning.lowContrast,
+ };
+ return statusColors[status];
+};
+
+export const getResponseTimeColor = (responseTime: number): PaletteKey => {
+ if (responseTime < 200) {
+ return "success";
+ } else if (responseTime < 300) {
+ return "warning";
+ } else {
+ return "error";
+ }
+};
+
+export const formatUrl = (url: string, maxLength: number = 55) => {
+ if (!url) return "";
+
+ const strippedUrl = url.replace(/^https?:\/\//, "");
+ return strippedUrl.length > maxLength
+ ? `${strippedUrl.slice(0, maxLength)}…`
+ : strippedUrl;
+};
diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js
index dce0faaf5..6372be646 100644
--- a/client/src/Utils/NetworkService.js
+++ b/client/src/Utils/NetworkService.js
@@ -1136,6 +1136,15 @@ class NetworkService {
},
});
}
+
+ async changePasswordByAdmin(config) {
+ const { userId, passwordForm } = config;
+ return this.axiosInstance.put(`auth/users/${userId}/password`, passwordForm, {
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ }
}
export default NetworkService;
diff --git a/client/src/Utils/Theme/constants.js b/client/src/Utils/Theme/constants.js
index 9df6665d0..45217fdeb 100644
--- a/client/src/Utils/Theme/constants.js
+++ b/client/src/Utils/Theme/constants.js
@@ -100,7 +100,6 @@ const newColors = {
blueGray900: "#515151",
blueBlueWave: "#1570EF",
lightBlueWave: "#CDE2FF",
- /* I changed green 100 and green 700. Need to change red and warning as well, and refactor the object following the structure */
green100: "#67cd78",
green200: "#4B9B77",
green400: "#079455",
diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js
index bda415c90..e65baf4c0 100644
--- a/client/src/Utils/Theme/globalTheme.js
+++ b/client/src/Utils/Theme/globalTheme.js
@@ -58,8 +58,8 @@ const baseTheme = (palette) => ({
variants: [
{
props: (props) => props.variant === "contained" && props.color === "accent",
+ backgroundColor: theme.palette.accent.main,
style: {
- backgroundColor: theme.palette.accent.main,
color: theme.palette.primary.contrastTextSecondaryDarkBg,
letterSpacing: "0.5px",
textShadow: "0 0 1px rgba(0, 0, 0, 0.15)",
diff --git a/client/src/Utils/Theme/v2/palette.ts b/client/src/Utils/Theme/v2/palette.ts
new file mode 100644
index 000000000..43370fe8b
--- /dev/null
+++ b/client/src/Utils/Theme/v2/palette.ts
@@ -0,0 +1,124 @@
+import { lighten, darken } from "@mui/material/styles";
+
+const typographyBase = 13;
+
+export const typographyLevels = {
+ base: typographyBase,
+ xs: `${(typographyBase - 4) / 16}rem`,
+ s: `${(typographyBase - 2) / 16}rem`,
+ m: `${typographyBase / 16}rem`,
+ l: `${(typographyBase + 2) / 16}rem`,
+ xl: `${(typographyBase + 10) / 16}rem`,
+};
+
+const colors = {
+ offWhite: "#FEFEFE",
+ offBlack: "#131315",
+ gray0: "#FDFDFD",
+ gray10: "#F4F4FF",
+ gray50: "#F9F9F9",
+ gray100: "#F3F3F3",
+ gray200: "#EFEFEF",
+ gray250: "#DADADA",
+ gray500: "#A2A3A3",
+ gray900: "#1c1c1c",
+ blueGray50: "#E8F0FE",
+ blueGray500: "#475467",
+ blueGray600: "#344054",
+ blueGray800: "#1C2130",
+ blueGray900: "#515151",
+ blueBlueWave: "#1570EF",
+ lightBlueWave: "#CDE2FF",
+ green100: "#67cd78",
+ green200: "#4B9B77",
+ green400: "#079455",
+ green700: "#026513",
+ orange100: "#FD8F22",
+ orange200: "#D69A5D",
+ orange600: "#9B734B",
+ orange700: "#884605",
+ red100: "#F27C7C",
+ red400: "#D92020",
+ red600: "#9B4B4B",
+ red700: "#980303",
+};
+
+export const lightPalette = {
+ accent: {
+ main: colors.blueBlueWave,
+ light: lighten(colors.blueBlueWave, 0.2),
+ dark: darken(colors.blueBlueWave, 0.2),
+ contrastText: colors.offWhite,
+ },
+ primary: {
+ main: colors.offWhite,
+ contrastText: colors.blueGray800,
+ contrastTextSecondary: colors.blueGray600,
+ contrastTextTertiary: colors.blueGray500,
+ lowContrast: colors.gray250,
+ },
+ secondary: {
+ main: colors.gray200,
+ light: colors.lightBlueWave,
+ contrastText: colors.blueGray600,
+ },
+ tertiary: {
+ main: colors.gray100,
+ contrastText: colors.blueGray800,
+ },
+ success: {
+ main: colors.green700,
+ contrastText: colors.offWhite,
+ lowContrast: colors.green400,
+ },
+ warning: {
+ main: colors.orange700,
+ contrastText: colors.offWhite,
+ lowContrast: colors.orange100,
+ },
+ error: {
+ main: colors.red700,
+ contrastText: colors.offWhite,
+ lowContrast: colors.red400,
+ },
+};
+
+export const darkPalette = {
+ accent: {
+ main: colors.blueBlueWave,
+ light: lighten(colors.blueBlueWave, 0.2),
+ dark: darken(colors.blueBlueWave, 0.2),
+ contrastText: colors.offWhite,
+ },
+ primary: {
+ main: colors.offBlack,
+ contrastText: colors.blueGray50,
+ contrastTextSecondary: colors.gray200,
+ contrastTextTertiary: colors.gray500,
+ lowContrast: colors.blueGray600,
+ },
+ secondary: {
+ main: "#313131",
+ light: colors.lightBlueWave,
+ contrastText: colors.gray200,
+ },
+ tertiary: {
+ main: colors.blueGray800,
+ contrastText: colors.gray100,
+ },
+ success: {
+ main: colors.green100,
+ contrastText: colors.offBlack,
+ lowContrast: colors.green200,
+ },
+ warning: {
+ main: colors.orange200,
+ contrastText: colors.offBlack,
+ lowContrast: colors.orange600,
+ },
+ error: {
+ main: colors.red100,
+ contrastText: colors.offBlack,
+ lowContrast: colors.red600,
+ },
+};
diff --git a/client/src/Utils/Theme/v2/theme.ts b/client/src/Utils/Theme/v2/theme.ts
new file mode 100644
index 000000000..d2581e236
--- /dev/null
+++ b/client/src/Utils/Theme/v2/theme.ts
@@ -0,0 +1,103 @@
+import { createTheme } from "@mui/material";
+import { lightPalette, darkPalette, typographyLevels } from "./palette";
+
+import type { Theme } from "@mui/material/styles";
+
+export type PaletteKey = {
+ [K in keyof Theme["palette"]]: Theme["palette"][K] extends { main: any } ? K : never;
+}[keyof Theme["palette"]];
+
+const fontFamilyPrimary = '"Inter" , sans-serif';
+const shadow =
+ "0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03)";
+
+export const theme = (mode: string, palette: any) =>
+ createTheme({
+ spacing: 2,
+ palette: {
+ mode: mode,
+ ...palette,
+ },
+ typography: {
+ fontFamily: fontFamilyPrimary,
+ fontSize: typographyLevels.base,
+ h1: {
+ fontSize: typographyLevels.xl,
+ color: palette.primary.contrastText,
+ fontWeight: 500,
+ },
+ h2: {
+ fontSize: typographyLevels.l,
+ color: palette.primary.contrastTextSecondary,
+ fontWeight: 400,
+ },
+
+ body1: {
+ fontSize: typographyLevels.m,
+ color: palette.primary.contrastTextTertiary,
+ fontWeight: 400,
+ },
+ body2: {
+ fontSize: typographyLevels.s,
+ color: palette.primary.contrastTextTertiary,
+ fontWeight: 400,
+ },
+ },
+
+ components: {
+ MuiFormLabel: {
+ styleOverrides: {
+ root: ({ theme }) => ({
+ fontSize: typographyLevels.base,
+ "&.Mui-focused": {
+ color: theme.palette.secondary.contrastText,
+ },
+ }),
+ },
+ },
+ MuiInputLabel: {
+ styleOverrides: {
+ root: ({ theme }) => ({
+ top: `-${theme.spacing(4)}`,
+ "&.MuiInputLabel-shrink": {
+ top: 0,
+ },
+ }),
+ },
+ },
+
+ MuiOutlinedInput: {
+ styleOverrides: {
+ root: {
+ "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
+ borderColor: palette.accent.main,
+ },
+ },
+ },
+ },
+
+ MuiPaper: {
+ styleOverrides: {
+ root: ({ theme }) => {
+ return {
+ marginTop: 4,
+ padding: 0,
+ border: 1,
+ borderStyle: "solid",
+ borderColor: theme.palette.primary.lowContrast,
+ borderRadius: 4,
+ boxShadow: shadow,
+ backgroundColor: theme.palette.primary.main,
+ backgroundImage: "none",
+ };
+ },
+ },
+ },
+ },
+ shape: {
+ borderRadius: 2,
+ },
+ });
+
+export const lightTheme = createTheme(theme("light", lightPalette));
+export const darkTheme = createTheme(theme("dark", darkPalette));
diff --git a/client/src/Utils/TimeUtils.ts b/client/src/Utils/TimeUtils.ts
new file mode 100644
index 000000000..618d9d1bc
--- /dev/null
+++ b/client/src/Utils/TimeUtils.ts
@@ -0,0 +1,53 @@
+import dayjs from "dayjs";
+import duration from "dayjs/plugin/duration";
+import utc from "dayjs/plugin/utc";
+import timezone from "dayjs/plugin/timezone";
+import customParseFormat from "dayjs/plugin/customParseFormat";
+
+dayjs.extend(utc);
+dayjs.extend(timezone);
+dayjs.extend(customParseFormat);
+dayjs.extend(duration);
+
+export const MS_PER_SECOND = 1000;
+export const MS_PER_MINUTE = 60 * MS_PER_SECOND;
+export const MS_PER_HOUR = 60 * MS_PER_MINUTE;
+export const MS_PER_DAY = 24 * MS_PER_HOUR;
+export const MS_PER_WEEK = MS_PER_DAY * 7;
+
+export const formatDateWithTz = (timestamp: string, format: string, timezone: string) => {
+ if (!timestamp) {
+ return "Unknown time";
+ }
+
+ const formattedDate = dayjs(timestamp).tz(timezone).format(format);
+ return formattedDate;
+};
+
+export const tickDateFormatLookup = (range: string) => {
+ const tickFormatLookup: Record = {
+ "2h": "h:mm A",
+ "24h": "h:mm A",
+ "7d": "MM/D, h:mm A",
+ "30d": "ddd. M/D",
+ };
+ const format = tickFormatLookup[range];
+ if (format === undefined) {
+ return "";
+ }
+ return format;
+};
+
+export const tooltipDateFormatLookup = (range: string) => {
+ const dateFormatLookup: Record = {
+ "2h": "ddd. MMMM D, YYYY, hh:mm A",
+ "24h": "ddd. MMMM D, YYYY, hh:mm A",
+ "7d": "ddd. MMMM D, YYYY, hh:mm A",
+ "30d": "ddd. MMMM D, YYYY",
+ };
+ const format = dateFormatLookup[range];
+ if (format === undefined) {
+ return "";
+ }
+ return format;
+};
diff --git a/client/src/Utils/toastUtils.jsx b/client/src/Utils/toastUtils.jsx
index 21cf13e2b..41b2d5482 100644
--- a/client/src/Utils/toastUtils.jsx
+++ b/client/src/Utils/toastUtils.jsx
@@ -1,6 +1,6 @@
import PropTypes from "prop-types";
import { toast, Slide } from "react-toastify";
-import Toast from "../Components/Toast";
+import Toast from "../Components/v1/Toast";
/**
* @param {object} props
* @param {'info' | 'error' | 'warning'} - The variant of the alert (e.g., "info", "error").
diff --git a/client/src/Validation/v2/zod.ts b/client/src/Validation/v2/zod.ts
new file mode 100644
index 000000000..6f0dbb951
--- /dev/null
+++ b/client/src/Validation/v2/zod.ts
@@ -0,0 +1,36 @@
+import { z } from "zod";
+import humanInterval from "human-interval";
+const urlRegex =
+ /^(?:https?:\/\/)?([a-zA-Z0-9.-]+|\d{1,3}(\.\d{1,3}){3}|\[[0-9a-fA-F:]+\])(:\d{1,5})?$/;
+
+const durationSchema = z
+ .string()
+ .optional()
+ .superRefine((val, ctx) => {
+ if (!val || val.trim() === "") return;
+ const ms = humanInterval(val);
+
+ if (!ms || isNaN(ms)) {
+ ctx.addIssue({
+ code: "custom",
+ message: "Invalid duration format",
+ });
+ } else if (ms < 10000) {
+ ctx.addIssue({
+ code: "custom",
+ message: "Minimum duration is 10 seconds",
+ });
+ }
+ });
+
+export const monitorSchema = z.object({
+ type: z.string().min(1, "You must select an option"),
+ url: z.string().min(1, "URL is required").regex(urlRegex, "Invalid URL"),
+ n: z.coerce
+ .number({ message: "Number required" })
+ .min(1, "Minimum value is 1")
+ .max(25, "Maximum value is 25"),
+ notificationChannels: z.array(z.string()).optional().default([]),
+ name: z.string().min(1, "Display name is required"),
+ interval: durationSchema,
+});
diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js
index 55141be3e..67c267ffd 100644
--- a/client/src/Validation/validation.js
+++ b/client/src/Validation/validation.js
@@ -437,6 +437,16 @@ const infrastructureMonitorValidation = joi.object({
"number.base": "Frequency must be a number.",
"any.required": "Frequency is required.",
}),
+ statusWindowSize: joi.number().min(1).max(20).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 cannot exceed 20.",
+ }),
+ statusWindowThreshold: joi.number().min(1).max(100).messages({
+ "number.base": "Status window threshold must be a number.",
+ "number.min": "Status window threshold must be at least 1%.",
+ "number.max": "Status window threshold cannot exceed 100%.",
+ }),
notifications: joi.array().items(joi.string()),
});
diff --git a/client/src/locales/ar.json b/client/src/locales/ar.json
index 60a2db4a4..d8019d5de 100644
--- a/client/src/locales/ar.json
+++ b/client/src/locales/ar.json
@@ -376,7 +376,52 @@
"email": "",
"role": "",
"created": ""
- }
+ },
+ "addTeamMember": {
+ "addMemberMenu": "",
+ "title": "",
+ "description": "",
+ "addButton": ""
+ },
+ "register": "",
+ "registerToast": {
+ "success": "",
+ "dbUserExists": "",
+ "unknownError": ""
+ },
+ "registerTeamMember": {
+ "title": "",
+ "auth": {
+ "common": {
+ "inputs": {
+ "firstName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "email": {
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "role": {
+ "errors": {
+ "empty": ""
+ }
+ }
+ }
+ }
+ }
+ },
+ "role": ""
},
"monitorState": {
"paused": "",
@@ -742,7 +787,8 @@
"failCountHeader": "",
"lastRunHeader": "",
"lastFinishedAtHeader": "",
- "lastRunTookHeader": ""
+ "lastRunTookHeader": "",
+ "intervalHeader": ""
},
"metricsTable": {
"title": "",
diff --git a/client/src/locales/cs.json b/client/src/locales/cs.json
index 10322c471..71c7e24b9 100644
--- a/client/src/locales/cs.json
+++ b/client/src/locales/cs.json
@@ -376,7 +376,52 @@
"email": "",
"role": "",
"created": ""
- }
+ },
+ "addTeamMember": {
+ "addMemberMenu": "",
+ "title": "",
+ "description": "",
+ "addButton": ""
+ },
+ "register": "",
+ "registerToast": {
+ "success": "",
+ "dbUserExists": "",
+ "unknownError": ""
+ },
+ "registerTeamMember": {
+ "title": "",
+ "auth": {
+ "common": {
+ "inputs": {
+ "firstName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "email": {
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "role": {
+ "errors": {
+ "empty": ""
+ }
+ }
+ }
+ }
+ }
+ },
+ "role": ""
},
"monitorState": {
"paused": "",
@@ -742,7 +787,8 @@
"failCountHeader": "",
"lastRunHeader": "",
"lastFinishedAtHeader": "",
- "lastRunTookHeader": ""
+ "lastRunTookHeader": "",
+ "intervalHeader": ""
},
"metricsTable": {
"title": "",
diff --git a/client/src/locales/de.json b/client/src/locales/de.json
index 115d9733f..ef8df5df4 100644
--- a/client/src/locales/de.json
+++ b/client/src/locales/de.json
@@ -380,7 +380,52 @@
"email": "Email",
"role": "Rolle",
"created": "Angelegt"
- }
+ },
+ "addTeamMember": {
+ "addMemberMenu": "",
+ "title": "",
+ "description": "",
+ "addButton": ""
+ },
+ "register": "",
+ "registerToast": {
+ "success": "",
+ "dbUserExists": "",
+ "unknownError": ""
+ },
+ "registerTeamMember": {
+ "title": "",
+ "auth": {
+ "common": {
+ "inputs": {
+ "firstName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "email": {
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "role": {
+ "errors": {
+ "empty": ""
+ }
+ }
+ }
+ }
+ }
+ },
+ "role": ""
},
"monitorState": {
"paused": "Pausiert",
@@ -746,7 +791,8 @@
"failCountHeader": "Anzahl Fehler",
"lastRunHeader": "Letzter Lauf um",
"lastFinishedAtHeader": "Zuletzt abgeschlossen um",
- "lastRunTookHeader": "Letzter Lauf dauerte"
+ "lastRunTookHeader": "Letzter Lauf dauerte",
+ "intervalHeader": ""
},
"metricsTable": {
"title": "Warteschlangenmetriken",
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 1aeb4b6a2..da19c38da 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -360,12 +360,6 @@
"demoUser": "Demo user"
},
"teamPanel": {
- "addTeamMember": {
- "addMemberMenu": "Add Team Member",
- "title": "Register new team member",
- "description": "Create a new user and share the credentials with them. This method gives the member immediate access to all monitors.",
- "addButton": "Add Member"
- },
"teamMembers": "Team members",
"filter": {
"all": "All",
@@ -381,6 +375,18 @@
"noMembers": "There are no team members with this role",
"getToken": "Get token",
"emailToken": "E-mail token",
+ "table": {
+ "name": "Name",
+ "email": "Email",
+ "role": "Role",
+ "created": "Created"
+ },
+ "addTeamMember": {
+ "addMemberMenu": "Add Team Member",
+ "title": "Register new team member",
+ "description": "Create a new user and share the credentials with them. This method gives the member immediate access to all monitors.",
+ "addButton": "Add Member"
+ },
"register": "Register a team member",
"registerToast": {
"success": "User created, share credentials with the member securely.",
@@ -420,11 +426,11 @@
}
},
"role": "Role",
- "table": {
- "name": "Name",
- "email": "Email",
- "role": "Role",
- "created": "Created"
+ "changeTeamPassword": {
+ "changePasswordMenu": "Reset Password",
+ "title": "Reset team member password",
+ "description": "Create a new password for this team member. You will need to share the password with them securely.",
+ "success": "Password successfully reset. Make sure to provide the credentials to the member in a secure way."
}
},
"monitorState": {
@@ -484,6 +490,7 @@
},
"uptimeGeneralInstructions": {
"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.",
+ "https": "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.",
"docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).",
"port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.",
@@ -785,14 +792,14 @@
"idHeader": "Monitor ID",
"urlHeader": "URL",
"typeHeader": "Type",
- "intervalHeader": "Interval",
"activeHeader": "Active",
"lockedAtHeader": "Locked at",
"runCountHeader": "Run count",
"failCountHeader": "Fail count",
"lastRunHeader": "Last run at",
"lastFinishedAtHeader": "Last finished at",
- "lastRunTookHeader": "Last run took"
+ "lastRunTookHeader": "Last run took",
+ "intervalHeader": "Interval"
},
"metricsTable": {
"title": "Queue metrics",
@@ -1094,9 +1101,13 @@
"chooseGame": "Choose game",
"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.",
+ "incidentConfigDescriptionV2": "Number of consecutive checks needed to change monitor status. Maximum 25",
"incidentConfigStatusWindowLabel": "How many checks should be in the sliding window?",
+ "incidentConfigStatusCheckNumber": "How many checks before status change?",
"incidentConfigStatusWindowThresholdLabel": "What percentage of checks in the sliding window fail/succeed before monitor status changes?",
- "incidentConfigTitle": "Incidents"
+ "incidentConfigTitle": "Incidents",
+ "intervalTitle": "Check interval",
+ "intervalDescription": "How often the monitor should be checked."
},
"dataRate": "Data Rate",
"dataReceived": "Data Received",
diff --git a/client/src/locales/es.json b/client/src/locales/es.json
index 52fd8c97b..440d40027 100644
--- a/client/src/locales/es.json
+++ b/client/src/locales/es.json
@@ -376,7 +376,52 @@
"email": "",
"role": "",
"created": ""
- }
+ },
+ "addTeamMember": {
+ "addMemberMenu": "",
+ "title": "",
+ "description": "",
+ "addButton": ""
+ },
+ "register": "",
+ "registerToast": {
+ "success": "",
+ "dbUserExists": "",
+ "unknownError": ""
+ },
+ "registerTeamMember": {
+ "title": "",
+ "auth": {
+ "common": {
+ "inputs": {
+ "firstName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "email": {
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "role": {
+ "errors": {
+ "empty": ""
+ }
+ }
+ }
+ }
+ }
+ },
+ "role": ""
},
"monitorState": {
"paused": "",
@@ -742,7 +787,8 @@
"failCountHeader": "",
"lastRunHeader": "",
"lastFinishedAtHeader": "",
- "lastRunTookHeader": ""
+ "lastRunTookHeader": "",
+ "intervalHeader": ""
},
"metricsTable": {
"title": "",
diff --git a/client/src/locales/fi.json b/client/src/locales/fi.json
index d42a9f1f9..d98ece35b 100644
--- a/client/src/locales/fi.json
+++ b/client/src/locales/fi.json
@@ -376,7 +376,52 @@
"email": "Sähköposti",
"role": "Rooli",
"created": "Luotu"
- }
+ },
+ "addTeamMember": {
+ "addMemberMenu": "",
+ "title": "",
+ "description": "",
+ "addButton": ""
+ },
+ "register": "",
+ "registerToast": {
+ "success": "",
+ "dbUserExists": "",
+ "unknownError": ""
+ },
+ "registerTeamMember": {
+ "title": "",
+ "auth": {
+ "common": {
+ "inputs": {
+ "firstName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "email": {
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "role": {
+ "errors": {
+ "empty": ""
+ }
+ }
+ }
+ }
+ }
+ },
+ "role": ""
},
"monitorState": {
"paused": "Tauota",
@@ -742,7 +787,8 @@
"failCountHeader": "",
"lastRunHeader": "",
"lastFinishedAtHeader": "",
- "lastRunTookHeader": ""
+ "lastRunTookHeader": "",
+ "intervalHeader": ""
},
"metricsTable": {
"title": "",
diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json
index d2a5fd19d..bfce12843 100644
--- a/client/src/locales/fr.json
+++ b/client/src/locales/fr.json
@@ -380,7 +380,52 @@
"email": "Email",
"role": "Rôle",
"created": "Créée"
- }
+ },
+ "addTeamMember": {
+ "addMemberMenu": "",
+ "title": "",
+ "description": "",
+ "addButton": ""
+ },
+ "register": "",
+ "registerToast": {
+ "success": "",
+ "dbUserExists": "",
+ "unknownError": ""
+ },
+ "registerTeamMember": {
+ "title": "",
+ "auth": {
+ "common": {
+ "inputs": {
+ "firstName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "email": {
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "role": {
+ "errors": {
+ "empty": ""
+ }
+ }
+ }
+ }
+ }
+ },
+ "role": ""
},
"monitorState": {
"paused": "Pause",
@@ -746,7 +791,8 @@
"failCountHeader": "Nombre d'échecs",
"lastRunHeader": "Dernière tentative le",
"lastFinishedAtHeader": "Dernière finalisation à",
- "lastRunTookHeader": "Dernière exécution effectuée"
+ "lastRunTookHeader": "Dernière exécution effectuée",
+ "intervalHeader": ""
},
"metricsTable": {
"title": "Métriques des files",
diff --git a/client/src/locales/ja.json b/client/src/locales/ja.json
index 4c90c485e..6107c19d0 100644
--- a/client/src/locales/ja.json
+++ b/client/src/locales/ja.json
@@ -380,7 +380,52 @@
"email": "メール",
"role": "役割",
"created": "作成日"
- }
+ },
+ "addTeamMember": {
+ "addMemberMenu": "",
+ "title": "",
+ "description": "",
+ "addButton": ""
+ },
+ "register": "",
+ "registerToast": {
+ "success": "",
+ "dbUserExists": "",
+ "unknownError": ""
+ },
+ "registerTeamMember": {
+ "title": "",
+ "auth": {
+ "common": {
+ "inputs": {
+ "firstName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "email": {
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "role": {
+ "errors": {
+ "empty": ""
+ }
+ }
+ }
+ }
+ }
+ },
+ "role": ""
},
"monitorState": {
"paused": "一時停止",
@@ -746,7 +791,8 @@
"failCountHeader": "失敗回数",
"lastRunHeader": "最終実行日時",
"lastFinishedAtHeader": "最終完了日時",
- "lastRunTookHeader": "最終実行時間"
+ "lastRunTookHeader": "最終実行時間",
+ "intervalHeader": ""
},
"metricsTable": {
"title": "キューメトリック",
diff --git a/client/src/locales/pt-BR.json b/client/src/locales/pt-BR.json
index 80fd34474..9e1fa6bc0 100644
--- a/client/src/locales/pt-BR.json
+++ b/client/src/locales/pt-BR.json
@@ -380,7 +380,52 @@
"email": "E-mail",
"role": "Cargo",
"created": "Criado"
- }
+ },
+ "addTeamMember": {
+ "addMemberMenu": "",
+ "title": "",
+ "description": "",
+ "addButton": ""
+ },
+ "register": "",
+ "registerToast": {
+ "success": "",
+ "dbUserExists": "",
+ "unknownError": ""
+ },
+ "registerTeamMember": {
+ "title": "",
+ "auth": {
+ "common": {
+ "inputs": {
+ "firstName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "email": {
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "role": {
+ "errors": {
+ "empty": ""
+ }
+ }
+ }
+ }
+ }
+ },
+ "role": ""
},
"monitorState": {
"paused": "Pausado",
@@ -746,7 +791,8 @@
"failCountHeader": "Contagem de falhas",
"lastRunHeader": "Última corrida em",
"lastFinishedAtHeader": "Última conclusão em",
- "lastRunTookHeader": "A última corrida aconteceu"
+ "lastRunTookHeader": "A última corrida aconteceu",
+ "intervalHeader": ""
},
"metricsTable": {
"title": "Métricas de fila",
diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json
index ecb1dd8f8..aa54de393 100644
--- a/client/src/locales/ru.json
+++ b/client/src/locales/ru.json
@@ -376,7 +376,52 @@
"email": "Email",
"role": "Роль",
"created": "Создано"
- }
+ },
+ "addTeamMember": {
+ "addMemberMenu": "",
+ "title": "",
+ "description": "",
+ "addButton": ""
+ },
+ "register": "",
+ "registerToast": {
+ "success": "",
+ "dbUserExists": "",
+ "unknownError": ""
+ },
+ "registerTeamMember": {
+ "title": "",
+ "auth": {
+ "common": {
+ "inputs": {
+ "firstName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "email": {
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "role": {
+ "errors": {
+ "empty": ""
+ }
+ }
+ }
+ }
+ }
+ },
+ "role": ""
},
"monitorState": {
"paused": "Остановлено",
@@ -421,23 +466,23 @@
"passwordRequirements": "Новый пароль должен содержать не менее 8 символов и содержать как минимум одну заглавную букву, одну строчную букву, одну цифру и один специальный символ.",
"saving": "Сохранение..."
},
- "emailSent": "",
- "failedToSendEmail": "",
- "settingsTestEmailSuccess": "",
- "settingsTestEmailFailed": "",
- "settingsTestEmailFailedWithReason": "",
+ "emailSent": "Письмо успешно отправлено",
+ "failedToSendEmail": "Не удалось отправить письмо",
+ "settingsTestEmailSuccess": "Тестовое письмо успешно отправлено",
+ "settingsTestEmailFailed": "Не удалось отправить тестовое письмо",
+ "settingsTestEmailFailedWithReason": "Не удалось отправить тестовое письмо: {{reason}}",
"settingsTestEmailUnknownError": "Неизвестная ошибка",
"statusMsg": {
- "paused": "",
+ "paused": "Мониторинг приостановлен",
"up": "",
"down": "",
- "pending": ""
+ "pending": "Ожидание..."
},
"uptimeGeneralInstructions": {
- "http": "",
- "ping": "",
- "docker": "",
- "port": "",
+ "http": "Введите URL-адрес или IP-адрес для мониторинга (например, https://example.com/ или 192.168.1.100) и добавьте понятное имя, которое будет отображаться на панели инструментов.",
+ "ping": "Ведите IP-адрес или имя хоста для пинга (например, 192.168.1.100 или example.com) и добавьте понятное имя, которое будет отображаться на панели инструментов.",
+ "docker": "Введите Docker ID вашего контейнера. Docker ID должен быть полным 64-символьным Docker ID. Вы можете запустить docker inspect , чтобы получить полный ID контейнера.",
+ "port": "Введите URL-адрес или IP-адрес сервера, номер порта и понятное имя, которое будет отображаться на панели инструментов.",
"game": ""
},
"common": {
@@ -447,24 +492,24 @@
"toggleTheme": ""
},
"toasts": {
- "networkError": "",
- "checkConnection": "",
+ "networkError": "Ошибка сети",
+ "checkConnection": "Пожалуйста, проверьте свое сетевое соединение",
"unknownError": "Неизвестная ошибка"
}
},
"auth": {
"common": {
"navigation": {
- "continue": "",
+ "continue": "Продолжить",
"back": "Назад"
},
"inputs": {
"email": {
- "label": "",
+ "label": "Электронная почта",
"placeholder": "",
"errors": {
"empty": "Для продолжения введите свой адрес электронной почты",
- "invalid": ""
+ "invalid": "Пожалуйста, проверьте правильность введённого адреса электронной почты"
}
},
"password": {
@@ -491,49 +536,49 @@
"highlighted": "одну прописную букву"
},
"match": {
- "beginning": "",
- "highlighted": ""
+ "beginning": "Пароли",
+ "highlighted": "должны совпадать"
}
},
"errors": {
- "empty": "",
- "length": "",
- "uppercase": "",
- "lowercase": "",
- "number": "",
- "special": "",
- "incorrect": ""
+ "empty": "Пожалуйста, введите пароль",
+ "length": "Пароль должен быть длиной не менее 8 символов",
+ "uppercase": "Пароль должен содержать минимум 1 прописную букву",
+ "lowercase": "Пароль должен содержать минимум 1 строчную букву",
+ "number": "Пароль должен содержать минимум 1 цифру",
+ "special": "Пароль должен содержать минимум 1 спецсимвол",
+ "incorrect": "Предоставленный вами пароль не соответствует нашим записям"
}
},
"passwordConfirm": {
- "label": "",
- "placeholder": "",
+ "label": "Подтвердите пароль",
+ "placeholder": "Введите пароль еще раз для подтверждения",
"errors": {
- "empty": "",
- "different": ""
+ "empty": "Пожалуйста, введите свой пароль еще раз для подтверждения (помогает избежать опечаток)",
+ "different": "Введённые пароли не совпадают, вероятно, один из них напечатан с ошибкой"
}
},
"firstName": {
- "label": "",
+ "label": "Имя",
"placeholder": "",
"errors": {
- "empty": "",
- "length": "",
- "pattern": ""
+ "empty": "Пожалуйста, введите свое имя",
+ "length": "Имя должно быть короче 50 символов",
+ "pattern": "Имя должно содержать только буквы, пробелы, апострофы или дефисы"
}
},
"lastName": {
- "label": "",
+ "label": "Фамилия",
"placeholder": "",
"errors": {
- "empty": "",
- "length": "",
- "pattern": ""
+ "empty": "Пожалуйста, введите свою фамилию",
+ "length": "Фамилия должна быть короче 50 символов",
+ "pattern": "Фамилия должна содержать только буквы, пробелы, апострофы или дефисы"
}
}
},
"errors": {
- "validation": ""
+ "validation": "Ошибка при проверке данных."
},
"fields": {
"password": {
@@ -549,20 +594,20 @@
}
},
"login": {
- "heading": "",
+ "heading": "Войдите, чтобы продолжить",
"subheadings": {
- "stepOne": "",
- "stepTwo": ""
+ "stepOne": "Введите свою электронную почту",
+ "stepTwo": "Введите свой пароль"
},
"links": {
- "forgotPassword": "",
- "register": "",
+ "forgotPassword": "Забыли пароль?",
+ "register": "У вас нет учетной записи?",
"forgotPasswordLink": "",
"registerLink": ""
},
"toasts": {
- "success": "",
- "incorrectPassword": ""
+ "success": "С возвращением! Вы успешно вошли в систему.",
+ "incorrectPassword": "Неверный пароль"
},
"errors": {
"password": {
@@ -573,42 +618,42 @@
},
"registration": {
"heading": {
- "superAdmin": "",
- "user": ""
+ "superAdmin": "Создать супер-администратора",
+ "user": "Зарегистрироваться"
},
"subheadings": {
- "stepOne": "",
- "stepTwo": "",
- "stepThree": ""
+ "stepOne": "Введите свои личные данные",
+ "stepTwo": "Введите вашу электронную почту",
+ "stepThree": "Создайте свой пароль"
},
"description": {
- "superAdmin": "",
- "user": ""
+ "superAdmin": "Создайте свой аккаунт супер-администратора, чтобы начать",
+ "user": "Зарегистрируйтесь как пользователь и обратитесь к супер-администратору для доступа к вашим мониторам"
},
"gettingStartedButton": {
- "superAdmin": "",
- "user": ""
+ "superAdmin": "Создать учетную запись супер-администратора",
+ "user": "Зарегистрироваться как обычный пользователь"
},
- "termsAndPolicies": "",
+ "termsAndPolicies": "Создавая учетную запись, вы соглашаетесь с нашими Условиями предоставления услуг и Политикой конфиденциальности.",
"links": {
- "login": ""
+ "login": "Уже есть учетная запись? Войти"
},
"toasts": {
- "success": ""
+ "success": "Добро пожаловать! Ваша учетная запись была создана успешно."
},
"welcome": ""
},
"forgotPassword": {
- "heading": "",
+ "heading": "Забыли пароль?",
"subheadings": {
- "stepOne": "",
- "stepTwo": "",
- "stepThree": "",
- "stepFour": ""
+ "stepOne": "Не волнуйтесь, мы вышлем вам инструкции для сброса.",
+ "stepTwo": "Мы выслали ссылку для сброса пароля на ",
+ "stepThree": "Ваш новый пароль должен отличаться от ваших предыдущих пароей.",
+ "stepFour": "Ваш пароль был успешно сброшен. Нажмите ниже, чтобы волшебным образом войти в систему."
},
"buttons": {
"openEmail": "",
- "resetPassword": ""
+ "resetPassword": "Сбросить пароль"
},
"imageAlts": {
"passwordKey": "",
@@ -742,7 +787,8 @@
"failCountHeader": "",
"lastRunHeader": "",
"lastFinishedAtHeader": "",
- "lastRunTookHeader": ""
+ "lastRunTookHeader": "",
+ "intervalHeader": ""
},
"metricsTable": {
"title": "",
diff --git a/client/src/locales/th.json b/client/src/locales/th.json
new file mode 100644
index 000000000..c3f571b6b
--- /dev/null
+++ b/client/src/locales/th.json
@@ -0,0 +1,1122 @@
+{
+ "submit": "ยอมรับ",
+ "title": "หัวข้อ",
+ "distributedStatusHeaderText": "ตรวจสอบแบบเรียลไทม์บนอุปกรณ์จริง",
+ "distributedStatusSubHeaderText": "ระบบทำงานด้วยอุปกรณ์นับล้านทั่วโลก ให้คุณตรวจสอบประสิทธิภาพระบบตามภูมิภาค ประเทศ หรือเมืองได้",
+ "settingsDisabled": "ปิดการใช้งาน",
+ "settingsSuccessSaved": "บันทึกการตั้งค่าเรียบร้อยแล้ว",
+ "settingsFailedToSave": "ไม่สามารถบันทึกการตั้งค่าได้",
+ "settingsStatsCleared": "สถิติถูกล้างเรียบร้อยแล้ว",
+ "settingsFailedToClearStats": "ไม่สามารถล้างสถิติได้",
+ "settingsMonitorsDeleted": "ลบมอนิเตอร์ออกทั้งหมดเรียบร้อยแล้ว",
+ "settingsFailedToDeleteMonitors": "ไม่สามารถลบมอนิเตอร์ออกทั้งหมดได้",
+ "starPromptTitle": "ให้ดาวกับฉัน",
+ "starPromptDescription": "ดูการอัปเดตล่าสุด และมีส่วนร่วมในการขยายชุมชนบน GitHub",
+ "https": "HTTPS",
+ "http": "HTTP",
+ "monitor": "monitor",
+ "aboutus": "เกี่ยวกับฉัน",
+ "now": "ตอนนี้",
+ "delete": "ลบ",
+ "configure": "ตั้งค่า",
+ "responseTime": "ระยะเวลาการตอบสนอง",
+ "ms": "ms",
+ "bar": "Bar",
+ "area": "พื้นที่",
+ "country": "ประเทศ",
+ "city": "จังหวัด",
+ "response": "การตอบสนอง",
+ "monitorStatusUp": "มอนิเตอร์ {name} ({url}) สถานะตอนนี้ออนไลน์และตอบสนองปกติ",
+ "monitorStatusDown": "มอนิเตอร์ {name} ({url}) สถานะตอนนี้ล่มและไม่ตอบสนอง",
+ "webhookSendSuccess": "การแจ้งเตือนผ่าน Webhook ถูกส่งเรียบร้อยแล้ว",
+ "webhookSendError": "ไม่สามารถส่งการแจ้งเตือนผ่าน Webhook ไปที่ {platform} ได้",
+ "webhookUnsupportedPlatform": "ระบบไม่รองรับแพลตฟอร์ม {platform} นี้",
+ "distributedRightCategoryTitle": "มอนิเตอร์",
+ "distributedStatusServerMonitors": "มอนิเตอร์เชิร์ฟเวอร์",
+ "distributedStatusServerMonitorsDescription": "ตรวจสอบสถานะของเซิร์ฟเวอร์ที่เกี่ยวข้อง",
+ "distributedUptimeCreateSelectURL": "คุณสามารถระบุ URL ของโฮสต์และเลือกประเภทของตัวตรวจสอบได้ที่นี่",
+ "distributedUptimeCreateChecks": "รายการตรวจสอบ",
+ "distributedUptimeCreateChecksDescription": "คุณสามารถเพิ่มหรือลบการตรวจสอบได้เสมอหลังจากเพิ่มเว็บไซต์ของคุณแล้ว",
+ "distributedUptimeCreateIncidentNotification": "การแจ้งเตือนปัญหา",
+ "distributedUptimeCreateIncidentDescription": "เมื่อเกิดเหตุการณ์ผิดปกติ ให้แจ้งเตือนผู้ใช้งาน",
+ "distributedUptimeCreateAdvancedSettings": "ตั้งค่าขั้นสูง",
+ "distributedUptimeDetailsNoMonitorHistory": "ยังไม่มีประวัติการตรวจสอบสำหรับมอนิเตอร์นี้",
+ "distributedUptimeDetailsStatusHeaderUptime": "เวลาระบบทำงาน:",
+ "distributedUptimeDetailsStatusHeaderLastUpdate": "แก้ไขล่าสุด",
+ "notifications": {
+ "enableNotifications": "เปิดการแจ้งเตือนของแพลตฟอร์ม {{platform}} นี้",
+ "testNotification": "ทดสอบระบบการแจ้งเตือน",
+ "addOrEditNotifications": "เพิ่มและแก้ไขระบบการแจ้งเตือน",
+ "slack": {
+ "label": "Slack",
+ "description": "เพื่อเปิดใช้งานการแจ้งเตือนผ่าน Slack ให้สร้างแอป Slack และเปิดใช้งาน Incoming Webhooks หลังจากนั้น เพียงใส่ URL ของ Webhook ที่นี่",
+ "webhookLabel": "URL ของ Webhook/เว็บฮุค",
+ "webhookPlaceholder": "https://hooks.slack.com/services/...",
+ "webhookRequired": "ต้องระบุ URL ของ Slack webhook"
+ },
+ "discord": {
+ "label": "Discord",
+ "description": "เพื่อส่งข้อมูลไปยังช่อง Discord ผ่าน Discord notifications โดยใช้ Webhooks ให้เปิดใช้งาน Incoming Webhooks ของ Discord",
+ "webhookLabel": "URL ของ Webhook/เว็บฮุค สำหรับ Discord",
+ "webhookPlaceholder": "https://discord.com/api/webhooks/...",
+ "webhookRequired": "ต้องระบุ URL ของ Discord webhook"
+ },
+ "telegram": {
+ "label": "Telegram",
+ "description": "เพื่อเปิดใช้งานการแจ้งเตือนผ่าน Telegram ให้สร้างบอท Telegram โดยใช้ BotFather ซึ่งเป็นบอททางการสำหรับสร้างและจัดการบอท Telegram จากนั้นนำ API token และ Chat ID มาใส่ที่นี่",
+ "tokenLabel": "Token/โทเคน ของบอท",
+ "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
+ "chatIdLabel": "Chat ID",
+ "chatIdPlaceholder": "-1001234567890",
+ "fieldsRequired": "ต้องระบุโทเค็นและรหัสแชทของ Telegram"
+ },
+ "webhook": {
+ "label": "Webhooks",
+ "description": "คุณสามารถตั้งค่าเว็บฮุคแบบกำหนดเองเพื่อรับการแจ้งเตือนเมื่อเกิดเหตุการณ์ผิดปกติ",
+ "urlLabel": "URL ของ Webhook",
+ "urlPlaceholder": "https://your-server.com/webhook",
+ "urlRequired": "ต้องระบุ URL ของ Webhook"
+ },
+ "testNotificationDevelop": "ทดสอบการแจ้งเตือน 2",
+ "integrationButton": "การรวมการแจ้งเตือน",
+ "testSuccess": "ส่งการแจ้งเตือนทดสอบเรียบร้อยแล้ว!",
+ "testFailed": "ไม่สามารถส่งการแจ้งเตือนทดสอบได้",
+ "unsupportedType": "การแจ้งเตือนไม่ได้รับการสนับสนุนประเภทนี้",
+ "networkError": "เกิดข้อผิดพลาดของเครือข่าย",
+ "fallback": {
+ "title": "ช่องทางแจ้งเตือนใช้เพื่อ:",
+ "checks": [
+ "แจ้งทีมเกี่ยวกับการหยุดทำงานหรือปัญหาประสิทธิภาพ",
+ "แจ้งวิศวกรเมื่อเกิดเหตุการณ์",
+ "ให้ผู้ดูแลระบบทราบเกี่ยวกับการเปลี่ยนแปลงของระบบ"
+ ],
+ "actionButton": "มาสร้างช่องทางการแจ้งเตือนแรกของคุณกันเถอะ!"
+ },
+ "createButton": "สร้างช่องทางแจ้งเตือน",
+ "createTitle": "ช่องทางแจ้งเตือน",
+ "create": {
+ "success": "สร้างช่องทางแจ้งเตือนสำเร็จแล้ว",
+ "failed": "ไม่สามารถสร้างช่องทางแจ้งเตือนได้"
+ },
+ "fetch": {
+ "success": "ดึงข้อมูลการแจ้งเตือนสำเร็จแล้ว",
+ "failed": "ไม่สามารถดึงข้อมูลการแจ้งเตือนได้"
+ },
+ "delete": {
+ "success": "ลบช่องทางแจ้งเตือนสำเร็จแล้ว",
+ "failed": "ไม่สามารถลบช่องทางแจ้งเตือนได้"
+ },
+ "edit": {
+ "success": "ปรับปรุงช่องทางแจ้งเตือนสำเร็จแล้ว",
+ "failed": "ไม่สามารถปรับปรุงการแจ้งเตือนได้"
+ },
+ "test": {
+ "success": "ส่งการแจ้งเตือนทดสอบสำเร็จแล้ว",
+ "failed": "ไม่สามารถส่งการแจ้งเตือนทดสอบได้"
+ }
+ },
+ "testLocale": "testLocale",
+ "add": "เพิ่ม",
+ "monitors": "monitors",
+ "distributedUptimeStatusCreateStatusPage": "หน้าตรวจสอบสถานะระบบ",
+ "distributedUptimeStatusCreateStatusPageAccess": "สิทธิ์เข้าถึง",
+ "distributedUptimeStatusCreateStatusPageReady": "เมื่อหน้าสถานะพร้อม คุณสามารถตั้งค่าเป็นเผยแพร่สู่สาธารณะได้",
+ "distributedUptimeStatusBasicInfoHeader": "ข้อมูลทั่วไป",
+ "distributedUptimeStatusBasicInfoDescription": "ระบุชื่อบริษัทและซับโดเมนสำหรับหน้าสถานะของคุณ",
+ "distributedUptimeStatusLogoHeader": "โลโก้",
+ "distributedUptimeStatusLogoDescription": "อัปโหลดโลโก้ให้ Status Page ของคุณ",
+ "distributedUptimeStatusLogoUploadButton": "อัพโหลดโลโก้",
+ "distributedUptimeStatusStandardMonitorsHeader": "มอนิเตอร์พื้นฐาน",
+ "distributedUptimeStatusStandardMonitorsDescription": "เพิ่มตัวตรวจสอบมาตรฐานไปยัง Status Page ของคุณ",
+ "distributedUptimeStatusCreateYour": "Create your",
+ "distributedUptimeStatusEditYour": "Edit your",
+ "distributedUptimeStatusPublishedLabel": "เผยแพร่และมองเห็นได้ต่อสาธารณะ",
+ "distributedUptimeStatusCompanyNameLabel": "ชื่อบริษัท",
+ "distributedUptimeStatusPageAddressLabel": "ที่อยู่หน้าสถานะของคุณ",
+ "distributedUptimeStatus30Days": "30 วัน",
+ "distributedUptimeStatus60Days": "60 วัน",
+ "distributedUptimeStatus90Days": "90 วัน",
+ "distributedUptimeStatusPageNotSetUp": "หน้าสถานะยังไม่ถูกสร้างขึ้น",
+ "distributedUptimeStatusContactAdmin": "กรุณาติดต่อผู้ดูแลระบบของคุณ เพื่อขอความช่วยเหลือ",
+ "distributedUptimeStatusPageNotPublic": "หน้าสถานะนี้ไม่เปิดเผยต่อสาธารณะ",
+ "distributedUptimeStatusPageDeleteDialog": "คุณต้องการลบหน้าสถานะนี้หรือไม่?",
+ "distributedUptimeStatusPageDeleteConfirm": "ใช่ ฉันต้องการลบหน้าสถานะนี้",
+ "distributedUptimeStatusPageDeleteDescription": "เมื่อถูกลบแล้ว หน้าสถานะของคุณจะไม่สามารถกู้คืนได้",
+ "distributedUptimeStatusDevices": "อุปกรณ์",
+ "distributedUptimeStatusUpt": "UPT",
+ "distributedUptimeStatusUptBurned": "UPT Burned",
+ "distributedUptimeStatusUptLogo": "Upt Logo",
+ "incidentsTableNoIncidents": "ยังไม่มีเหตุการณ์ผิดปกติบันทึกไว้",
+ "incidentsTablePaginationLabel": "เหตุการณ์ผิดปกติ",
+ "incidentsTableMonitorName": "ชื่อมอนิเตอร์",
+ "incidentsTableStatus": "สถานะ",
+ "incidentsTableDateTime": "วันและเวลา",
+ "incidentsTableStatusCode": "รหัสสถานะ",
+ "incidentsTableMessage": "ข้อความ",
+ "incidentsOptionsHeader": "เหตุการณ์ผิดปกติสำหรับ:",
+ "incidentsOptionsHeaderFilterBy": "กรองโดย:",
+ "incidentsOptionsHeaderFilterAll": "ท้ังหมด",
+ "incidentsOptionsHeaderFilterDown": "ล่ม",
+ "incidentsOptionsHeaderFilterCannotResolve": "ไม่แก้ไข/หาสาเหตุไม่ได้",
+ "incidentsOptionsHeaderShow": "แสดง:",
+ "incidentsOptionsHeaderLastHour": "ชั่วโมงที่แล้ว",
+ "incidentsOptionsHeaderLastDay": "วันที่แล้ว",
+ "incidentsOptionsHeaderLastWeek": "สัปดาห์ที่แล้ว",
+ "incidentsOptionsPlaceholderAllServers": "เชิร์ฟเวอร์ทั้งหมด",
+ "infrastructureCreateYour": "สร้าง",
+ "infrastructureCreateGeneralSettingsDescription": "คุณสามารถเลือก URL ของโฮสต์ พร้อมกับชื่อที่เป็นมิตร Friendly Name และรหัสลับการอนุญาตเพื่อเชื่อมต่อกับตัวแทนเซิร์ฟเวอร์ Server Agent",
+ "infrastructureServerRequirement": "เซิร์ฟเวอร์ที่คุณมอนิเตอร์ต้องกำลังทำงานด้วย",
+ "infrastructureCustomizeAlerts": "ปรับแต่งการแจ้งเตือน",
+ "infrastructureAlertNotificationDescription": "ส่งการแจ้งเตือนถึงผู้ใช้เมื่อค่าขีดจำกัดเกินเปอร์เซ็นต์ที่กำหนด",
+ "infrastructureCreateMonitor": "สร้าง Infrastructure Monitor",
+ "infrastructureProtocol": "โปรโตคอล",
+ "infrastructureServerUrlLabel": "URL ของเซิร์ฟเวอร์",
+ "infrastructureDisplayNameLabel": "ชื่อที่แสดง",
+ "infrastructureAuthorizationSecretLabel": "Authorization secret / คีย์ลับสำหรับการยืนยันตัวตน",
+ "gb": "กิกะไบต์ GB",
+ "mb": "เมกะไบต์ MB",
+ "mem": "แรม",
+ "memoryUsage": "การใช้งานแรม",
+ "cpu": "ซีพียู",
+ "cpuUsage": "การใช้งาน CPU",
+ "cpuTemperature": "อุณหภูมิ CPU",
+ "diskUsage": "การใช้งาน Disk",
+ "used": "ใช้งาน",
+ "total": "ทั้งหมด",
+ "cores": "คอร์",
+ "frequency": "ความถี่",
+ "status": "สถานะ",
+ "cpuPhysical": "ซีพียู (คอร์หลัก)",
+ "cpuLogical": "ซีพียู (คอร์เสมือน)",
+ "cpuFrequency": "ความถี่ของ CPU",
+ "avgCpuTemperature": "อุณหภูมิเฉลี่ยของซีพียู",
+ "memory": "แรม",
+ "disk": "ดิส/พื้นที่",
+ "uptime": "",
+ "os": "ระบบปฏิบัติการ",
+ "host": "โฮส",
+ "actions": "จัดการ",
+ "integrations": "การรวมระบบ",
+ "integrationsPrism": "เชื่อมต่อ Prism กับบริการที่คุณชื่นชอบ",
+ "integrationsSlack": "Slack",
+ "integrationsSlackInfo": "เชื่อมต่อกับ Slack และดูเหตุการณ์ผิดปกติในช่องทาง",
+ "integrationsDiscord": "Discord",
+ "integrationsDiscordInfo": "เชื่อมต่อกับ Discord และดูเหตุการณ์ผิดปกติได้โดยตรงในช่องทาง",
+ "integrationsZapier": "Zapier",
+ "integrationsZapierInfo": "ส่งเหตุการณ์ทั้งหมดไปยัง Zapier แล้วสามารถดูได้ทุกที่",
+ "commonSave": "บันทึก",
+ "createYour": "Create your",
+ "createMonitor": "",
+ "pause": "พักชั่วคราว",
+ "resume": "ดำเนินการต่อ",
+ "editing": "กำลังปรับปรุง…",
+ "url": "URL",
+ "access": "การเข้าถึง",
+ "timezone": "เขตเวลา",
+ "features": "ฟีเจอร์",
+ "administrator": "ผู้ดูแลระบบ",
+ "loginHere": "กรุณาเข้าสู่ระบบที่นี่",
+ "displayName": "ชื่อที่แสดง",
+ "urlMonitor": "ลิงก์ที่จะมอนิเตอร์",
+ "portToMonitor": "พอร์ตที่จะมอนิเตอร์",
+ "websiteMonitoring": "เว็บไชต์ที่จะมอนิเตอร์",
+ "websiteMonitoringDescription": "ใช้ HTTP(s) เพื่อตรวจสอบเว็บไซต์หรือ API endpoint ของคุณ",
+ "pingMonitoring": "มอนิเตอร์แบบ Ping",
+ "pingMonitoringDescription": "ตรวจสอบว่าเซิร์ฟเวอร์ของคุณพร้อมใช้งานหรือไม่",
+ "dockerContainerMonitoring": "มอนิเตอร์คอนเทนเนอร์ใน Docker",
+ "dockerContainerMonitoringDescription": "ตรวจสอบว่าคอนเทนเนอร์ Docker ของคุณกำลังทำงานอยู่หรือไม่",
+ "portMonitoring": "มอนิเตอร์แบบ Ping",
+ "portMonitoringDescription": "ตรวจสอบว่าพอร์ตของคุณเปิดอยู่หรือไม่",
+ "createMaintenanceWindow": "สร้างช่วงเวลาบำรุงรักษา",
+ "createMaintenance": "สร้างการบำรุงรักษา",
+ "editMaintenance": "แก้ไขการบำรุงรักษา",
+ "maintenanceWindowName": "ชื่อหน้าบำรุงรักษา",
+ "friendlyNameInput": "ชื่อแสดงผล",
+ "friendlyNamePlaceholder": "Maintenance เริ่มต้นเวลา __ : __ และใช้เวลา ___ นาที",
+ "maintenanceRepeat": "ทำซ้ำการบำรุงรักษา",
+ "maintenance": "การบำรุงรักษา",
+ "duration": "ระยะเวลา",
+ "addMonitors": "เพิ่มมอนิเตอร์",
+ "window": "ช่วงเวลา",
+ "cancel": "ยกเลิก",
+ "message": "ข้อความ",
+ "low": "Low",
+ "high": "High",
+ "statusCode": "รหัสสถานะ",
+ "date&Time": "วันที่และเวลา",
+ "type": "ประเภท",
+ "statusPageName": "ชื่อ Status Page",
+ "publicURL": "URL สาธารณะ",
+ "repeat": "ทำซ้ำ",
+ "edit": "แก้ไข",
+ "createA": "สร้าง A",
+ "remove": "ลบ",
+ "maintenanceWindowDescription": "ช่วงเวลาการบำรุงรักษา การตรวจสอบทั้งหมดของมอนิเตอร์ที่ถูกเลือกจะถูกระงับ ไม่ทำการตรวจสอบเครือข่ายใด ๆ ทำให้ไม่มีการอัปเดตสถานะหรือแจ้งเตือนเกิดขึ้น มอนิเตอร์ของคุณจะคงแสดงสถานะล่าสุดที่ทราบ และหน้าสถานะจะมีสัญลักษณ์บ่งบอกว่ากำลังอยู่ระหว่างการบำรุงรักษา เมื่อเวลาการบำรุงรักษาสิ้นสุด การตรวจสอบจะกลับมาทำงานโดยอัตโนมัติ และหากพบปัญหา การแจ้งเตือนจะถูกส่งออก ช่วงเวลาการบำรุงรักษาจะไม่ถูกนับรวมในการคำนวณความพร้อมใช้งาน",
+ "startTime": "เวลาเริ่มต้น",
+ "timeZoneInfo": "วันที่และเวลาทั้งหมดเป็นเวลาตามโซน GMT+0",
+ "monitorsToApply": "มอนิเตอร์ที่จะใช้ช่วงเวลาการบำรุงรักษา",
+ "nextWindow": "ช่วงเวลาถัดไป",
+ "notFoundButton": "ไปยังหน้าจอแดชบอร์ดหลัก",
+ "pageSpeedConfigureSettingsDescription": "คุณสามารถเลือก URL ของโฮสต์ พร้อมกับประเภทของมอนิเตอร์ได้",
+ "monitorDisplayName": "ชื่อที่แสดงของมอนิเตอร์",
+ "whenNewIncident": "เมื่อเกิดเหตุการณ์ใหม่",
+ "notifySMS": "แจ้งเตือนทางSMS (กำลังจะมา)",
+ "notifyEmails": "แจ้งเตือนทางอีเมลไปยังหลายที่อยู่ด้วย (กำลังจะมา)",
+ "seperateEmails": "คุณสามารถแยกอีเมลหลายอันด้วยเครื่องหมายจุลภาค , ได้",
+ "checkFrequency": "ความถี่ในการตรวจสอบ",
+ "matchMethod": "วิธีการจับคู่",
+ "expectedValue": "ค่าที่คาดหวัง",
+ "deleteDialogTitle": "คุณแน่ใจหรือว่าต้องการลบมอนิเตอร์นี้ไหม",
+ "deleteDialogDescription": "เมื่อลบแล้ว มอนิเตอร์นี้ไม่สามารถกู้คืนได้",
+ "pageSpeedMonitor": "มอนิเตอร์ความเร็วหน้าเว็บ",
+ "shown": "แสดง",
+ "ago": "ที่ผ่านมา",
+ "companyName": "ชื่อบริษัท",
+ "pageSpeedDetailsPerformanceReport": "เป็นค่าโดยประมาณและอาจแตกต่างกันไป",
+ "pageSpeedDetailsPerformanceReportCalculator": "ดูเครื่องคิดเลข",
+ "checkingEvery": "ตรวจสอบทุก",
+ "statusPageCreateSettings": "หากหน้าสถานะของคุณพร้อมแล้ว คุณสามารถทำเครื่องหมายว่าเผยแพร่แล้วได้",
+ "basicInformation": "ข้อมูลพื้นฐาน",
+ "statusPageCreateBasicInfoDescription": "กำหนดชื่อบริษัทและซับโดเมนที่หน้าสถานะของคุณชี้ไป",
+ "statusPageCreateSelectTimeZoneDescription": "เลือกเขตเวลาที่หน้าสถานะของคุณจะแสดง",
+ "statusPageCreateAppearanceDescription": "กําหนดรูปลักษณ์เริ่มต้นของหน้าสถานะสาธารณะของคุณ",
+ "statusPageCreateSettingsCheckboxLabel": "เผยแพร่และเปิดเผยต่อสาธารณะ",
+ "statusPageCreateBasicInfoStatusPageAddress": "ที่อยู่หน้าสถานะของคุณ",
+ "statusPageCreateTabsContent": "หน้าสถานะเซิร์ฟเวอร์",
+ "statusPageCreateTabsContentDescription": "คุณสามารถเพิ่มเซิร์ฟเวอร์ที่คุณตรวจสอบเข้าไปยังหน้าสถานะของคุณได้จำนวนเท่าใดก็ได้ และสามารถจัดลำดับใหม่เพื่อการแสดงผลที่ดีที่สุด",
+ "statusPageCreateTabsContentFeaturesDescription": "แสดงรายละเอียดเพิ่มเติมบนหน้าสถานะ",
+ "showCharts": "แสดงแผนภูมิ",
+ "showUptimePercentage": "แสดงเปอร์เซ็นต์ความพร้อมใช้งาน",
+ "removeLogo": "ลบโลโก้",
+ "statusPageStatus": "ยังไม่ได้ตั้งค่าหน้าสถานะสาธารณะ",
+ "statusPageStatusContactAdmin": "กรุณาติดต่อผู้ดูแลระบบของคุณ",
+ "statusPageStatusNotPublic": "หน้าสถานะนี้ไม่เป็นสาธารณะ",
+ "statusPageStatusNoPage": "ที่นี่ยังไม่มีหน้าสถานะ",
+ "statusPageStatusServiceStatus": "สถานะบริการ",
+ "deleteStatusPage": "คุณต้องการลบหน้าสถานะนี้หรือไม่ไหม?",
+ "deleteStatusPageConfirm": "ใช่ ลบหน้าสถานะ",
+ "deleteStatusPageDescription": "เมื่อลบแล้ว หน้าสถานะของคุณไม่สามารถกู้คืนได้",
+ "uptimeCreate": "ค่าที่คาดหวังถูกใช้เพื่อตรวจสอบผลลัพธ์การตอบกลับ และการจับคู่จะเป็นตัวกำหนดสถานะ",
+ "uptimeCreateJsonPath": "นิพจน์นี้จะถูกประเมินกับข้อมูล JSON ที่ตอบกลับ และผลลัพธ์จะถูกใช้เพื่อตรวจสอบกับค่าที่คาดหวัง ดู",
+ "uptimeCreateJsonPathQuery": "for query language documentation.",
+ "maintenanceTableActionMenuDialogTitle": "คุณแน่ใจหรือว่าต้องการลบช่วงเวลาการบำรุงรักษานี้?",
+ "infrastructureEditYour": "Edit your",
+ "infrastructureEditMonitor": "บันทึก Infrastructure มอนิเตอร์",
+ "infrastructureMonitorCreated": "สร้างมอนิเตอร์ Infrastructure เรียบร้อยแล้ว!",
+ "infrastructureMonitorUpdated": "อัพเดทมอนิเตอร์ Infrastructure เรียบร้อยแล้ว!",
+ "errorInvalidTypeId": "ประเภทการแจ้งเตือนที่ระบุไม่ถูกต้อง",
+ "errorInvalidFieldId": "รหัสฟิลด์ที่ระบุไม่ถูกต้อง",
+ "inviteNoTokenFound": "ไม่พบโทเค็นคำเชิญ",
+ "pageSpeedWarning": "คำเตือน: คุณยังไม่ได้เพิ่มคีย์ Google PageSpeed API หากไม่มีคีย์นี้ มอนิเตอร์ความเร็วหน้าเว็บจะไม่ทำงาน",
+ "pageSpeedLearnMoreLink": "คลิกที่นี่",
+ "pageSpeedAddApiKey": "เพื่อเพิ่มคีย์ API ของคุณ",
+ "update": "อัปเดต",
+ "invalidFileFormat": "รูปแบบไฟล์ไม่รองรับ!",
+ "invalidFileSize": "ขนาดไฟล์ใหญ่เกินไป!",
+ "ClickUpload": "คลิกเพื่ออัปโหลด",
+ "DragandDrop": "ลากและวางเลย",
+ "MaxSize": "ขนาดสูงสุด",
+ "SupportedFormats": "รูปแบบที่รองรับ",
+ "FirstName": "ชื่อจริง",
+ "LastName": "นามสกุล",
+ "EmailDescriptionText": "นี่คือที่อยู่อีเมลปัจจุบันของคุณ — ไม่สามารถแก้ไขได้",
+ "YourPhoto": "รูปโปรไฟล์",
+ "PhotoDescriptionText": "รูปนี้จะแสดงบนหน้าประวัติของคุณ",
+ "save": "บันทึก",
+ "DeleteDescriptionText": "การทำเช่นนี้จะลบบัญชีและข้อมูลทั้งหมดที่เกี่ยวข้องบนเซิร์ฟเวอร์ ไม่สามารถกู้คืนได้",
+ "DeleteAccountWarning": "การลบบัญชีหมายความว่าคุณจะไม่สามารถเข้าสู่ระบบอีก และข้อมูลทั้งหมดของคุณจะถูกลบ ไม่สามารถกู้คืนได้",
+ "DeleteWarningTitle": "คุณแน่ใจหรือว่าต้องการลบบัญชีนี้?",
+ "bulkImport": {
+ "title": "นำเข้าจำนวนมาก",
+ "selectFileTips": "เลือกไฟล์ CSV เพื่อนำเข้า",
+ "selectFileDescription": "คุณสามารถดาวน์โหลด template หรือ sample ของเราได้",
+ "selectFile": "เลือกไฟล์",
+ "parsingFailed": "การแยกวิเคราะห์ล้มเหลว",
+ "uploadSuccess": "สร้างมอนิเตอร์เรียบร้อยแล้ว!",
+ "validationFailed": "การตรวจสอบล้มเหลว",
+ "noFileSelected": "ยังไม่ได้เลือกไฟล์",
+ "fallbackPage": "นำเข้าไฟล์เพื่ออัปโหลดรายการเซิร์ฟเวอร์เป็นกลุ่ม",
+ "invalidFileType": "ประเภทไฟล์ไม่ถูกต้อง",
+ "uploadFailed": "อัปโหลดล้มเหลว"
+ },
+ "DeleteAccountTitle": "ลบบัญชี",
+ "DeleteAccountButton": "ลบบัญชี",
+ "publicLink": "ลิงก์สาธารณะ",
+ "maskedPageSpeedKeyPlaceholder": "*************************************",
+ "reset": "รีเช็ต",
+ "ignoreTLSError": "ละเว้นข้อผิดพลาด TLS/SSL",
+ "tlsErrorIgnored": "ละเว้นข้อผิดพลาด TLS/SSL แล้ว",
+ "ignoreTLSErrorDescription": "ละเว้นข้อผิดพลาด TLS/SSL และดำเนินการตรวจสอบความพร้อมใช้งานของเว็บไซต์ต่อ",
+ "createNew": "สร้างใหม่",
+ "greeting": {
+ "prepend": "สวัสดี",
+ "append": "ช่วงบ่ายคือเวลาเล่นของคุณ—มาทำให้สุดยอดกันเถอะ!",
+ "overview": "นี่คือภาพรวมระบบมอนิเตอร์ประเภท {{type}} ของคุณ"
+ },
+ "roles": {
+ "superAdmin": "Super admin",
+ "admin": "แอดมิน",
+ "teamMember": "สมาชิกในทีม",
+ "demoUser": "ผู้ใช้ Demo"
+ },
+ "teamPanel": {
+ "teamMembers": "สมาชิกในทีม",
+ "filter": {
+ "all": "ทั้งหมด",
+ "member": "สมาชิก"
+ },
+ "inviteTeamMember": "เชิญสมาชิกเข้าทีม",
+ "inviteNewTeamMember": "เชิญสมาชิกทีมใหม่",
+ "inviteDescription": "เมื่อคุณเพิ่มสมาชิกทีมใหม่ เขาจะสามารถเข้าถึงมอนิเตอร์ทั้งหมดได้",
+ "email": "อีเมล",
+ "selectRole": "เลือกบทบาท",
+ "inviteLink": "ลิงก์เชิญ",
+ "cancel": "ยกเลิก",
+ "noMembers": "ยังไม่มีสมาชิกทีมในบทบาทนี้",
+ "getToken": "รับโทเค็น",
+ "emailToken": "Token ทางอีเมล",
+ "table": {
+ "name": "ชื่อ",
+ "email": "อีเมล",
+ "role": "บทบาท",
+ "created": "สร้างเมื่อ"
+ },
+ "addTeamMember": {
+ "addMemberMenu": "เพิ่มสมาชิกทีม",
+ "title": "ลงทะเบียนสมาชิกทีมใหม่",
+ "description": "สร้างผู้ใช้ใหม่และแชร์ข้อมูลการเข้าสู่ระบบให้กับพวกเขา วิธีนี้จะทำให้สมาชิกสามารถเข้าถึงตัวตรวจสอบทั้งหมดได้ทันที",
+ "addButton": "เพิ่มสมาชิก"
+ },
+ "register": "ลงทะเบียนสมาชิกทีม",
+ "registerToast": {
+ "success": "สร้างผู้ใช้เรียบร้อยแล้ว กรุณาแชร์ข้อมูลการเข้าสู่ระบบให้สมาชิกอย่างปลอดภัย",
+ "dbUserExists": "ผู้ใช้นี้มีอยู่แล้ว",
+ "unknownError": "เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ"
+ },
+ "registerTeamMember": {
+ "title": "ลงทะเบียนสมาชิกทีม",
+ "auth": {
+ "common": {
+ "inputs": {
+ "firstName": {
+ "errors": {
+ "empty": "กรุณากรอกชื่อ",
+ "pattern": "ชื่อต้องมีเฉพาะตัวอักษร เว้นวรรค เครื่องหมายอัญประกาศ หรือขีดกลาง"
+ }
+ },
+ "lastName": {
+ "errors": {
+ "empty": "กรุณากรอกนามสกุล",
+ "pattern": "นามสกุลต้องมีเฉพาะตัวอักษร เว้นวรรค เครื่องหมายอัญประกาศ หรือขีดกลาง"
+ }
+ },
+ "email": {
+ "errors": {
+ "empty": "กรุณากรอกอีเมลเพื่อดำเนินการต่อ",
+ "invalid": "กรุณาตรวจสอบความถูกต้องของอีเมลอีกครั้ง"
+ }
+ },
+ "role": {
+ "errors": {
+ "empty": "ต้องระบุบทบาท"
+ }
+ }
+ }
+ }
+ }
+ },
+ "role": "บทบาท"
+ },
+ "monitorState": {
+ "paused": "หยุดชั่วคราว",
+ "resumed": "ดำเนินการต่อ",
+ "active": "ใช้งานอยู่"
+ },
+ "menu": {
+ "uptime": "ความพร้อมใช้งาน",
+ "pagespeed": "ความเร็วหน้าเว็บ",
+ "infrastructure": "โครงสร้างพื้นฐาน",
+ "incidents": "เหตุการณ์",
+ "statusPages": "หน้าสถานะ",
+ "maintenance": "การบำรุงรักษา",
+ "integrations": "การรวมระบบ",
+ "settings": "การตั้งค่า",
+ "support": "การสนับสนุน",
+ "discussions": "การสนทนา",
+ "docs": "เอกสาร",
+ "changelog": "บันทึกการเปลี่ยนแปลง",
+ "profile": "โปรไฟล์",
+ "password": "รหัสผ่าน",
+ "team": "ทีม",
+ "logOut": "ออกจากระบบ",
+ "notifications": "การแจ้งเตือน",
+ "logs": "Logs"
+ },
+ "settingsEmailUser": "อีเมลผู้ใช้ - ชื่อผู้ใช้สำหรับการยืนยันตัวตน จะใช้แทนอีเมลหากระบุไว้",
+ "state": "สถานะ",
+ "statusBreadCrumbsStatusPages": "หน้าสถานะ",
+ "statusBreadCrumbsDetails": "รายละเอียด",
+ "commonSaving": "กำลังบันทึก...",
+ "navControls": "การควบคุม",
+ "incidentsPageTitle": "เหตุการณ์",
+ "passwordPanel": {
+ "passwordChangedSuccess": "รหัสผ่านของคุณถูกเปลี่ยนเรียบร้อยแล้ว",
+ "passwordInputIncorrect": "คุณป้อนรหัสผ่านที่ไม่ถูกต้อง",
+ "currentPassword": "รหัสผ่านปัจจุบัน",
+ "enterCurrentPassword": "กรอกรหัสผ่านปัจจุบันของคุณ",
+ "newPassword": "รหัสผ่านใหม่",
+ "enterNewPassword": "กรอกรหัสผ่านใหม่ของคุณ",
+ "confirmNewPassword": "ยืนยันรหัสผ่านใหม่อีกครั้ง",
+ "passwordRequirements": "รหัสผ่านใหม่ต้องมีอย่างน้อย 8 ตัวอักษร และต้องมีตัวอักษรใหญ่ ตัวอักษรเล็ก ตัวเลข และอักขระพิเศษอย่างละอย่าง 1",
+ "saving": "กำลังบันทึก..."
+ },
+ "emailSent": "ส่งอีเมลเรียบร้อยแล้ว",
+ "failedToSendEmail": "ส่งอีเมลล้มเหลว",
+ "settingsTestEmailSuccess": "ส่งอีเมลทดสอบเรียบร้อยแล้ว",
+ "settingsTestEmailFailed": "ส่งอีเมลทดสอบล้มเหลว",
+ "settingsTestEmailFailedWithReason": "ส่งอีเมลทดสอบล้มเหลว: {{reason}}",
+ "settingsTestEmailUnknownError": "ข้อผิดพลาดไม่ทราบสาเหตุ",
+ "statusMsg": {
+ "paused": "การตรวจสอบถูกพักชั่วคราว",
+ "up": "เว็บไซต์ของคุณใช้งานได้",
+ "down": "เว็บไซต์ของคุณล่ม",
+ "pending": "รอดำเนินการ..."
+ },
+ "uptimeGeneralInstructions": {
+ "http": "กรอก URL หรือ IP ที่ต้องการตรวจสอบ (เช่น https://example.com/\n หรือ 192.168.1.100) และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด",
+ "ping": "กรอกที่อยู่ IP หรือชื่อโฮสต์ที่ต้องการ ping (เช่น 192.168.1.100 หรือ example.com) และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด",
+ "docker": "กรอกชื่อหรือ ID ของคอนเทนเนอร์ Docker คุณสามารถใช้ชื่อคอนเทนเนอร์ (เช่น my-app) หรือ ID ของคอนเทนเนอร์ (ID เต็ม 64 ตัวอักษร หรือ ID สั้น)",
+ "port": "กรอก URL หรือ IP ของเซิร์ฟเวอร์ หมายเลขพอร์ต และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด",
+ "game": "กรอกที่อยู่ IP หรือชื่อโฮสต์และหมายเลขพอร์ตเพื่อทำการ ping (เช่น 192.168.1.100 หรือ example.com) และเลือกประเภทเกม"
+ },
+ "common": {
+ "appName": "Checkmate",
+ "monitoringAgentName": "Capture",
+ "buttons": {
+ "toggleTheme": "สลับโหมดสว่าง & มืด"
+ },
+ "toasts": {
+ "networkError": "ข้อผิดพลาดเครือข่าย",
+ "checkConnection": "กรุณาตรวจสอบการเชื่อมต่อของคุณ",
+ "unknownError": "ข้อผิดพลาดไม่ทราบสาเหตุ"
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "ดำเนินการต่อ",
+ "back": "ย้อนกลับ"
+ },
+ "inputs": {
+ "email": {
+ "label": "อีเมล",
+ "placeholder": "jordan.ellis@domain.com",
+ "errors": {
+ "empty": "เพื่อดำเนินการต่อ กรุณากรอกที่อยู่อีเมลของคุณ",
+ "invalid": "กรุณาตรวจสอบความถูกต้องของอีเมลที่กรอก"
+ }
+ },
+ "password": {
+ "label": "รหัสผ่าน",
+ "rules": {
+ "length": {
+ "beginning": "ต้องมีความยาวอย่างน้อย",
+ "highlighted": "8 ตัวอักษร"
+ },
+ "special": {
+ "beginning": "ต้องมีอย่างน้อย",
+ "highlighted": "อักขระพิเศษ 1 ตัว"
+ },
+ "number": {
+ "beginning": "ต้องมีอย่างน้อย",
+ "highlighted": "ตัวเลข 1 ตัว"
+ },
+ "uppercase": {
+ "beginning": "ต้องมีอย่างน้อย",
+ "highlighted": "ตัวอักษรใหญ่ 1 ตัว"
+ },
+ "lowercase": {
+ "beginning": "ต้องมีอย่างน้อย",
+ "highlighted": "ตัวอักษรเล็ก 1 ตัว"
+ },
+ "match": {
+ "beginning": "รหัสผ่าน",
+ "highlighted": "ต้องตรงกัน"
+ }
+ },
+ "errors": {
+ "empty": "กรุณากรอกรหัสผ่านของคุณ",
+ "length": "รหัสผ่านต้องมีความยาวอย่างน้อย 8 ตัวอักษร",
+ "uppercase": "รหัสผ่านต้องมีตัวอักษรใหญ่อย่างน้อย 1 ตัว",
+ "lowercase": "รหัสผ่านต้องมีตัวอักษรเล็กอย่างน้อย 1 ตัว",
+ "number": "รหัสผ่านต้องมีตัวเลขอย่างน้อย 1 ตัว",
+ "special": "รหัสผ่านต้องมีอักขระพิเศษอย่างน้อย 1 ตัว",
+ "incorrect": "รหัสผ่านที่คุณกรอกไม่ตรงกับข้อมูลของเรา"
+ }
+ },
+ "passwordConfirm": {
+ "label": "ยืนยันรหัสผ่าน",
+ "placeholder": "กรอกรหัสผ่านอีกครั้งเพื่อยืนยัน",
+ "errors": {
+ "empty": "กรุณากรอกรหัสผ่านอีกครั้งเพื่อยืนยัน (ช่วยลดข้อผิดพลาดจากการพิมพ์ผิด)",
+ "different": "รหัสผ่านที่กรอกไม่ตรงกัน อาจมีตัวใดตัวหนึ่งพิมพ์ผิด"
+ }
+ },
+ "firstName": {
+ "label": "ชื่อ",
+ "placeholder": "Brownyroll",
+ "errors": {
+ "empty": "กรุณากรอกชื่อของคุณ",
+ "length": "ชื่อต้องมีความยาวไม่เกิน 50 ตัวอักษร",
+ "pattern": "ชื่อต้องประกอบด้วยตัวอักษร ช่องว่าง เครื่องหมาย ' หรือ - เท่านั้น"
+ }
+ },
+ "lastName": {
+ "label": "นามสกุล",
+ "placeholder": ".",
+ "errors": {
+ "empty": "กรุณากรอกนามสกุลของคุณ",
+ "length": "นามสกุลต้องมีความยาวไม่เกิน 50 ตัวอักษร",
+ "pattern": "นามสกุลต้องประกอบด้วยตัวอักษร ช่องว่าง เครื่องหมาย ' หรือ - เท่านั้น"
+ }
+ }
+ },
+ "errors": {
+ "validation": "เกิดข้อผิดพลาดในการตรวจสอบข้อมูล"
+ },
+ "fields": {
+ "password": {
+ "errors": {
+ "incorrect": "รหัสผ่านที่คุณป้อนไม่ตรงกับข้อมูลของเรา"
+ }
+ },
+ "role": {
+ "errors": {
+ "min": "ต้องมีอย่างน้อยหนึ่งบทบาท"
+ }
+ }
+ }
+ },
+ "login": {
+ "heading": "เข้าสู่ระบบเพื่อดำเนินการต่อ",
+ "subheadings": {
+ "stepOne": "กรอกอีเมลของคุณ",
+ "stepTwo": "กรอกรหัสผ่านของคุณ"
+ },
+ "links": {
+ "forgotPassword": "ลืมรหัสผ่าน?",
+ "register": "ยังไม่มีบัญชีใช่ไหม?",
+ "forgotPasswordLink": "รีเซ็ตรหัสผ่าน",
+ "registerLink": "สมัครสมาชิกที่นี่"
+ },
+ "toasts": {
+ "success": "ยินดีต้อนรับกลับ! คุณเข้าสู่ระบบเรียบร้อยแล้ว",
+ "incorrectPassword": "รหัสผ่านไม่ถูกต้อง"
+ },
+ "errors": {
+ "password": {
+ "incorrect": "รหัสผ่านที่คุณป้อนไม่ตรงกับข้อมูลของเรา"
+ }
+ },
+ "welcome": "ยินดีต้อนรับกลับสู่ Checkmate!"
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "สร้าง Super Admin",
+ "user": "สมัครสมาชิก"
+ },
+ "subheadings": {
+ "stepOne": "กรอกรายละเอียดส่วนตัวของคุณ",
+ "stepTwo": "กรอกอีเมลของคุณ",
+ "stepThree": "สร้างรหัสผ่านของคุณ"
+ },
+ "description": {
+ "superAdmin": "สร้างบัญชี Super Admin เพื่อเริ่มต้นใช้งาน",
+ "user": "สมัครสมาชิกผู้ใช้และขอสิทธิ์เข้าถึงมอนิเตอร์จาก Super Admin"
+ },
+ "gettingStartedButton": {
+ "superAdmin": "สร้างบัญชี Super Admin",
+ "user": "สมัครสมาชิกผู้ใช้ทั่วไป"
+ },
+ "termsAndPolicies": "การสร้างบัญชีถือว่าคุณยอมรับ ข้อกำหนดการให้บริการ และ นโยบายความเป็นส่วนตัว ของเรา",
+ "links": {
+ "login": "มีบัญชีแล้ว? เข้าสู่ระบบ"
+ },
+ "toasts": {
+ "success": "ยินดีต้อนรับ! สร้างบัญชีเรียบร้อยแล้ว"
+ },
+ "welcome": "ยินดีต้อนรับสู่ Checkmate!"
+ },
+ "forgotPassword": {
+ "heading": "ลืมรหัสผ่าน?",
+ "subheadings": {
+ "stepOne": "ไม่ต้องกังวล เราจะส่งคำแนะนำเพื่อรีเซ็ตรหัสผ่านให้คุณ",
+ "stepTwo": "เราได้ส่งลิงก์รีเซ็ตรหัสผ่านไปยัง ",
+ "stepThree": "รหัสผ่านใหม่ต้องแตกต่างจากรหัสผ่านที่ใช้ก่อนหน้านี้",
+ "stepFour": "รีเซ็ตรหัสผ่านเรียบร้อยแล้ว คลิกด้านล่างเพื่อเข้าสู่ระบบทันที"
+ },
+ "buttons": {
+ "openEmail": "เปิดแอปอีเมล",
+ "resetPassword": "รีเซ็ตรหัสผ่าน"
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "กลับไปที่ เข้าสู่ระบบ",
+ "resend": "ไม่ได้รับอีเมล? คลิกเพื่อส่งอีกครั้ง"
+ },
+ "toasts": {
+ "sent": "คำแนะนำถูกส่งไปยัง .",
+ "emailNotFound": "ไม่พบอีเมลนี้",
+ "redirect": "กำลังเปลี่ยนหน้าใน ...",
+ "success": "รีเซ็ตรหัสผ่านสำเร็จแล้ว",
+ "error": "ไม่สามารถรีเซ็ตรหัสผ่านได้ กรุณาลองอีกครั้งภายหลังหรือติดต่อฝ่ายสนับสนุน"
+ }
+ }
+ },
+ "errorPages": {
+ "serverUnreachable": {
+ "toasts": {
+ "reconnected": "เชื่อมต่อกับเซิร์ฟเวอร์สำเร็จแล้ว",
+ "stillUnreachable": "เซิร์ฟเวอร์ยังไม่สามารถเข้าถึงได้ กรุณาลองอีกครั้งภายหลัง"
+ },
+ "alertBox": "เกิดข้อผิดพลาดการเชื่อมต่อเซิร์ฟเวอร์",
+ "description": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้ กรุณาตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณหรือยืนยันการตั้งค่า deployment หากปัญหายังคงอยู่",
+ "retryButton": {
+ "default": "ลองเชื่อมต่ออีกครั้ง",
+ "processing": "กำลังเชื่อมต่อ…"
+ }
+ }
+ },
+ "createNotifications": {
+ "title": "สร้างช่องทางแจ้งเตือน",
+ "nameSettings": {
+ "title": "ชื่อ",
+ "description": "ชื่อที่บ่งบอกถึงการตั้งค่าการรวมระบบของคุณ",
+ "nameLabel": "ชื่อ",
+ "namePlaceholder": "เช่น การแจ้งเตือนผ่าน Slack"
+ },
+ "typeSettings": {
+ "title": "ประเภท",
+ "description": "เลือกประเภทของช่องทางแจ้งเตือนที่คุณต้องการสร้าง",
+ "typeLabel": "ประเภท"
+ },
+ "emailSettings": {
+ "title": "อีเมล",
+ "description": "ที่อยู่อีเมลปลายทาง",
+ "emailLabel": "อีเมล",
+ "emailPlaceholder": "e.g. john@example.com"
+ },
+ "slackSettings": {
+ "title": "Slack",
+ "description": "ตั้งค่า Slack webhook ของคุณที่นี่",
+ "webhookLabel": "URL ของ Slack webhook",
+ "webhookPlaceholder": "https://hooks.slack.com/services/..."
+ },
+ "pagerdutySettings": {
+ "title": "PagerDuty",
+ "description": "ตั้งค่าการรวมระบบ PagerDuty ของคุณที่นี่",
+ "integrationKeyLabel": "คีย์การรวมระบบ",
+ "integrationKeyPlaceholder": "1234567890"
+ },
+ "discordSettings": {
+ "title": "Discord",
+ "description": "ตั้งค่า Discord webhook ของคุณที่นี่",
+ "webhookLabel": "URL ของ Discord Webhook",
+ "webhookPlaceholder": "https://your-server.com/webhook"
+ },
+ "webhookSettings": {
+ "title": "Webhook",
+ "description": "ตั้งค่า webhook ของคุณที่นี่",
+ "webhookLabel": "Webhook URL",
+ "webhookPlaceholder": "https://your-server.com/webhook"
+ },
+ "testNotification": "ทดสอบการแจ้งเตือน",
+ "dialogDeleteTitle": "คุณแน่ใจหรือไม่ว่าต้องการลบการแจ้งเตือนนี้?",
+ "dialogDeleteConfirm": "ลบ"
+ },
+ "notificationConfig": {
+ "title": "การแจ้งเตือน",
+ "description": "เลือกช่องทางแจ้งเตือนที่คุณต้องการใช้"
+ },
+ "monitorStatus": {
+ "checkingEvery": "ตรวจสอบทุก {{interval}}",
+ "withCaptureAgent": "โดยใช้ Capture agent {{version}}",
+ "up": "ออนไลน์",
+ "down": "ออฟไลน์",
+ "paused": "หยุดชั่วคราว"
+ },
+ "advancedMatching": "การจับคู่ขั้นสูง",
+ "sendTestNotifications": "ส่งการแจ้งเตือนทดสอบ",
+ "selectAll": "Select all",
+ "showAdminLoginLink": "แสดงลิงก์ \"ผู้ดูแลระบบ? เข้าสู่ระบบที่นี่\" บนหน้าสถานะ",
+ "logsPage": {
+ "title": "Logs",
+ "description": "ระบบ Logs 1000 บรรทัดล่าสุด",
+ "tabs": {
+ "queue": "คิวงาน",
+ "logs": "Logs เซิร์ฟเวอร์",
+ "diagnostics": "วินิจฉัย"
+ },
+ "toast": {
+ "fetchLogsSuccess": "ดึง Logs สำเร็จแล้ว"
+ },
+ "logLevelSelect": {
+ "title": "Logs level",
+ "values": {
+ "all": "All",
+ "info": "Info",
+ "warn": "Warn",
+ "error": "Error",
+ "debug": "Debug"
+ }
+ }
+ },
+ "queuePage": {
+ "title": "Queue",
+ "refreshButton": "Refresh",
+ "flushButton": "Flush queue",
+ "jobTable": {
+ "title": "งานที่อยู่ในคิวปัจจุบัน",
+ "idHeader": "รหัสมอนิเตอร์",
+ "urlHeader": "URL",
+ "typeHeader": "Type",
+ "activeHeader": "ใช้งานอยู่",
+ "lockedAtHeader": "ล็อกเมื่อ",
+ "runCountHeader": "จำนวนรัน",
+ "failCountHeader": "จำนวนล้มเหลว",
+ "lastRunHeader": "รันล่าสุดเมื่อ",
+ "lastFinishedAtHeader": "เสร็จสิ้นล่าสุดเมื่อ",
+ "lastRunTookHeader": "รันครั้งล่าสุดใช้เวลา",
+ "intervalHeader": "ช่วงเวลา"
+ },
+ "metricsTable": {
+ "title": "คิวเมตริก",
+ "metricHeader": "เมตริก",
+ "valueHeader": "Value"
+ },
+ "failedJobTable": {
+ "title": "งานที่ล้มเหลว",
+ "monitorIdHeader": "รหัสมอนิเตอร์",
+ "monitorUrlHeader": "URL มอนิเตอร์",
+ "failCountHeader": "จำนวนล้มเหลว",
+ "failedAtHeader": "ล้มเหลวล่าสุดเมื่อ",
+ "failReasonHeader": "สาเหตุที่ล้มเหลว"
+ }
+ },
+ "export": {
+ "title": "ส่งออกมอนิเตอร์",
+ "success": "ส่งออกมอนิเตอร์สำเร็จแล้ว",
+ "failed": "ไม่สามารถส่งออกมอนิเตอร์ได้"
+ },
+ "monitorActions": {
+ "title": "ส่งออก/นำเข้า",
+ "import": "นำเข้ามอนิเตอร์",
+ "export": "ส่งออกมอนิเตอร์",
+ "deleteSuccess": "ลบมอนิเตอร์สำเร็จ",
+ "deleteFailed": "ลบมอนิเตอร์ล้มเหลว",
+ "details": "รายละเอียด"
+ },
+ "settingsPage": {
+ "aboutSettings": {
+ "labelDevelopedBy": "พัฒนาโดย Bluewave Labs",
+ "labelVersion": "เวอร์ชัน",
+ "title": "เกี่ยวกับ"
+ },
+ "demoMonitorsSettings": {
+ "buttonAddMonitors": "เพิ่มมอนิเตอร์ตัวอย่าง",
+ "description": "เพิ่มมอนิเตอร์ตัวอย่างเพื่อการสาธิต",
+ "title": "มอนิเตอร์สาธิต"
+ },
+ "emailSettings": {
+ "buttonSendTestEmail": "ส่งอีเมลทดสอบ",
+ "description": "ตั้งค่าอีเมลสำหรับระบบของคุณ ใช้สำหรับส่งการแจ้งเตือนและเตือนภัย",
+ "descriptionTransport": "สร้าง SMTP transport สำหรับ NodeMailer",
+ "labelAddress": "ที่อยู่อีเมล - ใช้สำหรับการยืนยันตัวตน",
+ "labelConnectionHost": "โฮสต์เชื่อมต่ออีเมล - ชื่อโฮสต์ที่ใช้ในคำทักทาย HELO/EHLO",
+ "labelHost": "โฮสต์อีเมล - ชื่อโฮสต์หรือ IP สำหรับเชื่อมต่อ",
+ "labelIgnoreTLS": "ปิดใช้งาน STARTTLS: ไม่ใช้ TLS แม้เซิร์ฟเวอร์รองรับ",
+ "labelPassword": "รหัสผ่านอีเมล - สำหรับยืนยันตัวตน",
+ "labelPasswordSet": "ตั้งรหัสผ่านเรียบร้อยแล้ว คลิก รีเซ็ต เพื่อเปลี่ยน",
+ "labelPool": "เปิดใช้ connection pooling: ใช้การเชื่อมต่อเดิมซ้ำเพื่อเพิ่มประสิทธิภาพ",
+ "labelPort": "พอร์ตอีเมล - พอร์ตสำหรับเชื่อมต่อ",
+ "labelRejectUnauthorized": "ปฏิเสธใบรับรองไม่ถูกต้อง: ปฏิเสธการเชื่อมต่อที่ใช้ใบรับรอง self-signed หรือไม่เชื่อถือ",
+ "labelRequireTLS": "บังคับ STARTTLS: ต้องการอัปเกรด TLS ล้มเหลวหากไม่รองรับ",
+ "labelSecure": "ใช้ SSL (แนะนำ): เข้ารหัสการเชื่อมต่อด้วย SSL/TLS",
+ "labelTLSServername": "ชื่อเซิร์ฟเวอร์ TLS - ชื่อโฮสต์เสริมสำหรับตรวจสอบ TLS เมื่อโฮสต์เป็น IP",
+ "labelUser": "ผู้ใช้อีเมล - ชื่อผู้ใช้สำหรับยืนยันตัวตน ใช้แทนที่อีเมลถ้ามีการระบุ",
+ "linkTransport": "ดูข้อกำหนดที่นี่",
+ "placeholderUser": "ปล่อยว่างหากไม่จำเป็น",
+ "title": "อีเมล",
+ "toastEmailRequiredFieldsError": "ต้องระบุอีเมล, โฮสต์, พอร์ต และรหัสผ่าน"
+ },
+ "pageSpeedSettings": {
+ "description": "ใส่ Google PageSpeed API Key ของคุณเพื่อเปิดใช้งานการตรวจสอบ PageSpeed ของ Google คลิก รีเซ็ต เพื่ออัปเดต Key",
+ "labelApiKeySet": "ตั้งค่า API key แล้ว คลิก รีเซ็ต เพื่อเปลี่ยน",
+ "labelApiKey": "PageSpeed API key",
+ "title": "Google PageSpeed API key"
+ },
+ "saveButtonLabel": "บันทึก",
+ "statsSettings": {
+ "clearAllStatsButton": "ล้างสถิติทั้งหมด",
+ "clearAllStatsDescription": "ล้างสถิติทั้งหมด การกระทำนี้ไม่สามารถย้อนกลับได้",
+ "clearAllStatsDialogConfirm": "ใช่ ล้างสถิติทั้งหมด",
+ "clearAllStatsDialogDescription": "เมื่อถูกลบแล้ว ประวัติการตรวจสอบและสถิติไม่สามารถกู้คืนได้",
+ "clearAllStatsDialogTitle": "คุณต้องการล้างสถิติทั้งหมดหรือไม่?",
+ "description": "กำหนดระยะเวลาที่ต้องการเก็บข้อมูลประวัติ คุณยังสามารถล้างข้อมูลทั้งหมดที่มีอยู่ได้",
+ "labelTTL": "จำนวนวันที่ต้องการเก็บประวัติการตรวจสอบ",
+ "labelTTLOptional": "0 หมายถึงไม่จำกัด",
+ "title": "ประวัติการตรวจสอบ"
+ },
+ "systemResetSettings": {
+ "buttonRemoveAllMonitors": "ลบมอนิเตอร์ทั้งหมด",
+ "description": "ลบมอนิเตอร์ทั้งหมดออกจากระบบของคุณ",
+ "dialogConfirm": "ใช่ ลบมอนิเตอร์ทั้งหมด",
+ "dialogDescription": "เมื่อถูกลบแล้ว มอนิเตอร์ไม่สามารถกู้คืนได้",
+ "dialogTitle": "คุณต้องการลบมอนิเตอร์ทั้งหมดหรือไม่?",
+ "title": "รีเซ็ตระบบ"
+ },
+ "timezoneSettings": {
+ "description": "เลือกโซนเวลาที่ใช้แสดงวันที่และเวลาในแอปทั้งหมด",
+ "label": "โซนเวลาที่แสดง",
+ "title": "โซนเวลาที่แสดง"
+ },
+ "title": "การตั้งค่า",
+ "uiSettings": {
+ "description": "สลับโหมดสว่าง/มืด หรือเปลี่ยนภาษาอินเทอร์เฟซผู้ใช้",
+ "labelLanguage": "ภาษา",
+ "labelTheme": "โหมดธีม",
+ "title": "รูปลักษณ์"
+ },
+ "urlSettings": {
+ "description": "แสดง IP/URL ของมอนิเตอร์บนหน้าสถานะสาธารณะ หากปิดใช้งาน จะแสดงเฉพาะชื่อมอนิเตอร์เพื่อป้องกันข้อมูลสำคัญ",
+ "label": "แสดง IP/URL บนหน้าสถานะ",
+ "selectDisabled": "ปิดใช้งาน",
+ "selectEnabled": "เปิดใช้งาน",
+ "title": "IP/URL ของมอนิเตอร์บนหน้าสถานะ"
+ },
+ "globalThresholds": {
+ "title": "เกณฑ์การตรวจสอบแบบรวม",
+ "description": "กำหนดค่าเกณฑ์ของ CPU, หน่วยความจำ, ดิสก์ และอุณหภูมิ หากระบุค่าไว้ ระบบจะเปิดใช้งานการตรวจสอบโดยอัตโนมัติ"
+ }
+ },
+ "statusPageCreate": {
+ "buttonSave": "บันทึก"
+ },
+ "incidentsOptionsHeaderFilterResolved": "แก้ไขแล้ว",
+ "settingsSave": "บันทึก",
+ "statusPageCreateAppearanceTitle": "รูปลักษณ์",
+ "confirmPassword": "ยืนยันรหัสผ่าน",
+ "monitorHooks": {
+ "failureAddDemoMonitors": "ไม่สามารถเพิ่มมอนิเตอร์ตัวอย่างได้",
+ "successAddDemoMonitors": "เพิ่มมอนิเตอร์ตัวอย่างสำเร็จ"
+ },
+ "settingsAppearance": "รูปลักษณ์",
+ "settingsDisplayTimezone": "โซนเวลาที่แสดง",
+ "settingsGeneralSettings": "การตั้งค่าทั่วไป",
+ "incidentsOptionsHeaderTotalIncidents": "จำนวนเหตุการณ์ทั้งหมด",
+ "statusPage": {
+ "deleteSuccess": "ลบหน้าสถานะสำเร็จ",
+ "deleteFailed": "ลบหน้าสถานะล้มเหลว",
+ "createSuccess": "สร้างหน้าสถานะสำเร็จ",
+ "updateSuccess": "อัปเดตหน้าสถานะสำเร็จ",
+ "generalSettings": "การตั้งค่าทั่วไป",
+ "contents": "เนื้อหา",
+ "fallback": {
+ "checks": [
+ "ตรวจสอบและแสดงสุขภาพของบริการแบบเรียลไทม์",
+ "ติดตามหลายบริการและแชร์สถานะ",
+ "แจ้งผู้ใช้ให้ทราบเกี่ยวกับการหยุดทำงานและประสิทธิภาพ"
+ ],
+ "title": "หน้าสถานะใช้สำหรับ:",
+ "actionButton": "มาสร้างหน้าสถานะแรกกันเถอะ!"
+ }
+ },
+ "testNotificationsDisabled": "ยังไม่มีการตั้งค่าการแจ้งเตือนสำหรับมอนิเตอร์นี้ คุณต้องเพิ่มโดยคลิกปุ่ม 'ตั้งค่า'",
+ "incidentsTableResolvedAt": "แก้ไขเมื่อ",
+ "incidentsTableActionResolve": "แก้ไข",
+ "checkHooks": {
+ "failureResolveOne": "แก้ไขเหตุการณ์ล้มเหลว",
+ "failureResolveAll": "แก้ไขเหตุการณ์ทั้งหมดล้มเหลว",
+ "failureResolveMonitor": "ไม่สามารถแก้ไขเหตุการณ์ของตัวตรวจสอบได้"
+ },
+ "checkFormError": "กรุณาตรวจสอบแบบฟอร์มสำหรับข้อผิดพลาด",
+ "diagnosticsPage": {
+ "diagnosticDescription": "วินิจฉัยระบบ",
+ "statsDescription": "สถิติระบบ",
+ "gauges": {
+ "heapAllocationTitle": "การจัดสรร Heap",
+ "heapAllocationSubtitle": "% ของหน่วยความจำที่ใช้ได้",
+ "heapUsageTitle": "การใช้งาน Heap",
+ "heapUsageSubtitle": "% ของหน่วยความจำที่ใช้ได้",
+ "heapUtilizationTitle": "การใช้งาน Heap",
+ "heapUtilizationSubtitle": "% ของที่จัดสรรแล้ว",
+ "instantCpuUsageTitle": "การใช้งาน CPU ทันที",
+ "instantCpuUsageSubtitle": "% ของเวลา 1 วินาทีที่ CPU ใช้"
+ },
+ "stats": {
+ "eventLoopDelayTitle": "ความล่าช้าวงจร Event Loop",
+ "uptimeTitle": "เวลาทำงาน",
+ "usedHeapSizeTitle": "ขนาดที่ใช้ Heap",
+ "totalHeapSizeTitle": "รวมขนาด Heap",
+ "osMemoryLimitTitle": "ขีดจำกัดหน่วยความจำ OS"
+ }
+ },
+ "pageSpeedLighthouseAPI": "ใช้ Lighthouse PageSpeed API เพื่อตรวจสอบเว็บไซต์ของคุณ",
+ "time": {
+ "threeMinutes": "3 นาที",
+ "fiveMinutes": "5 นาที",
+ "tenMinutes": "10 นาที",
+ "twentyMinutes": "20 นาที",
+ "oneHour": "1 ชั่วโมง",
+ "oneDay": "1 วัน",
+ "oneWeek": "1 สัปดาห์",
+ "fourMinutes": "4 นาที",
+ "oneMinute": "1 นาที",
+ "twoMinutes": "2 นาที",
+ "fifteenSeconds": "15 วินาที",
+ "thirtySeconds": "30 วินาที"
+ },
+ "general": {
+ "noOptionsFound": "ไม่พบ {{unit}}"
+ },
+ "infrastructureMonitor": {
+ "fallback": {
+ "checks": [
+ "ติดตามประสิทธิภาพของเซิร์ฟเวอร์ของคุณ",
+ "ระบุจุดคอขวดและเพิ่มประสิทธิภาพการใช้งาน",
+ "มั่นใจในความเสถียรด้วยการตรวจสอบแบบเรียลไทม์"
+ ],
+ "title": "ตัวตรวจสอบโครงสร้างพื้นฐานใช้สำหรับ:",
+ "actionButton": "มาสร้างตัวตรวจสอบโครงสร้างพื้นฐานตัวแรกกันเถอะ!"
+ }
+ },
+ "maintenanceWindow": {
+ "fallback": {
+ "checks": [
+ "กำหนดช่วงเวลาบำรุงรักษา",
+ "ลดความเข้าใจผิด",
+ "หยุดส่งการแจ้งเตือนในช่วงเวลาบำรุงรักษา"
+ ],
+ "title": "ช่วงเวลาบำรุงรักษาใช้สำหรับ:",
+ "actionButton": "มาสร้างช่วงเวลาบำรุงรักษาแรกกันเถอะ!"
+ }
+ },
+ "pageSpeed": {
+ "fallback": {
+ "checks": [
+ "รายงานเกี่ยวกับประสบการณ์ผู้ใช้ของหน้าเว็บ",
+ "ช่วยวิเคราะห์ความเร็วของหน้าเว็บ",
+ "ให้คำแนะนำในการปรับปรุงหน้าเว็บ"
+ ],
+ "title": "ตัวตรวจสอบ PageSpeed ใช้สำหรับ:",
+ "actionButton": "มาสร้างตัวตรวจสอบ PageSpeed ตัวแรกกันเถอะ!"
+ }
+ },
+ "uptimeMonitor": {
+ "fallback": {
+ "checks": [
+ "ตรวจสอบว่าเว็บไซต์หรือเซิร์ฟเวอร์ออนไลน์และตอบสนองหรือไม่",
+ "แจ้งทีมเมื่อเกิดปัญหาการหยุดทำงานหรือประสิทธิภาพ",
+ "ตรวจสอบ HTTP endpoints, ping, คอนเทนเนอร์ และพอร์ต",
+ "ติดตามสถิติ uptime และความเสถียรย้อนหลัง"
+ ],
+ "title": "ตัวตรวจสอบ Uptime ใช้สำหรับ:",
+ "actionButton": "มาสร้างตัวตรวจสอบ Uptime ตัวแรกกันเถอะ!"
+ }
+ },
+ "editUserPage": {
+ "form": {
+ "email": "อีเมล",
+ "firstName": "ชื่อจริง",
+ "lastName": "นามสกุล",
+ "role": "บทบาท",
+ "save": "บันทึก"
+ },
+ "table": {
+ "actionHeader": "การดำเนินการ",
+ "roleHeader": "บทบาท"
+ },
+ "title": "แก้ไขผู้ใช้",
+ "toast": {
+ "successUserUpdate": "อัปเดตผู้ใช้เรียบร้อยแล้ว",
+ "validationErrors": "ข้อผิดพลาดในการตรวจสอบข้อมูล"
+ }
+ },
+ "incidentsPageActionResolveMonitor": "แก้ไขเหตุการณ์ของตัวตรวจสอบ",
+ "incidentsPageActionResolveAll": "แก้ไขเหตุการณ์ทั้งหมด",
+ "matchMethodOptions": {
+ "equal": "Equal",
+ "equalPlaceholder": "success",
+ "include": "Include",
+ "includePlaceholder": "ok",
+ "regex": "Regex",
+ "regexPlaceholder": "^(success|ok)$",
+ "text": "วิธีการจับคู่"
+ },
+ "monitorType": {
+ "docker": {
+ "label": "ชื่อ/ID คอนเทนเนอร์",
+ "namePlaceholder": "คอนเทนเนอร์ของฉัน",
+ "placeholder": "my-app or abcd1234"
+ },
+ "http": {
+ "label": "URL ที่ต้องการตรวจสอบ",
+ "namePlaceholder": "Google",
+ "placeholder": "google.com"
+ },
+ "ping": {
+ "label": "ที่อยู่ IP ที่ต้องการตรวจสอบ",
+ "namePlaceholder": "Google",
+ "placeholder": "1.1.1.1"
+ },
+ "port": {
+ "label": "ที่อยู่ IP ที่ต้องการตรวจสอบ",
+ "namePlaceholder": "Localhost:5173",
+ "placeholder": "localhost"
+ },
+ "game": {
+ "label": "URL ที่ต้องการมอนิเตอร์",
+ "namePlaceholder": "localhost:5173",
+ "placeholder": "localhost"
+ }
+ },
+ "uptimeAdvancedMatching": {
+ "jsonPath": "JSON Path"
+ },
+ "bytesPerSecond": "ไบต์ต่อวินาที",
+ "bytesReceived": "ไบต์ที่รับ",
+ "bytesSent": "ไบต์ที่ส่ง",
+ "chooseGame": "เลือกเกม",
+ "createMonitorPage": {
+ "incidentConfigDescription": "การใช้หน้าต่างเลื่อนจะช่วยกำหนดเวลาที่ตัวตรวจสอบล้มเหลว สถานะของตัวตรวจสอบจะเปลี่ยนก็ต่อเมื่อเปอร์เซ็นต์ของการตรวจสอบในหน้าต่างเลื่อนตรงตามค่าที่กำหนด",
+ "incidentConfigStatusWindowLabel": "ต้องมีการตรวจสอบกี่ครั้งในหน้าต่างเลื่อน?",
+ "incidentConfigStatusWindowThresholdLabel": "ต้องการให้กี่เปอร์เซ็นต์ของการตรวจสอบในหน้าต่างเลื่อนล้มเหลว/สำเร็จ ก่อนที่จะเปลี่ยนสถานะของตัวตรวจสอบ?",
+ "incidentConfigTitle": "เหตุการณ์"
+ },
+ "dataRate": "อัตราการส่งข้อมูล",
+ "dataReceived": "ข้อมูลที่รับ",
+ "dataSent": "ข้อมูลที่ส่ง",
+ "details": "รายละเอียด",
+ "drops": "แพ็กเก็ตที่หล่น",
+ "errors": "ข้อผิดพลาด",
+ "errorsIn": "ข้อผิดพลาดขาเข้า",
+ "errorsOut": "ข้อผิดพลาดขาออก",
+ "gameServerMonitoring": "มอนิเตอร์เซิร์ฟเวอร์เกม",
+ "gameServerMonitoringDescription": "ตรวจสอบว่าเซิร์ฟเวอร์เกมของคุณกำลังทำงานอยู่หรือไม่",
+ "network": "เน็ตเวอร์",
+ "networkDrops": "การดรอปของเน็ตเวอร์",
+ "networkErrors": "เน็ตเวอร์ ล้มเหลว",
+ "networkInterface": "เน็ตเวอร์ อินเทอร์เฟซ",
+ "noNetworkStatsAvailable": "ไม่มีสถิติของเน็ตเวอร์",
+ "packetsPerSecond": "แพ็กเก็ตต่อวินาที",
+ "packetsReceived": "แพ็กเก็ตที่รับ",
+ "packetsReceivedRate": "อัตราการรับแพ็กเก็ต",
+ "packetsSent": "แพ็กเก็ตที่ส่ง",
+ "rate": "อัตรา",
+ "selectInterface": "เลือกอินเทอร์เฟซ"
+}
diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json
index 8271da521..0ab45ebe9 100644
--- a/client/src/locales/tr.json
+++ b/client/src/locales/tr.json
@@ -376,7 +376,52 @@
"email": "Eposta",
"role": "Rol",
"created": "Oluşturulma"
- }
+ },
+ "addTeamMember": {
+ "addMemberMenu": "",
+ "title": "",
+ "description": "",
+ "addButton": ""
+ },
+ "register": "",
+ "registerToast": {
+ "success": "",
+ "dbUserExists": "",
+ "unknownError": ""
+ },
+ "registerTeamMember": {
+ "title": "",
+ "auth": {
+ "common": {
+ "inputs": {
+ "firstName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "email": {
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "role": {
+ "errors": {
+ "empty": ""
+ }
+ }
+ }
+ }
+ }
+ },
+ "role": ""
},
"monitorState": {
"paused": "Durduruldu",
@@ -742,7 +787,8 @@
"failCountHeader": "",
"lastRunHeader": "",
"lastFinishedAtHeader": "Son tamamlama",
- "lastRunTookHeader": ""
+ "lastRunTookHeader": "",
+ "intervalHeader": ""
},
"metricsTable": {
"title": "",
diff --git a/client/src/locales/zh-CN.json b/client/src/locales/zh-CN.json
new file mode 100644
index 000000000..c2d1aa6fc
--- /dev/null
+++ b/client/src/locales/zh-CN.json
@@ -0,0 +1,1114 @@
+{
+ "submit": "提交",
+ "title": "标题",
+ "distributedStatusHeaderText": "实时、真实设备覆盖",
+ "distributedStatusSubHeaderText": "在全球数百万设备的支持下,按全球区域、国家或城市查看系统性能",
+ "settingsDisabled": "禁用",
+ "settingsSuccessSaved": "设置保存成功",
+ "settingsFailedToSave": "保存设置失败",
+ "settingsStatsCleared": "统计数据已成功清除",
+ "settingsFailedToClearStats": "清除统计数据失败",
+ "settingsMonitorsDeleted": "已成功删除所有监视器",
+ "settingsFailedToDeleteMonitors": "删除所有监视器失败",
+ "starPromptTitle": "打星Checkmate",
+ "starPromptDescription": "在 GitHub 上查看最新版本,并帮助社区发展壮大",
+ "https": "HTTPS",
+ "http": "HTTP",
+ "monitor": "监控",
+ "aboutus": "关于我们",
+ "now": "现在",
+ "delete": "删除",
+ "configure": "配置",
+ "responseTime": "响应时间",
+ "ms": "毫秒",
+ "bar": "条",
+ "area": "区域",
+ "country": "国家",
+ "city": "城市",
+ "response": "响应",
+ "monitorStatusUp": "监控目标 {name} ({url}) 现在处于正常状态并正在响应",
+ "monitorStatusDown": "监控目标 {name} ({url}) 已下线且无响应",
+ "webhookSendSuccess": "Webhook通知发送成功",
+ "webhookSendError": "向 {platform} 发送 Webhook 通知时出错",
+ "webhookUnsupportedPlatform": "不支持的平台:{platform}",
+ "distributedRightCategoryTitle": "监控",
+ "distributedStatusServerMonitors": "服务器监控",
+ "distributedStatusServerMonitorsDescription": "监控相关服务器的状态",
+ "distributedUptimeCreateSelectURL": "在这里,您可以选择主机的URL以及监控类型。",
+ "distributedUptimeCreateChecks": "分布式正常运行时间创建检查",
+ "distributedUptimeCreateChecksDescription": "在添加站点之后,您随时可以增加或删除检查项。",
+ "distributedUptimeCreateIncidentNotification": "事件通知",
+ "distributedUptimeCreateIncidentDescription": "当发生事件时,通知用户。",
+ "distributedUptimeCreateAdvancedSettings": "高级设置",
+ "distributedUptimeDetailsNoMonitorHistory": "此监控器尚未有检查历史记录。",
+ "distributedUptimeDetailsStatusHeaderUptime": "运行时间:",
+ "distributedUptimeDetailsStatusHeaderLastUpdate": "最后更新于",
+ "notifications": {
+ "enableNotifications": "启用 {{platform}} 通知",
+ "testNotification": "测试通知",
+ "addOrEditNotifications": "添加或修改通知方式",
+ "slack": {
+ "label": "Slack",
+ "description": "要启用 Slack 通知,请先创建一个 Slack 应用并启用传入的 Webhook。之后,只需在此处提供 Webhook URL 即可。",
+ "webhookLabel": "Webhook 网址",
+ "webhookPlaceholder": "https://hooks.slack.com/services/...",
+ "webhookRequired": "需要 Slack Webhook URL"
+ },
+ "discord": {
+ "label": "Discord",
+ "description": "要通过 Discord 通知将数据从 Checkmate 发送到 Discord 频道,可以使用 Discord 的入站 WebHooks 功能。",
+ "webhookLabel": "Discord 接收网址",
+ "webhookPlaceholder": "https://discord.com/api/webhooks/...",
+ "webhookRequired": "需要 Discord Webhook URL"
+ },
+ "telegram": {
+ "label": "Telegram",
+ "description": "要启用 Telegram 通知,请使用 BotFather创建一个 Telegram 机器人。然后,获取 API 令牌和聊天 ID 并在此处填写。",
+ "tokenLabel": "你的机器人token",
+ "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
+ "chatIdLabel": "你的Chat ID",
+ "chatIdPlaceholder": "-1001234567890",
+ "fieldsRequired": "需要 Telegram 令牌和聊天 ID"
+ },
+ "webhook": {
+ "label": "Webhooks",
+ "description": "你可以设置自定义的 Webhook,以便在事件发生时接收通知。",
+ "urlLabel": "Webhook URL",
+ "urlPlaceholder": "https://your-server.com/webhook",
+ "urlRequired": "Webhook URL 是必填项"
+ },
+ "testNotificationDevelop": "测试通知 2",
+ "integrationButton": "通知集成",
+ "testSuccess": "测试通知发送成功!",
+ "testFailed": "测试通知发送失败",
+ "unsupportedType": "不支持的通知类型",
+ "networkError": "发生网络错误",
+ "fallback": {
+ "title": "通知通道用于:",
+ "checks": [
+ "提醒团队有关停机时间或性能问题",
+ "让工程师知道事件发生时",
+ "让管理员了解系统更改"
+ ],
+ "actionButton": "让我们创建您的第一个通知渠道!"
+ },
+ "createButton": "创建通知渠道",
+ "createTitle": "通知渠道",
+ "create": {
+ "success": "通知创建成功",
+ "failed": "通知创建失败"
+ },
+ "fetch": {
+ "success": "成功获取通知",
+ "failed": "无法获取通知"
+ },
+ "delete": {
+ "success": "通知已成功删除",
+ "failed": "删除通知失败"
+ },
+ "edit": {
+ "success": "通知已成功更新",
+ "failed": "无法更新通知"
+ },
+ "test": {
+ "success": "测试通知发送成功",
+ "failed": "测试通知发送失败"
+ }
+ },
+ "testLocale": "测试区域设置",
+ "add": "添加",
+ "monitors": "监视器",
+ "distributedUptimeStatusCreateStatusPage": "状态页",
+ "distributedUptimeStatusCreateStatusPageAccess": "访问",
+ "distributedUptimeStatusCreateStatusPageReady": "如果状态页面准备就绪,您可以标记为已发布。",
+ "distributedUptimeStatusBasicInfoHeader": "基础信息",
+ "distributedUptimeStatusBasicInfoDescription": "定义公司名称和状态页指向的子域名。",
+ "distributedUptimeStatusLogoHeader": "Logo",
+ "distributedUptimeStatusLogoDescription": "为您的状态页面上传一个 Logo",
+ "distributedUptimeStatusLogoUploadButton": "上传 Logo",
+ "distributedUptimeStatusStandardMonitorsHeader": "标准监视器",
+ "distributedUptimeStatusStandardMonitorsDescription": "将标准监视器附加到您的状态页面。",
+ "distributedUptimeStatusCreateYour": "创建您的",
+ "distributedUptimeStatusEditYour": "编辑您的",
+ "distributedUptimeStatusPublishedLabel": "发布并公开可见",
+ "distributedUptimeStatusCompanyNameLabel": "公司名称",
+ "distributedUptimeStatusPageAddressLabel": "您的状态页面地址",
+ "distributedUptimeStatus30Days": "30 天",
+ "distributedUptimeStatus60Days": "60 天",
+ "distributedUptimeStatus90Days": "90 天",
+ "distributedUptimeStatusPageNotSetUp": "未设置状态页面。",
+ "distributedUptimeStatusContactAdmin": "请联系您的管理员",
+ "distributedUptimeStatusPageNotPublic": "此状态页面不公开。",
+ "distributedUptimeStatusPageDeleteDialog": "你想删除这个状态页吗?",
+ "distributedUptimeStatusPageDeleteConfirm": "确定,删除状态页",
+ "distributedUptimeStatusPageDeleteDescription": "删除后将无法恢复您的状态页面。",
+ "distributedUptimeStatusDevices": "设备",
+ "distributedUptimeStatusUpt": "UPT",
+ "distributedUptimeStatusUptBurned": "分布式正常运行时间状态运行已完成",
+ "distributedUptimeStatusUptLogo": "分布式UptimeStatusUptLogo",
+ "incidentsTableNoIncidents": "无事件记录",
+ "incidentsTablePaginationLabel": "事件",
+ "incidentsTableMonitorName": "监控名称",
+ "incidentsTableStatus": "状态",
+ "incidentsTableDateTime": "日期&时间",
+ "incidentsTableStatusCode": "状态码",
+ "incidentsTableMessage": "信息",
+ "incidentsOptionsHeader": "事件:",
+ "incidentsOptionsHeaderFilterBy": "筛选:",
+ "incidentsOptionsHeaderFilterAll": "全部",
+ "incidentsOptionsHeaderFilterDown": "离线",
+ "incidentsOptionsHeaderFilterCannotResolve": "无法解析",
+ "incidentsOptionsHeaderShow": "显示:",
+ "incidentsOptionsHeaderLastHour": "过去一小时",
+ "incidentsOptionsHeaderLastDay": "过去一天",
+ "incidentsOptionsHeaderLastWeek": "过去一周",
+ "incidentsOptionsPlaceholderAllServers": "所有服务器",
+ "infrastructureCreateYour": "创建你的",
+ "infrastructureCreateGeneralSettingsDescription": "在这里您可以选择主机的 URL,以及连接到服务器代理时使用的友好名称和授权密钥。",
+ "infrastructureServerRequirement": "你正在监控的服务器必须运行在",
+ "infrastructureCustomizeAlerts": "自定义告警",
+ "infrastructureAlertNotificationDescription": "当阈值超过指定百分比时向用户(们)发送通知。",
+ "infrastructureCreateMonitor": "创建基础设施监控",
+ "infrastructureProtocol": "协议",
+ "infrastructureServerUrlLabel": "服务器 URL",
+ "infrastructureDisplayNameLabel": "显示名称",
+ "infrastructureAuthorizationSecretLabel": "授权密钥",
+ "gb": "GB",
+ "mb": "MB",
+ "mem": "内存",
+ "memoryUsage": "内存使用率",
+ "cpu": "CPU",
+ "cpuUsage": "CPU 使用率",
+ "cpuTemperature": "CPU 温度",
+ "diskUsage": "硬盘使用率",
+ "used": "已使用",
+ "total": "总共",
+ "cores": "核心",
+ "frequency": "频率",
+ "status": "状态",
+ "cpuPhysical": "CPU(物理核心)",
+ "cpuLogical": "CPU(逻辑核心)",
+ "cpuFrequency": "CPU 频率",
+ "avgCpuTemperature": "CPU 平均温度",
+ "memory": "内存",
+ "disk": "硬盘",
+ "uptime": "运行时间",
+ "os": "系统",
+ "host": "主机",
+ "actions": "动作",
+ "integrations": "继承",
+ "integrationsPrism": "将 Prism 连接到您喜欢的服务。",
+ "integrationsSlack": "集成Slack",
+ "integrationsSlackInfo": "集成SlackInfo",
+ "integrationsDiscord": "集成Discord",
+ "integrationsDiscordInfo": "集成Discord信息",
+ "integrationsZapier": "集成Zapier",
+ "integrationsZapierInfo": "集成ZapierInfo",
+ "commonSave": "保存",
+ "createYour": "创建你的",
+ "createMonitor": "创建监控",
+ "pause": "暂停",
+ "resume": "恢复",
+ "editing": "编辑中...",
+ "url": "网址",
+ "access": "访问",
+ "timezone": "时区",
+ "features": "特征",
+ "administrator": "管理员?",
+ "loginHere": "点击此处登录",
+ "displayName": "显示名称",
+ "urlMonitor": "监控的 URL",
+ "portToMonitor": "监控端口",
+ "websiteMonitoring": "网站监控",
+ "websiteMonitoringDescription": "使用 HTTP(s)监控您的网站或 API 端点。",
+ "pingMonitoring": "ping监控",
+ "pingMonitoringDescription": "检查您的服务器是否可用。",
+ "dockerContainerMonitoring": "Docker容器监控",
+ "dockerContainerMonitoringDescription": "检查你的 Docker 容器是否正在运行。",
+ "portMonitoring": "端口监控",
+ "portMonitoringDescription": "检查你的端口是否已打开。",
+ "createMaintenanceWindow": "创建维护窗口",
+ "createMaintenance": "创建维护",
+ "editMaintenance": "编辑维护",
+ "maintenanceWindowName": "维护窗口名称",
+ "friendlyNameInput": "友好名称",
+ "friendlyNamePlaceholder": "维护时间于 __:__ ,持续 ___ 分钟",
+ "maintenanceRepeat": "维护重复",
+ "maintenance": "维护",
+ "duration": "期间",
+ "addMonitors": "添加监视器",
+ "window": "窗口",
+ "cancel": "取消",
+ "message": "信息",
+ "low": "低的",
+ "high": "高的",
+ "statusCode": "状态码",
+ "date&Time": "日期&时间",
+ "type": "类型",
+ "statusPageName": "状态页名称",
+ "publicURL": "公共 URL",
+ "repeat": "重复",
+ "edit": "编辑",
+ "createA": "创建A",
+ "remove": "移除",
+ "maintenanceWindowDescription": "在维护时段内,所选监视器的所有监视都将暂停。不会执行任何网络检查,从而阻止触发任何状态更新或通知。您的显示器将冻结在其最后的已知状态,并且状态页面将显示维护指示器。维护时段结束后,监控会自动恢复,如果检测到问题,将触发警报。维护期不计入正常运行时间计算。",
+ "startTime": "开始时间",
+ "timeZoneInfo": "所有日期和时间均为 GMT+0 时区。",
+ "monitorsToApply": "监控器应用",
+ "nextWindow": "下一个窗口",
+ "notFoundButton": "未找到按钮",
+ "pageSpeedConfigureSettingsDescription": "您可以在此处选择主机的 URL 以及监视器类型。",
+ "monitorDisplayName": "监视器显示名称",
+ "whenNewIncident": "当新事件发生时",
+ "notifySMS": "通过短信通知(即将推出)",
+ "notifyEmails": "还通过电子邮件通知多个地址(即将推出)",
+ "seperateEmails": "单独的电子邮件",
+ "checkFrequency": "检查频率",
+ "matchMethod": "match方法",
+ "expectedValue": "预期值",
+ "deleteDialogTitle": "确定要删除此监视器吗?",
+ "deleteDialogDescription": "一旦删除,将无法检索此监视器。",
+ "pageSpeedMonitor": "页面速度监控器",
+ "shown": "显示",
+ "ago": "前",
+ "companyName": "公司名称",
+ "pageSpeedDetailsPerformanceReport": "值是估计值,可能会有所不同。",
+ "pageSpeedDetailsPerformanceReportCalculator": "页面速度详情性能报告计算器",
+ "checkingEvery": "检查每一个",
+ "statusPageCreateSettings": "如果您的状态页面已准备就绪,您可以将其标记为已发布。",
+ "basicInformation": "基础信息",
+ "statusPageCreateBasicInfoDescription": "定义公司名称和状态页面指向的子域。",
+ "statusPageCreateSelectTimeZoneDescription": "选择状态页面的显示时区。",
+ "statusPageCreateAppearanceDescription": "定义公共状态页面的默认外观。",
+ "statusPageCreateSettingsCheckboxLabel": "已发布并向公众可见",
+ "statusPageCreateBasicInfoStatusPageAddress": "您的状态页面地址",
+ "statusPageCreateTabsContent": "状态页服务器",
+ "statusPageCreateTabsContentDescription": "您可以将监视的任意数量的服务器添加到状态页面。您还可以重新排序它们以获得最佳观看体验。",
+ "statusPageCreateTabsContentFeaturesDescription": "在状态页上显示更多详细信息",
+ "showCharts": "显示图表",
+ "showUptimePercentage": "显示正常运行时间百分比",
+ "removeLogo": "移除Logo",
+ "statusPageStatus": "未设置公开状态页面。",
+ "statusPageStatusContactAdmin": "请联系您的管理员",
+ "statusPageStatusNotPublic": "此状态页面不公开。",
+ "statusPageStatusNoPage": "这里没有状态页面。",
+ "statusPageStatusServiceStatus": "服务状态",
+ "deleteStatusPage": "要删除此状态页面吗?",
+ "deleteStatusPageConfirm": "是,删除状态页",
+ "deleteStatusPageDescription": "一旦删除,您的状态页面将无法检索。",
+ "uptimeCreate": "期望值用于与响应结果进行匹配,匹配确定状态。",
+ "uptimeCreateJsonPath": "将根据响应 JSON 数据评估此表达式,并将结果用于与预期值进行匹配。看",
+ "uptimeCreateJsonPathQuery": "查询语言文档。",
+ "maintenanceTableActionMenuDialogTitle": "您真的想删除此维护窗口吗?",
+ "infrastructureEditYour": "编辑您的",
+ "infrastructureEditMonitor": "保存基础架构监视器",
+ "infrastructureMonitorCreated": "基础设施监控创建成功!",
+ "infrastructureMonitorUpdated": "基础设施监控更新成功!",
+ "errorInvalidTypeId": "提供的通知类型无效",
+ "errorInvalidFieldId": "提供的字段 ID 无效",
+ "inviteNoTokenFound": "未找到邀请令牌",
+ "pageSpeedWarning": "警告:您尚未添加 Google PageSpeed API 密钥。没有它,PageSpeed 监视器将无法运行。",
+ "pageSpeedLearnMoreLink": "点击这里",
+ "pageSpeedAddApiKey": "以添加您的 API 密钥。",
+ "update": "更新",
+ "invalidFileFormat": "不支持的文件格式!",
+ "invalidFileSize": "文件太大了!",
+ "ClickUpload": "点击上传",
+ "DragandDrop": "拖放",
+ "MaxSize": "最大尺寸",
+ "SupportedFormats": "支持的格式",
+ "FirstName": "名",
+ "LastName": "姓",
+ "EmailDescriptionText": "这是您当前的电子邮件地址 - 无法更改。",
+ "YourPhoto": "个人资料照片",
+ "PhotoDescriptionText": "这张照片将显示在您的个人资料页面中。",
+ "save": "保存",
+ "DeleteDescriptionText": "这将从服务器中删除帐户和所有关联数据。这是不可逆的。",
+ "DeleteAccountWarning": "删除您的帐户意味着您将无法再次登录,并且您的所有数据都将被删除。这是不可逆的。",
+ "DeleteWarningTitle": "真的删除这个帐户?",
+ "bulkImport": {
+ "title": "批量导入",
+ "selectFileTips": "选择要上传的 CSV 文件",
+ "selectFileDescription": "您可以下载我们的模板或示例",
+ "selectFile": "选择文件",
+ "parsingFailed": "解析失败",
+ "uploadSuccess": "监视器创建成功!",
+ "validationFailed": "验证失败",
+ "noFileSelected": "未选择文件",
+ "fallbackPage": "导入文件以批量上传服务器列表",
+ "invalidFileType": "文件类型无效",
+ "uploadFailed": "上传失败"
+ },
+ "DeleteAccountTitle": "删除账户",
+ "DeleteAccountButton": "删除帐户",
+ "publicLink": "公共链接",
+ "maskedPageSpeedKeyPlaceholder": "*************************************",
+ "reset": "重置",
+ "ignoreTLSError": "忽略 TLS/SSL 错误",
+ "tlsErrorIgnored": "已忽略 TLS/SSL 错误",
+ "ignoreTLSErrorDescription": "忽略 TLS/SSL 错误并继续检查网站可用性",
+ "createNew": "新建",
+ "greeting": {
+ "prepend": "你好",
+ "append": "下午时间,尽情发挥!",
+ "overview": "以下是您的 {{type}} 监视器的概述。"
+ },
+ "roles": {
+ "superAdmin": "超级管理员",
+ "admin": "管理",
+ "teamMember": "团队成员",
+ "demoUser": "演示用户"
+ },
+ "teamPanel": {
+ "teamMembers": "团队成员",
+ "filter": {
+ "all": "全部",
+ "member": "成员"
+ },
+ "inviteTeamMember": "邀请新团队成员",
+ "inviteNewTeamMember": "邀请新团队成员",
+ "inviteDescription": "当您添加新的团队成员时,他们将可以访问所有监视器。",
+ "email": "电子邮件",
+ "selectRole": "选择角色",
+ "inviteLink": "邀请链接",
+ "cancel": "取消",
+ "noMembers": "没有具有此角色的团队成员",
+ "getToken": "获取Token",
+ "emailToken": "电子邮件令牌",
+ "table": {
+ "name": "名字",
+ "email": "邮箱",
+ "role": "角色",
+ "created": "创建"
+ },
+ "addTeamMember": {
+ "addMemberMenu": "添加团队成员",
+ "title": "注册新团队成员",
+ "description": "创建一个新用户并与他们共享凭据。此方法使成员能够立即访问所有监视器。",
+ "addButton": "添加成员"
+ },
+ "register": "注册团队成员",
+ "registerToast": {
+ "success": "用户创建,安全地与成员共享凭据。",
+ "dbUserExists": "用户已存在。",
+ "unknownError": "发生未知错误。"
+ },
+ "registerTeamMember": {
+ "title": "注册团队成员",
+ "auth": {
+ "common": {
+ "inputs": {
+ "firstName": {
+ "errors": {
+ "empty": "请输入姓名",
+ "pattern": "名称只能包含字母、空格、撇号或连字符"
+ }
+ },
+ "lastName": {
+ "errors": {
+ "empty": "请输入姓氏",
+ "pattern": "姓氏只能包含字母、空格、撇号或连字符"
+ }
+ },
+ "email": {
+ "errors": {
+ "empty": "要继续,请输入电子邮件地址",
+ "invalid": "请重新检查输入的电子邮件地址的有效性"
+ }
+ },
+ "role": {
+ "errors": {
+ "empty": "角色是必需的"
+ }
+ }
+ }
+ }
+ }
+ },
+ "role": "角色"
+ },
+ "monitorState": {
+ "paused": "暂停",
+ "resumed": "恢复",
+ "active": "已激活"
+ },
+ "menu": {
+ "uptime": "上线时间",
+ "pagespeed": "页面速度",
+ "infrastructure": "基础设施",
+ "incidents": "事件",
+ "statusPages": "状态页",
+ "maintenance": "维护",
+ "integrations": "集成",
+ "settings": "设置",
+ "support": "支持",
+ "discussions": "讨论",
+ "docs": "文档",
+ "changelog": "变更日志",
+ "profile": "用户配置",
+ "password": "密码",
+ "team": "团队",
+ "logOut": "登出",
+ "notifications": "通知",
+ "logs": "日志"
+ },
+ "settingsEmailUser": "电子邮件用户 - 用于身份验证的用户名,如果指定,则覆盖电子邮件地址",
+ "state": "状态",
+ "statusBreadCrumbsStatusPages": "状态页",
+ "statusBreadCrumbsDetails": "详情",
+ "commonSaving": "保存中...",
+ "navControls": "控制",
+ "incidentsPageTitle": "事件页面标题",
+ "passwordPanel": {
+ "passwordChangedSuccess": "您的密码已成功更改。",
+ "passwordInputIncorrect": "您的密码输入错误。",
+ "currentPassword": "当前密码",
+ "enterCurrentPassword": "输入当前密码",
+ "newPassword": "新密码",
+ "enterNewPassword": "输入新密码",
+ "confirmNewPassword": "确认新密码",
+ "passwordRequirements": "新密码必须至少包含 8 个字符,并且必须至少包含一个大写字母、一个小写字母、一个数字和一个特殊字符。",
+ "saving": "保存..."
+ },
+ "emailSent": "电子邮件已发送",
+ "failedToSendEmail": "电子邮件发送失败",
+ "settingsTestEmailSuccess": "设置测试电子邮件成功",
+ "settingsTestEmailFailed": "发送测试电子邮件失败",
+ "settingsTestEmailFailedWithReason": "发送测试电子邮件失败: {{reason}}",
+ "settingsTestEmailUnknownError": "未知错误",
+ "statusMsg": {
+ "paused": "监视已暂停。",
+ "up": "您的网站已上线。",
+ "down": "您的网站已下线。",
+ "pending": "等待中..."
+ },
+ "uptimeGeneralInstructions": {
+ "http": "输入要监控的 URL 或 IP(例如.. https://example.com/ or 192.168.1.100)并添加仪表板上显示的清晰显示名称。",
+ "ping": "输入要 ping 的 IP 地址或主机名(例如.. 192.168.1.100 或 example.com),并添加显示在仪表板上的清晰显示名称。",
+ "docker": "码输入 Docker 容器名称或 ID。您可以使用容器名称(例如.. my-app)或容器 ID(完整的 64 字符 ID 或短 ID)。",
+ "port": "输入服务器的 URL 或 IP、端口号和仪表板上显示的清晰显示名称。",
+ "game": "输入要 ping 的 IP 地址或主机名和端口号(例如.. 192.168.1.100 或 example.com)并选择游戏类型。"
+ },
+ "common": {
+ "appName": "Checkmate",
+ "monitoringAgentName": "捕获",
+ "buttons": {
+ "toggleTheme": "切换主题明&暗"
+ },
+ "toasts": {
+ "networkError": "网络错误",
+ "checkConnection": "请检查你的网络连接",
+ "unknownError": "未知错误"
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "继续",
+ "back": "返回"
+ },
+ "inputs": {
+ "email": {
+ "label": "电子邮件",
+ "placeholder": "jordan.ellis@domain.com",
+ "errors": {
+ "empty": "要继续,请输入您的电子邮件地址",
+ "invalid": "请重新检查输入的电子邮件地址的有效性"
+ }
+ },
+ "password": {
+ "label": "密码",
+ "rules": {
+ "length": {
+ "beginning": "必须至少",
+ "highlighted": "8 个字符长"
+ },
+ "special": {
+ "beginning": "必须至少包含",
+ "highlighted": "一个特殊字符"
+ },
+ "number": {
+ "beginning": "必须至少包含",
+ "highlighted": "一个数字"
+ },
+ "uppercase": {
+ "beginning": "必须至少包含",
+ "highlighted": "一个大写字母"
+ },
+ "lowercase": {
+ "beginning": "必须至少包含",
+ "highlighted": "一个小写字母"
+ },
+ "match": {
+ "beginning": "密码",
+ "highlighted": "必须匹配"
+ }
+ },
+ "errors": {
+ "empty": "请输入您的密码",
+ "length": "密码长度必须至少为 8 个字符",
+ "uppercase": "密码必须至少包含 1 个大写字母",
+ "lowercase": "密码必须至少包含 1 个小写字母",
+ "number": "密码必须至少包含 1 个数字",
+ "special": "密码必须至少包含 1 个特殊字符",
+ "incorrect": "您提供的密码与我们的记录不符"
+ }
+ },
+ "passwordConfirm": {
+ "label": "确认密码",
+ "placeholder": "重新输入密码以确认",
+ "errors": {
+ "empty": "请再次输入您的密码以进行确认(有助于解决拼写错误)",
+ "different": "输入的密码不匹配,因此其中一个密码可能输入错误"
+ }
+ },
+ "firstName": {
+ "label": "名字",
+ "placeholder": "Jordan",
+ "errors": {
+ "empty": "请输入您的姓名",
+ "length": "姓名必须少于 50 个字符",
+ "pattern": "名称只能包含字母、空格、撇号或连字符"
+ }
+ },
+ "lastName": {
+ "label": "姓",
+ "placeholder": "Ellis",
+ "errors": {
+ "empty": "请输入您的姓氏",
+ "length": "姓氏必须少于 50 个字符",
+ "pattern": "姓氏只能包含字母、空格、撇号或连字符"
+ }
+ }
+ },
+ "errors": {
+ "validation": "验证数据时出错。"
+ },
+ "fields": {
+ "password": {
+ "errors": {
+ "incorrect": "您提供的密码与我们的记录不符"
+ }
+ },
+ "role": {
+ "errors": {
+ "min": "必须至少设置一个角色"
+ }
+ }
+ }
+ },
+ "login": {
+ "heading": "登录以继续",
+ "subheadings": {
+ "stepOne": "输入您的电子邮件",
+ "stepTwo": "输入您的密码"
+ },
+ "links": {
+ "forgotPassword": "忘记密码?",
+ "register": "没有帐户?",
+ "forgotPasswordLink": "重置密码",
+ "registerLink": "注册链接"
+ },
+ "toasts": {
+ "success": "欢迎回来!您已成功登录。",
+ "incorrectPassword": "密码错误"
+ },
+ "errors": {
+ "password": {
+ "incorrect": "您提供的密码与我们的记录不符"
+ }
+ },
+ "welcome": "欢迎回到 Checkmate!"
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "创建超级管理员",
+ "user": "注册"
+ },
+ "subheadings": {
+ "stepOne": "输入您的个人详细信息",
+ "stepTwo": "输入您的电子邮件",
+ "stepThree": "创建密码"
+ },
+ "description": {
+ "superAdmin": "创建超级用户帐户以开始使用",
+ "user": "注册为用户并请求超级管理员访问您的监视器"
+ },
+ "gettingStartedButton": {
+ "superAdmin": "创建超级管理员帐户",
+ "user": "注册普通用户"
+ },
+ "termsAndPolicies": "创建帐户即表示您同意我们的服务条款和隐私政策 。",
+ "links": {
+ "login": "已拥有帐号? 登录"
+ },
+ "toasts": {
+ "success": "欢迎!您的账号已成功创建。"
+ },
+ "welcome": "欢迎来到Checkmate!"
+ },
+ "forgotPassword": {
+ "heading": "忘记密码?",
+ "subheadings": {
+ "stepOne": "不用担心,我们会向您发送重置说明。",
+ "stepTwo": "我们已发送密码重置链接到 ",
+ "stepThree": "您的新密码必须与以前使用的密码不同。",
+ "stepFour": "您的密码已成功重置。点击下方登录。"
+ },
+ "buttons": {
+ "openEmail": "打开电子邮件应用",
+ "resetPassword": "重置密码"
+ },
+ "imageAlts": {
+ "passwordKey": "密码图标",
+ "email": "电子邮件图标",
+ "lock": "锁定图标",
+ "passwordConfirm": "密码确认图标"
+ },
+ "links": {
+ "login": "返回到登录",
+ "resend": "没有收到电子邮件? 点击重新发送"
+ },
+ "toasts": {
+ "sent": "指令发送至 .",
+ "emailNotFound": "未找到电子邮件。",
+ "redirect": "将在秒后重定向...",
+ "success": "您的密码已成功重置。",
+ "error": "无法重置密码。请稍后重试或联系支持人员。"
+ }
+ }
+ },
+ "errorPages": {
+ "serverUnreachable": {
+ "toasts": {
+ "reconnected": "已成功重新连接到服务器。",
+ "stillUnreachable": "服务器仍然无法访问。请稍后重试。"
+ },
+ "alertBox": "服务器连接错误",
+ "description": "我们无法连接到服务器。如果问题仍然存在,请检查您的互联网连接或验证您的部署配置。",
+ "retryButton": {
+ "default": "尝试重连",
+ "processing": "连接中..."
+ }
+ }
+ },
+ "createNotifications": {
+ "title": "创建通知通道",
+ "nameSettings": {
+ "title": "名字",
+ "description": "集成的描述性名称。",
+ "nameLabel": "名字",
+ "namePlaceholder": "例如.. Slack 通知"
+ },
+ "typeSettings": {
+ "title": "类型",
+ "description": "选择要创建的通知通道类型。",
+ "typeLabel": "类型"
+ },
+ "emailSettings": {
+ "title": "邮箱",
+ "description": "目标电子邮件地址。",
+ "emailLabel": "电子邮件地址",
+ "emailPlaceholder": "例如.. john@example.com"
+ },
+ "slackSettings": {
+ "title": "Slack",
+ "description": "在此处配置您的 Slack Webhook",
+ "webhookLabel": "Slack webhook 网址",
+ "webhookPlaceholder": "https://hooks.slack.com/services/..."
+ },
+ "pagerdutySettings": {
+ "title": "PagerDuty",
+ "description": "在此处配置您的 PagerDuty 集成",
+ "integrationKeyLabel": "集成密钥",
+ "integrationKeyPlaceholder": "1234567890"
+ },
+ "discordSettings": {
+ "title": "Discord",
+ "description": "在此处配置您的 Discord Webhook",
+ "webhookLabel": "Discord Webhook 网址",
+ "webhookPlaceholder": "https://your-server.com/webhook"
+ },
+ "webhookSettings": {
+ "title": "Webhook",
+ "description": "在此处配置 Webhook",
+ "webhookLabel": "Webhook网址",
+ "webhookPlaceholder": "https://your-server.com/webhook"
+ },
+ "testNotification": "测试通知",
+ "dialogDeleteTitle": "你确认要删除此通知吗?",
+ "dialogDeleteConfirm": "删除"
+ },
+ "notificationConfig": {
+ "title": "通知",
+ "description": "选择要使用的通知渠道"
+ },
+ "monitorStatus": {
+ "checkingEvery": "每隔 {{interval}} 检查一次",
+ "withCaptureAgent": "使用 Capture 代理 {{version}}",
+ "up": "在线",
+ "down": "离线",
+ "paused": "暂停"
+ },
+ "advancedMatching": "高级匹配",
+ "sendTestNotifications": "发送测试通知",
+ "selectAll": "全选",
+ "showAdminLoginLink": "显示“管理员?在此处登录“状态页面上的链接",
+ "logsPage": {
+ "title": "日志",
+ "description": "系统日志 - 最后 1000 行",
+ "tabs": {
+ "queue": "作业队列",
+ "logs": "服务器日志",
+ "diagnostics": "诊断"
+ },
+ "toast": {
+ "fetchLogsSuccess": "日志获取成功"
+ },
+ "logLevelSelect": {
+ "title": "日志等级",
+ "values": {
+ "all": "全部",
+ "info": "信息",
+ "warn": "警告",
+ "error": "错误",
+ "debug": "调试"
+ }
+ }
+ },
+ "queuePage": {
+ "title": "队列",
+ "refreshButton": "刷新",
+ "flushButton": "刷新队列",
+ "jobTable": {
+ "title": "当前在队列中的作业",
+ "idHeader": "监视器 ID",
+ "urlHeader": "网址",
+ "typeHeader": "类型",
+ "activeHeader": "有效",
+ "lockedAtHeader": "锁定在",
+ "runCountHeader": "运行计数",
+ "failCountHeader": "失败计数",
+ "lastRunHeader": "上次运行时间",
+ "lastFinishedAtHeader": "最后完成时间",
+ "lastRunTookHeader": "上次运行花费了",
+ "intervalHeader": "间隔"
+ },
+ "metricsTable": {
+ "title": "队列指标",
+ "metricHeader": "度量",
+ "valueHeader": "值"
+ },
+ "failedJobTable": {
+ "title": "失败的作业",
+ "monitorIdHeader": "监视器ID",
+ "monitorUrlHeader": "监控网址",
+ "failCountHeader": "失败计数",
+ "failedAtHeader": "上次失败时于",
+ "failReasonHeader": "失败原因"
+ }
+ },
+ "export": {
+ "title": "导出监视器",
+ "success": "监视器导出成功!",
+ "failed": "导出监视器失败"
+ },
+ "monitorActions": {
+ "title": "导出/导入",
+ "import": "导入监视器",
+ "export": "导出监视器",
+ "deleteSuccess": "监视项删除成功",
+ "deleteFailed": "删除监视项失败",
+ "details": "详情"
+ },
+ "settingsPage": {
+ "aboutSettings": {
+ "labelDevelopedBy": "由 Bluewave Labs 开发",
+ "labelVersion": "版本",
+ "title": "关于"
+ },
+ "demoMonitorsSettings": {
+ "buttonAddMonitors": "添加演示监视器",
+ "description": "添加示例监视器以进行演示。",
+ "title": "示例监视器"
+ },
+ "emailSettings": {
+ "buttonSendTestEmail": "发送测试电子邮件",
+ "description": "配置系统的电子邮件设置。这用于发送通知和警报。",
+ "descriptionTransport": "这将为 NodeMailer 构建 SMTP 传输",
+ "labelAddress": "电子邮件地址 - 用于身份验证",
+ "labelConnectionHost": "电子邮件连接主机 - 要在 HELO/EHLO 问候语中使用的主机名",
+ "labelHost": "电子邮件主机 - 要连接到的主机名或 IP 地址",
+ "labelIgnoreTLS": "禁用 STARTTLS:即使服务器支持 TLS,也不要使用 TLS",
+ "labelPassword": "电子邮件密码 - 身份验证密码",
+ "labelPasswordSet": "已设置密码。单击重置以更改它。",
+ "labelPool": "启用连接池:重用现有连接以提高性能",
+ "labelPort": "电子邮件端口 - 要连接的端口",
+ "labelRejectUnauthorized": "拒绝无效证书:拒绝具有自签名或不受信任的证书的连接",
+ "labelRequireTLS": "强制 STARTTLS:需要 TLS 升级,如果不支持则失败",
+ "labelSecure": "使用 SSL(推荐):使用 SSL/TLS 加密连接",
+ "labelTLSServername": "TLS 服务器名称 - 当主机是 IP 时,用于 TLS 验证的可选主机名",
+ "labelUser": "电子邮件用户 - 用于身份验证的用户名,如果指定,则覆盖电子邮件地址",
+ "linkTransport": "在此处查看规格",
+ "placeholderUser": "如果不需要,请留空",
+ "title": "电子邮件",
+ "toastEmailRequiredFieldsError": "需要输入电子邮件地址、主机、端口和密码"
+ },
+ "pageSpeedSettings": {
+ "description": "输入您的 Google PageSpeed API 密钥以启用 Google PageSpeed 监控。单击重置以更新密钥。",
+ "labelApiKeySet": "API 密钥已设置。单击重置以更改它。",
+ "labelApiKey": "ageSpeed API 密钥",
+ "title": "Google PageSpeed API 密钥"
+ },
+ "saveButtonLabel": "保存",
+ "statsSettings": {
+ "clearAllStatsButton": "清除所有统计",
+ "clearAllStatsDescription": "清除所有统计数据。这是不可逆转的。",
+ "clearAllStatsDialogConfirm": "是的,清除所有统计数据",
+ "clearAllStatsDialogDescription": "一旦删除,监控历史记录和统计信息将无法检索。",
+ "clearAllStatsDialogTitle": "你想清除所有的统计数据吗?",
+ "description": "定义要保留历史数据的时间。您还可以清除所有现有数据。",
+ "labelTTL": "要保留监控历史记录的天数。",
+ "labelTTLOptional": "0 表示无限",
+ "title": "查看历史记录"
+ },
+ "systemResetSettings": {
+ "buttonRemoveAllMonitors": "移除所有监视项",
+ "description": "从系统中移除所有监视项。",
+ "dialogConfirm": "确定,移除所有监视项",
+ "dialogDescription": "移除后,无法恢复这些监视项。",
+ "dialogTitle": "你想移除所有的监视项吗?",
+ "title": "系统复位"
+ },
+ "timezoneSettings": {
+ "description": "选择用于在整个应用程序中显示日期和时间时区。",
+ "label": "显示时区",
+ "title": "显示时区"
+ },
+ "title": "设置",
+ "uiSettings": {
+ "description": "在亮色和暗色模式之间切换,或更改用户界面语言。",
+ "labelLanguage": "语言",
+ "labelTheme": "主题模式",
+ "title": "外观"
+ },
+ "urlSettings": {
+ "description": "在公共状态页面上显示监控的 IP 地址或 URL。如果禁用,则仅显示监控名称以保护敏感信息。",
+ "label": "在状态页面显示 IP/URL",
+ "selectDisabled": "禁用",
+ "selectEnabled": "启用",
+ "title": "在状态页面上监控 IP/URL"
+ },
+ "globalThresholds": {
+ "title": "全局阈值",
+ "description": "配置 CPU、内存、磁盘和温度阈值。如果提供了阈值,则会自动启用监控。"
+ }
+ },
+ "statusPageCreate": {
+ "buttonSave": "保存"
+ },
+ "incidentsOptionsHeaderFilterResolved": "已解决",
+ "settingsSave": "保存",
+ "statusPageCreateAppearanceTitle": "外观",
+ "confirmPassword": "确认密码",
+ "monitorHooks": {
+ "failureAddDemoMonitors": "未能添加示例监视器",
+ "successAddDemoMonitors": "成功添加示例监视器"
+ },
+ "settingsAppearance": "外观",
+ "settingsDisplayTimezone": "显示时区",
+ "settingsGeneralSettings": "常规设置",
+ "incidentsOptionsHeaderTotalIncidents": "事件总数",
+ "statusPage": {
+ "deleteSuccess": "状态页面已成功删除",
+ "deleteFailed": "删除状态页面失败",
+ "createSuccess": "状态页创建成功",
+ "updateSuccess": "状态页更新成功",
+ "generalSettings": "常规设置",
+ "contents": "内容",
+ "fallback": {
+ "checks": [
+ "实时监控和显示服务的运行状况",
+ "跟踪多个服务并共享其状态",
+ "让用户了解中断和性能"
+ ],
+ "title": "状态页用于:",
+ "actionButton": "让我们创建你的第一个状态页!"
+ }
+ },
+ "testNotificationsDisabled": "此监视器没有通知设置。您需要通过单击‘配置’按钮来添加一个",
+ "incidentsTableResolvedAt": "解决于",
+ "incidentsTableActionResolve": "解决",
+ "checkHooks": {
+ "failureResolveOne": "未能解决事件。",
+ "failureResolveAll": "未能解决所有事件。",
+ "failureResolveMonitor": "故障排除监控事件失败。"
+ },
+ "checkFormError": "请检查表单中的错误。",
+ "diagnosticsPage": {
+ "diagnosticDescription": "系统诊断",
+ "statsDescription": "系统统计数据",
+ "gauges": {
+ "heapAllocationTitle": "堆分配",
+ "heapAllocationSubtitle": "% 的可用内存占用率",
+ "heapUsageTitle": "堆使用情况",
+ "heapUsageSubtitle": "% 的可用内存占用率",
+ "heapUtilizationTitle": "堆利用率",
+ "heapUtilizationSubtitle": "% 的分配百分比",
+ "instantCpuUsageTitle": "当前 CPU 使用率",
+ "instantCpuUsageSubtitle": "% 的1秒CPU占用率"
+ },
+ "stats": {
+ "eventLoopDelayTitle": "事件循环延迟",
+ "uptimeTitle": "运行时间",
+ "usedHeapSizeTitle": "已用堆内存大小",
+ "totalHeapSizeTitle": "总堆大小",
+ "osMemoryLimitTitle": "系统内存限制"
+ }
+ },
+ "pageSpeedLighthouseAPI": "使用 Lighthouse PageSpeed API 监控您的网站",
+ "time": {
+ "threeMinutes": "3 分钟",
+ "fiveMinutes": "5 分钟",
+ "tenMinutes": "10 分钟",
+ "twentyMinutes": "20 分钟",
+ "oneHour": "1 小时",
+ "oneDay": "1 天",
+ "oneWeek": "1 周",
+ "fourMinutes": "4 分钟",
+ "oneMinute": "1 分钟",
+ "twoMinutes": "2 分钟",
+ "fifteenSeconds": "15 秒",
+ "thirtySeconds": "30 秒"
+ },
+ "general": {
+ "noOptionsFound": "{{unit}} 未找到"
+ },
+ "infrastructureMonitor": {
+ "fallback": {
+ "checks": [
+ "追踪服务器性能表现",
+ "识别瓶颈并优化资源利用率",
+ "通过实时监控保障系统可靠性"
+ ],
+ "title": "基础设施监视器用于:",
+ "actionButton": "让我们创建你的第一个基础设施视器!"
+ }
+ },
+ "maintenanceWindow": {
+ "fallback": {
+ "checks": ["标记维护时段", "消除所有误解", "停止在维护窗口内发送警报"],
+ "title": "维护时段用于:",
+ "actionButton": "让我们创建您的第一个维护时段!"
+ }
+ },
+ "pageSpeed": {
+ "fallback": {
+ "checks": ["报告页面的用户体验", "帮助分析网页速度", "就如何改进页面提出建议"],
+ "title": "PageSpeed 监视器用于:",
+ "actionButton": "让我们创建您的第一个 PageSpeed 监视器!"
+ }
+ },
+ "uptimeMonitor": {
+ "fallback": {
+ "checks": [
+ "检测网站或服务器是否在线及响应正常",
+ "向团队发送停机或性能问题警报",
+ "监控 HTTP 端点、Ping 测试、容器及端口状态",
+ "追踪历史运行时长与可靠性趋势"
+ ],
+ "title": "正常运行时间监视器用于:",
+ "actionButton": "让我们创建您的第一个正常运行时间监视器!"
+ }
+ },
+ "editUserPage": {
+ "form": {
+ "email": "电子邮件",
+ "firstName": "名",
+ "lastName": "姓氏",
+ "role": "角色",
+ "save": "保存"
+ },
+ "table": {
+ "actionHeader": "操作",
+ "roleHeader": "角色"
+ },
+ "title": "编辑用户",
+ "toast": {
+ "successUserUpdate": "用户更新成功",
+ "validationErrors": "验证错误"
+ }
+ },
+ "incidentsPageActionResolveMonitor": "解决监控事件",
+ "incidentsPageActionResolveAll": "解决所有事件",
+ "matchMethodOptions": {
+ "equal": "相等",
+ "equalPlaceholder": "成功",
+ "include": "包含",
+ "includePlaceholder": "OK",
+ "regex": "正则表达式",
+ "regexPlaceholder": "^(success|ok)$",
+ "text": "匹配方法"
+ },
+ "monitorType": {
+ "docker": {
+ "label": "容器 ID",
+ "namePlaceholder": "我的容器",
+ "placeholder": "我的应用程序或 abcd1234"
+ },
+ "http": {
+ "label": "要监控的 URL",
+ "namePlaceholder": "Google",
+ "placeholder": "google.com"
+ },
+ "ping": {
+ "label": "要监控的 IP 地址",
+ "namePlaceholder": "Google",
+ "placeholder": "1.1.1.1"
+ },
+ "port": {
+ "label": "要监控的 URL",
+ "namePlaceholder": "localhost:5173",
+ "placeholder": "localhost"
+ },
+ "game": {
+ "label": "丢包监控网址",
+ "namePlaceholder": "localhost:5173",
+ "placeholder": "localhost"
+ }
+ },
+ "uptimeAdvancedMatching": {
+ "jsonPath": "JSON 路径"
+ },
+ "bytesPerSecond": "每秒字节数",
+ "bytesReceived": "已接收字节数",
+ "bytesSent": "已发送字节数",
+ "chooseGame": "选择游戏",
+ "createMonitorPage": {
+ "incidentConfigDescription": "滑动窗口用于确定监视器何时发生故障。仅当滑动窗口中的检查百分比满足指定值时,监视器的状态才会改变。",
+ "incidentConfigStatusWindowLabel": "在滑动窗口中应包含多少次检测?",
+ "incidentConfigStatusWindowThresholdLabel": "在监视器状态改变之前,滑动窗口中的检查失败/成功的百分比是多少?",
+ "incidentConfigTitle": "事件"
+ },
+ "dataRate": "数据速率",
+ "dataReceived": "已接收数据",
+ "dataSent": "已发送数据",
+ "details": "详情",
+ "drops": "丢包",
+ "errors": "错误",
+ "errorsIn": "错误于",
+ "errorsOut": "错误输出",
+ "gameServerMonitoring": "游戏服务器监视",
+ "gameServerMonitoringDescription": "检查你的游戏服务器是否正在运行",
+ "network": "网络",
+ "networkDrops": "网络丢包",
+ "networkErrors": "网络错误",
+ "networkInterface": "网络接口",
+ "noNetworkStatsAvailable": "没有可用的网络统计数据。",
+ "packetsPerSecond": "每秒数据包数",
+ "packetsReceived": "已接收数据包",
+ "packetsReceivedRate": "数据包接收率",
+ "packetsSent": "已发送数据包",
+ "rate": "速度",
+ "selectInterface": "选择接口"
+}
diff --git a/client/src/locales/zh-TW.json b/client/src/locales/zh-TW.json
index 8e0dc2ed2..e57ebd487 100644
--- a/client/src/locales/zh-TW.json
+++ b/client/src/locales/zh-TW.json
@@ -380,7 +380,52 @@
"email": "",
"role": "",
"created": ""
- }
+ },
+ "addTeamMember": {
+ "addMemberMenu": "",
+ "title": "",
+ "description": "",
+ "addButton": ""
+ },
+ "register": "",
+ "registerToast": {
+ "success": "",
+ "dbUserExists": "",
+ "unknownError": ""
+ },
+ "registerTeamMember": {
+ "title": "",
+ "auth": {
+ "common": {
+ "inputs": {
+ "firstName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "errors": {
+ "empty": "",
+ "pattern": ""
+ }
+ },
+ "email": {
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "role": {
+ "errors": {
+ "empty": ""
+ }
+ }
+ }
+ }
+ }
+ },
+ "role": ""
},
"monitorState": {
"paused": "",
@@ -746,7 +791,8 @@
"failCountHeader": "",
"lastRunHeader": "",
"lastFinishedAtHeader": "",
- "lastRunTookHeader": ""
+ "lastRunTookHeader": "",
+ "intervalHeader": ""
},
"metricsTable": {
"title": "",
diff --git a/client/src/main.jsx b/client/src/main.jsx
index 01ecefcc6..dd42587af 100644
--- a/client/src/main.jsx
+++ b/client/src/main.jsx
@@ -8,7 +8,7 @@ import { PersistGate } from "redux-persist/integration/react";
import NetworkServiceProvider from "./Utils/NetworkServiceProvider.jsx";
import { networkService } from "./Utils/NetworkService";
export { networkService };
-import I18nLoader from "./Components/I18nLoader";
+import I18nLoader from "./Components/v1/I18nLoader";
ReactDOM.createRoot(document.getElementById("root")).render(
diff --git a/client/src/store.js b/client/src/store.js
index 69aa3d50b..565d68519 100644
--- a/client/src/store.js
+++ b/client/src/store.js
@@ -1,6 +1,7 @@
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import authReducer from "./Features/Auth/authSlice";
+import v2AuthReducer from "./Features/Auth/v2AuthSlice";
import uiReducer from "./Features/UI/uiSlice";
import storage from "redux-persist/lib/storage";
import { persistReducer, persistStore, createTransform } from "redux-persist";
@@ -19,12 +20,13 @@ const authTransform = createTransform(
const persistConfig = {
key: "root",
storage,
- whitelist: ["auth", "ui"],
+ whitelist: ["auth", "v2Auth", "ui"],
transforms: [authTransform],
};
const rootReducer = combineReducers({
auth: authReducer,
+ v2Auth: v2AuthReducer,
ui: uiReducer,
});
diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json
new file mode 100644
index 000000000..f44839e9f
--- /dev/null
+++ b/client/tsconfig.app.json
@@ -0,0 +1,32 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true,
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+ "allowJs": true,
+ "checkJs": false
+ },
+ "include": ["src"]
+}
diff --git a/client/tsconfig.json b/client/tsconfig.json
new file mode 100644
index 000000000..3b77ae840
--- /dev/null
+++ b/client/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "files": [],
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
+ "compilerOptions": {
+ "types": ["node"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "skipLibCheck": true,
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+ "allowJs": true,
+ "checkJs": false
+ }
+}
diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json
new file mode 100644
index 000000000..bbd18d2b2
--- /dev/null
+++ b/client/tsconfig.node.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true,
+ "allowJs": true,
+ "checkJs": false
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/client/tsconfig.tsbuildinfo b/client/tsconfig.tsbuildinfo
new file mode 100644
index 000000000..685a9569a
--- /dev/null
+++ b/client/tsconfig.tsbuildinfo
@@ -0,0 +1 @@
+{"root":["./vite.config.ts","./dist/assets/index-BtHE-Ph5.js","./src/App.jsx","./src/main.jsx","./src/store.js","./src/Components/LanguageSelector.jsx","./src/Components/ActionsMenu/index.jsx","./src/Components/Alert/index.jsx","./src/Components/Animated/PulseDot.jsx","./src/Components/ArrowLeft/index.jsx","./src/Components/ArrowRight/index.jsx","./src/Components/Avatar/index.jsx","./src/Components/Breadcrumbs/index.jsx","./src/Components/Buttons/RoundGradientButton.jsx","./src/Components/Charts/AreaChart/index.jsx","./src/Components/Charts/BarChart/index.jsx","./src/Components/Charts/ChartBox/EmptyView.jsx","./src/Components/Charts/ChartBox/index.jsx","./src/Components/Charts/CustomGauge/index.jsx","./src/Components/Charts/DePINStatusPageBarChart/index.jsx","./src/Components/Charts/LegendBox/index.jsx","./src/Components/Charts/MonitorDetailsAreaChart/index.jsx","./src/Components/Charts/StatusPageBarChart/index.jsx","./src/Components/Charts/Utils/chartUtilFunctions.js","./src/Components/Charts/Utils/chartUtils.jsx","./src/Components/Charts/Utils/gradientUtils.jsx","./src/Components/Check/Check.jsx","./src/Components/CircularCount/index.jsx","./src/Components/Common/AppBar.jsx","./src/Components/Common/Footer.jsx","./src/Components/ConfigBox/index.jsx","./src/Components/ConfigRow/index.jsx","./src/Components/Dialog/genericDialog.jsx","./src/Components/Dialog/index.jsx","./src/Components/Dot/index.jsx","./src/Components/Fallback/FallBackActionButtons.jsx","./src/Components/Fallback/FallbackBackground.jsx","./src/Components/Fallback/FallbackCheckList.jsx","./src/Components/Fallback/FallbackContainer.jsx","./src/Components/Fallback/FallbackPageSpeedWarning.jsx","./src/Components/Fallback/FallbackTitle.jsx","./src/Components/Fallback/index.jsx","./src/Components/FilterHeader/index.jsx","./src/Components/GenericFallback/NetworkError.jsx","./src/Components/GenericFallback/index.jsx","./src/Components/HOC/withAdminCheck.jsx","./src/Components/Heading/index.jsx","./src/Components/Host/index.jsx","./src/Components/HttpStatusLabel/index.jsx","./src/Components/I18nLoader/index.jsx","./src/Components/IconBox/index.jsx","./src/Components/Image/index.jsx","./src/Components/InfoBox/index.jsx","./src/Components/Inputs/Checkbox/index.jsx","./src/Components/Inputs/ColorPicker/index.jsx","./src/Components/Inputs/FieldWrapper/index.jsx","./src/Components/Inputs/ImageUpload/index.jsx","./src/Components/Inputs/Radio/index.jsx","./src/Components/Inputs/Search/index.jsx","./src/Components/Inputs/Select/index.jsx","./src/Components/Inputs/TextInput/index.jsx","./src/Components/Inputs/TextInput/indexV2.tsx","./src/Components/Inputs/TextInput/Adornments/index.jsx","./src/Components/Label/index.jsx","./src/Components/Layouts/AppLayout/index.jsx","./src/Components/Layouts/HomeLayout/index.jsx","./src/Components/Link/index.jsx","./src/Components/MonitorActions/index.jsx","./src/Components/MonitorCountHeader/index.jsx","./src/Components/MonitorCountHeader/skeleton.jsx","./src/Components/MonitorCreateHeader/index.jsx","./src/Components/MonitorCreateHeader/skeleton.jsx","./src/Components/MonitorDetailsControlHeader/index.jsx","./src/Components/MonitorDetailsControlHeader/skeleton.jsx","./src/Components/MonitorDetailsControlHeader/status.jsx","./src/Components/MonitorTimeFrameHeader/index.jsx","./src/Components/MonitorTimeFrameHeader/skeleton.jsx","./src/Components/NotificationConfig/index.jsx","./src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx","./src/Components/NotificationIntegrationModal/Components/TabComponent.jsx","./src/Components/NotificationIntegrationModal/Components/TabPanel.jsx","./src/Components/NotificationIntegrationModal/Hooks/useNotification.js","./src/Components/PageStateWrapper/index.jsx","./src/Components/ProgressBars/index.jsx","./src/Components/ProgressStepper/index.jsx","./src/Components/ProtectedRoute/index.jsx","./src/Components/RoleProtectedRoute/index.jsx","./src/Components/ShareComponent/index.jsx","./src/Components/Sidebar/index.jsx","./src/Components/Sidebar/components/authFooter.jsx","./src/Components/Sidebar/components/collapseButton.jsx","./src/Components/Sidebar/components/logo.jsx","./src/Components/Sidebar/components/navItem.jsx","./src/Components/Skeletons/FullPage/index.jsx","./src/Components/StandardContainer/index.jsx","./src/Components/StarPrompt/index.jsx","./src/Components/StatBox/index.jsx","./src/Components/StatusBoxes/index.jsx","./src/Components/StatusBoxes/skeleton.jsx","./src/Components/Subheader/index.jsx","./src/Components/Tab/index.jsx","./src/Components/Table/TableUtils.js","./src/Components/Table/index.jsx","./src/Components/Table/skeleton.jsx","./src/Components/Table/TablePagination/index.jsx","./src/Components/Table/TablePagination/Actions/index.jsx","./src/Components/TextLink/index.jsx","./src/Components/ThemeSwitch/SunAndMoonIcon.jsx","./src/Components/ThemeSwitch/index.jsx","./src/Components/Toast/body.jsx","./src/Components/Toast/index.jsx","./src/Components/WalletProvider/index.jsx","./src/Features/Auth/authSlice.js","./src/Features/UI/uiSlice.js","./src/Hooks/v1/checkHooks.js","./src/Hooks/v1/inviteHooks.js","./src/Hooks/v1/logHooks.js","./src/Hooks/v1/monitorHooks.js","./src/Hooks/v1/settingsHooks.js","./src/Hooks/v1/useIsAdmin.js","./src/Hooks/v1/useMonitorUtils.js","./src/Hooks/v1/useNotifications.js","./src/Hooks/v1/useSendTestEmail.js","./src/Hooks/v1/userHooks.js","./src/Hooks/v2/UseApi.tsx","./src/Pages/v1/ServerUnreachable.jsx","./src/Pages/v1/About/index.jsx","./src/Pages/v1/Account/index.jsx","./src/Pages/v1/Account/EditUser/index.jsx","./src/Pages/v1/Account/EditUser/hooks/editUser.js","./src/Pages/v1/Account/components/PasswordPanel.jsx","./src/Pages/v1/Account/components/ProfilePanel.jsx","./src/Pages/v1/Account/components/TeamPanel.jsx","./src/Pages/v1/Account/components/AddMemberMenu/index.jsx","./src/Pages/v1/Account/components/AddTeamMember/index.jsx","./src/Pages/v1/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx","./src/Pages/v1/Account/components/RoleTable/index.jsx","./src/Pages/v1/Auth/CheckEmail.jsx","./src/Pages/v1/Auth/ForgotPassword.jsx","./src/Pages/v1/Auth/NewPasswordConfirmed.jsx","./src/Pages/v1/Auth/SetNewPassword.jsx","./src/Pages/v1/Auth/Login/index.jsx","./src/Pages/v1/Auth/Login/hooks/useLoadingSubmit.jsx","./src/Pages/v1/Auth/Login/hooks/useLoginForm.jsx","./src/Pages/v1/Auth/Login/hooks/useLoginSubmit.jsx","./src/Pages/v1/Auth/Login/hooks/useValidateLoginForm.jsx","./src/Pages/v1/Auth/Register/index.jsx","./src/Pages/v1/Auth/components/AuthHeader.jsx","./src/Pages/v1/Auth/components/AuthPageWrapper.jsx","./src/Pages/v1/Auth/components/PasswordTooltip.jsx","./src/Pages/v1/Auth/hooks/usePasswordFeedback.jsx","./src/Pages/v1/Auth/hooks/useValidatePassword.jsx","./src/Pages/v1/Incidents/index.jsx","./src/Pages/v1/Incidents/Components/IncidentTable/index.jsx","./src/Pages/v1/Incidents/Components/OptionsHeader/index.jsx","./src/Pages/v1/Incidents/Components/OptionsHeader/skeleton.jsx","./src/Pages/v1/Incidents/Components/StatusBoxes/StatusBox.jsx","./src/Pages/v1/Incidents/Components/StatusBoxes/index.jsx","./src/Pages/v1/Incidents/Components/StatusBoxes/skeleton.jsx","./src/Pages/v1/Infrastructure/Create/index.jsx","./src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx","./src/Pages/v1/Infrastructure/Create/Components/MonitorActionButtons.jsx","./src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx","./src/Pages/v1/Infrastructure/Create/Components/CustomThreshold/index.jsx","./src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx","./src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx","./src/Pages/v1/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx","./src/Pages/v1/Infrastructure/Details/index.jsx","./src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx","./src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx","./src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx","./src/Pages/v1/Infrastructure/Details/Components/BaseContainer/index.jsx","./src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx","./src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx","./src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx","./src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx","./src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx","./src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx","./src/Pages/v1/Infrastructure/Details/Components/NetworkStats/skeleton.jsx","./src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx","./src/Pages/v1/Infrastructure/Details/Hooks/useHardwareUtils.jsx","./src/Pages/v1/Infrastructure/Monitors/index.jsx","./src/Pages/v1/Infrastructure/Monitors/Components/Filters/index.jsx","./src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx","./src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx","./src/Pages/v1/Integrations/index.jsx","./src/Pages/v1/Logs/index.jsx","./src/Pages/v1/Logs/Diagnostics/index.jsx","./src/Pages/v1/Logs/Diagnostics/components/gauges/index.jsx","./src/Pages/v1/Logs/Diagnostics/components/stats/index.jsx","./src/Pages/v1/Logs/Diagnostics/utils/utils.js","./src/Pages/v1/Logs/Logs/index.jsx","./src/Pages/v1/Logs/Queue/index.jsx","./src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx","./src/Pages/v1/Logs/Queue/components/JobTable/index.jsx","./src/Pages/v1/Logs/Queue/components/Metrics/index.jsx","./src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx","./src/Pages/v1/Maintenance/index.jsx","./src/Pages/v1/Maintenance/CreateMaintenance/index.jsx","./src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx","./src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx","./src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx","./src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx","./src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx","./src/Pages/v1/Maintenance/MaintenanceTable/index.jsx","./src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx","./src/Pages/v1/NotFound/index.jsx","./src/Pages/v1/Notifications/index.jsx","./src/Pages/v1/Notifications/utils.js","./src/Pages/v1/Notifications/components/ActionMenu.jsx","./src/Pages/v1/Notifications/create/index.jsx","./src/Pages/v1/PageSpeed/Create/index.jsx","./src/Pages/v1/PageSpeed/Create/skeleton.jsx","./src/Pages/v1/PageSpeed/Details/index.jsx","./src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChart.jsx","./src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx","./src/Pages/v1/PageSpeed/Details/Components/Charts/PieChart.jsx","./src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx","./src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx","./src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx","./src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx","./src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx","./src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx","./src/Pages/v1/PageSpeed/Monitors/index.jsx","./src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx","./src/Pages/v1/PageSpeed/Monitors/Components/MonitorGrid/index.jsx","./src/Pages/v1/Settings/SettingsAbout.jsx","./src/Pages/v1/Settings/SettingsDemoMonitors.jsx","./src/Pages/v1/Settings/SettingsEmail.jsx","./src/Pages/v1/Settings/SettingsGlobalThresholds.jsx","./src/Pages/v1/Settings/SettingsPagespeed.jsx","./src/Pages/v1/Settings/SettingsStats.jsx","./src/Pages/v1/Settings/SettingsTimeZone.jsx","./src/Pages/v1/Settings/SettingsUI.jsx","./src/Pages/v1/Settings/SettingsURL.jsx","./src/Pages/v1/Settings/index.jsx","./src/Pages/v1/StatusPage/Create/index.jsx","./src/Pages/v1/StatusPage/Create/Components/MonitorList/index.jsx","./src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx","./src/Pages/v1/StatusPage/Create/Components/Skeleton/index.jsx","./src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx","./src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx","./src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx","./src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx","./src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx","./src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx","./src/Pages/v1/StatusPage/Status/index.jsx","./src/Pages/v1/StatusPage/Status/Components/AdminLink/index.jsx","./src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx","./src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx","./src/Pages/v1/StatusPage/Status/Components/Skeleton/index.jsx","./src/Pages/v1/StatusPage/Status/Components/StatusBar/index.jsx","./src/Pages/v1/StatusPage/Status/Hooks/useStatusPageDelete.jsx","./src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx","./src/Pages/v1/StatusPage/StatusPages/index.jsx","./src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx","./src/Pages/v1/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx","./src/Pages/v1/Uptime/BulkImport/Upload.jsx","./src/Pages/v1/Uptime/BulkImport/index.jsx","./src/Pages/v1/Uptime/Create/index.jsx","./src/Pages/v1/Uptime/Create/skeleton.jsx","./src/Pages/v1/Uptime/Details/index.jsx","./src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx","./src/Pages/v1/Uptime/Details/Components/ChartBoxes/skeleton.jsx","./src/Pages/v1/Uptime/Details/Components/Charts/CustomLabels.jsx","./src/Pages/v1/Uptime/Details/Components/Charts/DownBarChart.jsx","./src/Pages/v1/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx","./src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx","./src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx","./src/Pages/v1/Uptime/Details/Components/Charts/UpBarChart.jsx","./src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx","./src/Pages/v1/Uptime/Details/Components/ResponseTable/skeleton.jsx","./src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx","./src/Pages/v1/Uptime/Details/Hooks/useCertificateFetch.jsx","./src/Pages/v1/Uptime/Monitors/index.jsx","./src/Pages/v1/Uptime/Monitors/Components/Filter/index.jsx","./src/Pages/v1/Uptime/Monitors/Components/LoadingSpinner/index.jsx","./src/Pages/v1/Uptime/Monitors/Components/SearchComponent/index.jsx","./src/Pages/v1/Uptime/Monitors/Components/Skeleton/index.jsx","./src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/index.jsx","./src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx","./src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx","./src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx","./src/Pages/v1/Uptime/Monitors/Hooks/useDebounce.jsx","./src/Pages/v2/Auth/Login.tsx","./src/Routes/index.jsx","./src/Utils/ApiClient.ts","./src/Utils/Logger.js","./src/Utils/NetworkService.js","./src/Utils/NetworkServiceProvider.jsx","./src/Utils/debounce.jsx","./src/Utils/fileUtils.js","./src/Utils/greeting.jsx","./src/Utils/i18n.js","./src/Utils/monitorUtils.js","./src/Utils/roleUtils.js","./src/Utils/stringUtils.js","./src/Utils/timeUtils.js","./src/Utils/toastUtils.jsx","./src/Utils/utils.js","./src/Utils/Theme/constants.js","./src/Utils/Theme/darkTheme.js","./src/Utils/Theme/extractColorObject.js","./src/Utils/Theme/globalTheme.js","./src/Utils/Theme/lightTheme.js","./src/Utils/Theme/v2/palette.ts","./src/Utils/Theme/v2/theme.ts","./src/Validation/error.js","./src/Validation/validation.js","./src/types/env.d.ts","./src/types/mui.d.ts"],"errors":true,"version":"5.9.2"}
\ No newline at end of file
diff --git a/client/vite.config.js b/client/vite.config.js
deleted file mode 100644
index 158dc5e4e..000000000
--- a/client/vite.config.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { defineConfig, loadEnv } from "vite";
-import react from "@vitejs/plugin-react";
-import svgr from "vite-plugin-svgr";
-import { execSync } from "child_process";
-
-export default defineConfig(({ mode }) => {
- const env = loadEnv(mode, process.cwd(), "");
- let version = "3.1.8";
-
- return {
- base: "/",
- plugins: [svgr(), react()],
- optimizeDeps: {
- include: ["@mui/material/Tooltip", "@emotion/styled"],
- },
- define: {
- __APP_VERSION__: JSON.stringify(version),
- },
- };
-});
diff --git a/client/vite.config.ts b/client/vite.config.ts
new file mode 100644
index 000000000..4c60ce2a4
--- /dev/null
+++ b/client/vite.config.ts
@@ -0,0 +1,27 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import svgr from "vite-plugin-svgr";
+import path from "path";
+import { fileURLToPath } from "url";
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+export default defineConfig(({}) => {
+ let version = "3.2.0";
+
+ return {
+ base: "/",
+ plugins: [svgr(), react()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "src"),
+ },
+ },
+ optimizeDeps: {
+ include: ["@mui/material/Tooltip", "@emotion/styled"],
+ },
+ define: {
+ __APP_VERSION__: JSON.stringify(version),
+ },
+ };
+});
diff --git a/docker/dev/build_images.sh b/docker/dev/build_images.sh
index e94be9fa5..eb43e31d8 100755
--- a/docker/dev/build_images.sh
+++ b/docker/dev/build_images.sh
@@ -5,11 +5,10 @@ cd "$(dirname "$0")"
cd ../..
# Define service names and their corresponding Dockerfiles in parallel arrays
-services=("uptime_client" "uptime_mongo" "uptime_redis" "uptime_server")
+services=("uptime_client" "uptime_mongo" "uptime_server")
dockerfiles=(
"./docker/dev/client.Dockerfile"
"./docker/dev/mongoDB.Dockerfile"
- "./docker/dev/redis.Dockerfile"
"./docker/dev/server.Dockerfile"
)
diff --git a/docker/dev/server.Dockerfile b/docker/dev/server.Dockerfile
index d24bff63f..2a6ebcb7c 100755
--- a/docker/dev/server.Dockerfile
+++ b/docker/dev/server.Dockerfile
@@ -13,6 +13,8 @@ RUN npm install
COPY ./server/ ./
+RUN npm run build
+
EXPOSE 52345
-CMD ["node", "src/index.js"]
\ No newline at end of file
+CMD ["node", "dist/index.js"]
\ No newline at end of file
diff --git a/docker/dist-arm/server.Dockerfile b/docker/dist-arm/server.Dockerfile
index e47db9a74..91ccd427e 100644
--- a/docker/dist-arm/server.Dockerfile
+++ b/docker/dist-arm/server.Dockerfile
@@ -35,10 +35,12 @@ RUN npm install
COPY server ./
-COPY --from=frontend-build /app/client/dist ./public
-
RUN chmod +x ./scripts/inject-vars.sh
+RUN npm run build
+
+COPY --from=frontend-build /app/client/dist ./public
+
EXPOSE 52345
-CMD ["sh", "-c", "./scripts/inject-vars.sh && node ./src/index.js"]
+CMD ["sh", "-c", "./scripts/inject-vars.sh && node ./dist/index.js"]
diff --git a/docker/dist-mono/docker-compose-local.yaml b/docker/dist-mono/docker-compose-local.yaml
new file mode 100755
index 000000000..a5616695b
--- /dev/null
+++ b/docker/dist-mono/docker-compose-local.yaml
@@ -0,0 +1,27 @@
+services:
+ server:
+ image: mono_server
+ restart: always
+ ports:
+ - "52345:52345"
+ environment:
+ - UPTIME_APP_API_BASE_URL=http://localhost:52345/api/v1
+ - UPTIME_APP_CLIENT_HOST=http://localhost
+ - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db
+ - CLIENT_HOST=http://localhost
+ - JWT_SECRET=my_secret
+ depends_on:
+ - mongodb
+ mongodb:
+ image: mono_mongo
+ restart: always
+ command: ["mongod", "--quiet", "--bind_ip_all"]
+ volumes:
+ - ./mongo/data:/data/db
+ healthcheck:
+ test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"]
+ interval: 5s
+ timeout: 30s
+ start_period: 0s
+ start_interval: 1s
+ retries: 30
diff --git a/docker/dist-mono/server.Dockerfile b/docker/dist-mono/server.Dockerfile
index 405f9e6ac..84b7127ed 100644
--- a/docker/dist-mono/server.Dockerfile
+++ b/docker/dist-mono/server.Dockerfile
@@ -19,12 +19,15 @@ WORKDIR /app/server
COPY server ./
-COPY --from=frontend-build /app/client/dist ./public
RUN npm install
+RUN npm run build
+
+COPY --from=frontend-build /app/client/dist ./public
+
RUN chmod +x ./scripts/inject-vars.sh
EXPOSE 52345
-CMD ./scripts/inject-vars.sh && node ./src/index.js
+CMD ./scripts/inject-vars.sh && node ./dist/index.js
diff --git a/docker/dist/server.Dockerfile b/docker/dist/server.Dockerfile
index d24bff63f..2a6ebcb7c 100755
--- a/docker/dist/server.Dockerfile
+++ b/docker/dist/server.Dockerfile
@@ -13,6 +13,8 @@ RUN npm install
COPY ./server/ ./
+RUN npm run build
+
EXPOSE 52345
-CMD ["node", "src/index.js"]
\ No newline at end of file
+CMD ["node", "dist/index.js"]
\ No newline at end of file
diff --git a/docker/prod/server.Dockerfile b/docker/prod/server.Dockerfile
index b78de2e33..9d9800e7b 100755
--- a/docker/prod/server.Dockerfile
+++ b/docker/prod/server.Dockerfile
@@ -15,6 +15,8 @@ RUN npm install
COPY ./server ./
+RUN npm run build
+
EXPOSE 52345
CMD ["node", "src/index.js"]
\ No newline at end of file
diff --git a/docker/staging/server.Dockerfile b/docker/staging/server.Dockerfile
index b78de2e33..7341b1e9b 100755
--- a/docker/staging/server.Dockerfile
+++ b/docker/staging/server.Dockerfile
@@ -15,6 +15,8 @@ RUN npm install
COPY ./server ./
+RUN npm run build
+
EXPOSE 52345
-CMD ["node", "src/index.js"]
\ No newline at end of file
+CMD ["node", "dist/index.js"]
\ No newline at end of file
diff --git a/server/.gitignore b/server/.gitignore
index 4bc621f83..9ae4e58ed 100755
--- a/server/.gitignore
+++ b/server/.gitignore
@@ -8,4 +8,5 @@ coverage
.clinic
node_modules
.vscode/*
-public
\ No newline at end of file
+public
+dist
\ No newline at end of file
diff --git a/server/nodemon.json b/server/nodemon.json
index 88d6ac686..389209fa7 100755
--- a/server/nodemon.json
+++ b/server/nodemon.json
@@ -1,5 +1,5 @@
{
"ignore": ["src/locales/*", "*.log", "node_modules/*"],
- "watch": ["src/**/*.js", "*.json"],
- "ext": "js,json"
+ "watch": ["src/**/*.ts", "src/**/*.js", "*.json"],
+ "ext": "ts,js,json"
}
diff --git a/server/package-lock.json b/server/package-lock.json
index 3e1130dc4..8c9eac34c 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -45,6 +45,10 @@
},
"devDependencies": {
"@eslint/js": "^9.17.0",
+ "@types/express": "5.0.3",
+ "@types/jsonwebtoken": "9.0.10",
+ "@types/nodemailer": "7.0.1",
+ "@types/ping": "0.4.4",
"c8": "10.1.3",
"chai": "5.2.0",
"eslint": "^9.17.0",
@@ -54,7 +58,9 @@
"mocha": "11.1.0",
"nodemon": "3.1.9",
"prettier": "^3.3.3",
- "sinon": "19.0.2"
+ "sinon": "19.0.2",
+ "tsx": "4.20.5",
+ "typescript": "5.9.2"
}
},
"node_modules/@asamuzakjp/css-color": {
@@ -70,6 +76,751 @@
"lru-cache": "^10.4.3"
}
},
+ "node_modules/@aws-crypto/sha256-browser": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
+ "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-js": "^5.2.0",
+ "@aws-crypto/supports-web-crypto": "^5.2.0",
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "@aws-sdk/util-locate-window": "^3.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
+ "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
+ "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
+ "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-js": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
+ "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/supports-web-crypto": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz",
+ "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/util": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
+ "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.222.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
+ "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
+ "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
+ "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2": {
+ "version": "3.895.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.895.0.tgz",
+ "integrity": "sha512-tYfgwoG4n8LEc0NHVriNNS+YqeDMJHHcUzJxJJ7Nols5Y3EJzv76QRvu2rdLa9XoxvTSFGXJSuqrd8s0ElbCQQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.894.0",
+ "@aws-sdk/credential-provider-node": "3.895.0",
+ "@aws-sdk/middleware-host-header": "3.893.0",
+ "@aws-sdk/middleware-logger": "3.893.0",
+ "@aws-sdk/middleware-recursion-detection": "3.893.0",
+ "@aws-sdk/middleware-user-agent": "3.895.0",
+ "@aws-sdk/region-config-resolver": "3.893.0",
+ "@aws-sdk/signature-v4-multi-region": "3.894.0",
+ "@aws-sdk/types": "3.893.0",
+ "@aws-sdk/util-endpoints": "3.895.0",
+ "@aws-sdk/util-user-agent-browser": "3.893.0",
+ "@aws-sdk/util-user-agent-node": "3.895.0",
+ "@smithy/config-resolver": "^4.2.2",
+ "@smithy/core": "^3.11.1",
+ "@smithy/fetch-http-handler": "^5.2.1",
+ "@smithy/hash-node": "^4.1.1",
+ "@smithy/invalid-dependency": "^4.1.1",
+ "@smithy/middleware-content-length": "^4.1.1",
+ "@smithy/middleware-endpoint": "^4.2.3",
+ "@smithy/middleware-retry": "^4.2.4",
+ "@smithy/middleware-serde": "^4.1.1",
+ "@smithy/middleware-stack": "^4.1.1",
+ "@smithy/node-config-provider": "^4.2.2",
+ "@smithy/node-http-handler": "^4.2.1",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/smithy-client": "^4.6.3",
+ "@smithy/types": "^4.5.0",
+ "@smithy/url-parser": "^4.1.1",
+ "@smithy/util-base64": "^4.1.0",
+ "@smithy/util-body-length-browser": "^4.1.0",
+ "@smithy/util-body-length-node": "^4.1.0",
+ "@smithy/util-defaults-mode-browser": "^4.1.3",
+ "@smithy/util-defaults-mode-node": "^4.1.3",
+ "@smithy/util-endpoints": "^3.1.2",
+ "@smithy/util-middleware": "^4.1.1",
+ "@smithy/util-retry": "^4.1.2",
+ "@smithy/util-utf8": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sso": {
+ "version": "3.895.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.895.0.tgz",
+ "integrity": "sha512-AQHk6iJrwce/NwZa5/Njy0ZGoHdxWCajkgufhXk53L0kRiC3vUPPWEV1m1F3etQWhaUsatcO2xtRuKvLpe4zgA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.894.0",
+ "@aws-sdk/middleware-host-header": "3.893.0",
+ "@aws-sdk/middleware-logger": "3.893.0",
+ "@aws-sdk/middleware-recursion-detection": "3.893.0",
+ "@aws-sdk/middleware-user-agent": "3.895.0",
+ "@aws-sdk/region-config-resolver": "3.893.0",
+ "@aws-sdk/types": "3.893.0",
+ "@aws-sdk/util-endpoints": "3.895.0",
+ "@aws-sdk/util-user-agent-browser": "3.893.0",
+ "@aws-sdk/util-user-agent-node": "3.895.0",
+ "@smithy/config-resolver": "^4.2.2",
+ "@smithy/core": "^3.11.1",
+ "@smithy/fetch-http-handler": "^5.2.1",
+ "@smithy/hash-node": "^4.1.1",
+ "@smithy/invalid-dependency": "^4.1.1",
+ "@smithy/middleware-content-length": "^4.1.1",
+ "@smithy/middleware-endpoint": "^4.2.3",
+ "@smithy/middleware-retry": "^4.2.4",
+ "@smithy/middleware-serde": "^4.1.1",
+ "@smithy/middleware-stack": "^4.1.1",
+ "@smithy/node-config-provider": "^4.2.2",
+ "@smithy/node-http-handler": "^4.2.1",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/smithy-client": "^4.6.3",
+ "@smithy/types": "^4.5.0",
+ "@smithy/url-parser": "^4.1.1",
+ "@smithy/util-base64": "^4.1.0",
+ "@smithy/util-body-length-browser": "^4.1.0",
+ "@smithy/util-body-length-node": "^4.1.0",
+ "@smithy/util-defaults-mode-browser": "^4.1.3",
+ "@smithy/util-defaults-mode-node": "^4.1.3",
+ "@smithy/util-endpoints": "^3.1.2",
+ "@smithy/util-middleware": "^4.1.1",
+ "@smithy/util-retry": "^4.1.2",
+ "@smithy/util-utf8": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/core": {
+ "version": "3.894.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.894.0.tgz",
+ "integrity": "sha512-7zbO31NV2FaocmMtWOg/fuTk3PC2Ji2AC0Fi2KqrppEDIcwLlTTuT9w/rdu/93Pz+wyUhCxWnDc0tPbwtCLs+A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.893.0",
+ "@aws-sdk/xml-builder": "3.894.0",
+ "@smithy/core": "^3.11.1",
+ "@smithy/node-config-provider": "^4.2.2",
+ "@smithy/property-provider": "^4.1.1",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/signature-v4": "^5.2.1",
+ "@smithy/smithy-client": "^4.6.3",
+ "@smithy/types": "^4.5.0",
+ "@smithy/util-base64": "^4.1.0",
+ "@smithy/util-body-length-browser": "^4.1.0",
+ "@smithy/util-middleware": "^4.1.1",
+ "@smithy/util-utf8": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-env": {
+ "version": "3.894.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.894.0.tgz",
+ "integrity": "sha512-2aiQJIRWOuROPPISKgzQnH/HqSfucdk5z5VMemVH3Mm2EYOrzBwmmiiFpmSMN3ST+sE8c7gusqycUchP+KfALQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.894.0",
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/property-provider": "^4.1.1",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-http": {
+ "version": "3.894.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.894.0.tgz",
+ "integrity": "sha512-Z5QQpqFRflszrT+lUq6+ORuu4jRDcpgCUSoTtlhczidMqfdOSckKmK3chZEfmUUJPSwoFQZ7EiVTsX3c886fBg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.894.0",
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/fetch-http-handler": "^5.2.1",
+ "@smithy/node-http-handler": "^4.2.1",
+ "@smithy/property-provider": "^4.1.1",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/smithy-client": "^4.6.3",
+ "@smithy/types": "^4.5.0",
+ "@smithy/util-stream": "^4.3.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-ini": {
+ "version": "3.895.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.895.0.tgz",
+ "integrity": "sha512-uIh7N4IN/yIk+qYMAkVpVkjhB90SGKSfaXEVcnmxzBDG6e5304HKT0esqoCVZvtFfLKasjm2TOpalM5l3fi/dA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.894.0",
+ "@aws-sdk/credential-provider-env": "3.894.0",
+ "@aws-sdk/credential-provider-http": "3.894.0",
+ "@aws-sdk/credential-provider-process": "3.894.0",
+ "@aws-sdk/credential-provider-sso": "3.895.0",
+ "@aws-sdk/credential-provider-web-identity": "3.895.0",
+ "@aws-sdk/nested-clients": "3.895.0",
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/credential-provider-imds": "^4.1.2",
+ "@smithy/property-provider": "^4.1.1",
+ "@smithy/shared-ini-file-loader": "^4.2.0",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-node": {
+ "version": "3.895.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.895.0.tgz",
+ "integrity": "sha512-7xsBCmkBUz+2sNqNsDJ1uyQsBvwhNFzwFt8wX39WrFJTpTQh3uNQ5g8QH21BbkKqIFKCLdvgHgwt3Ub5RGVuPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "3.894.0",
+ "@aws-sdk/credential-provider-http": "3.894.0",
+ "@aws-sdk/credential-provider-ini": "3.895.0",
+ "@aws-sdk/credential-provider-process": "3.894.0",
+ "@aws-sdk/credential-provider-sso": "3.895.0",
+ "@aws-sdk/credential-provider-web-identity": "3.895.0",
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/credential-provider-imds": "^4.1.2",
+ "@smithy/property-provider": "^4.1.1",
+ "@smithy/shared-ini-file-loader": "^4.2.0",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-process": {
+ "version": "3.894.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.894.0.tgz",
+ "integrity": "sha512-VU74GNsj+SsO+pl4d+JimlQ7+AcderZaC6bFndQssQdFZ5NRad8yFNz5Xbec8CPJr+z/VAwHib6431F5nYF46g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.894.0",
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/property-provider": "^4.1.1",
+ "@smithy/shared-ini-file-loader": "^4.2.0",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-sso": {
+ "version": "3.895.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.895.0.tgz",
+ "integrity": "sha512-bZCcHUZGz+XlCaK0KEOHGHkMtlwIvnpxJvlZtSCVaBdX/IgouxaB42fxChflxSMRWF45ygdezfky4i17f6vC4w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/client-sso": "3.895.0",
+ "@aws-sdk/core": "3.894.0",
+ "@aws-sdk/token-providers": "3.895.0",
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/property-provider": "^4.1.1",
+ "@smithy/shared-ini-file-loader": "^4.2.0",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-web-identity": {
+ "version": "3.895.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.895.0.tgz",
+ "integrity": "sha512-tKbXbOp2xrL02fxKvB7ko1E4Uvyy5TF9qi5pT2MVWNnfSsBlUM80aJ6tyUPKWXdUTdAlPrU3XcwgQl/DnnRa9A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.894.0",
+ "@aws-sdk/nested-clients": "3.895.0",
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/property-provider": "^4.1.1",
+ "@smithy/shared-ini-file-loader": "^4.2.0",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-host-header": {
+ "version": "3.893.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.893.0.tgz",
+ "integrity": "sha512-qL5xYRt80ahDfj9nDYLhpCNkDinEXvjLe/Qen/Y/u12+djrR2MB4DRa6mzBCkLkdXDtf0WAoW2EZsNCfGrmOEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-logger": {
+ "version": "3.893.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.893.0.tgz",
+ "integrity": "sha512-ZqzMecjju5zkBquSIfVfCORI/3Mge21nUY4nWaGQy+NUXehqCGG4W7AiVpiHGOcY2cGJa7xeEkYcr2E2U9U0AA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-recursion-detection": {
+ "version": "3.893.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.893.0.tgz",
+ "integrity": "sha512-H7Zotd9zUHQAr/wr3bcWHULYhEeoQrF54artgsoUGIf/9emv6LzY89QUccKIxYd6oHKNTrTyXm9F0ZZrzXNxlg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.893.0",
+ "@aws/lambda-invoke-store": "^0.0.1",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-sdk-s3": {
+ "version": "3.894.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.894.0.tgz",
+ "integrity": "sha512-0C3lTVdTuv5CkJ4LulpA7FmGFSKrGUKxnFZ6+qGjYjNzbdiHXfq0TyEBiDmVqDkoV2k4AT2H/m0Xw//rTkcNEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.894.0",
+ "@aws-sdk/types": "3.893.0",
+ "@aws-sdk/util-arn-parser": "3.893.0",
+ "@smithy/core": "^3.11.1",
+ "@smithy/node-config-provider": "^4.2.2",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/signature-v4": "^5.2.1",
+ "@smithy/smithy-client": "^4.6.3",
+ "@smithy/types": "^4.5.0",
+ "@smithy/util-config-provider": "^4.1.0",
+ "@smithy/util-middleware": "^4.1.1",
+ "@smithy/util-stream": "^4.3.2",
+ "@smithy/util-utf8": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-user-agent": {
+ "version": "3.895.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.895.0.tgz",
+ "integrity": "sha512-JUqQW2RPp4I95wZ/Im9fTiaX3DF55oJgeoiNlLdHkQZPSNNS/pT1WMWMReSvJdcfSNU3xSUaLtI+h4mQjQUDbQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.894.0",
+ "@aws-sdk/types": "3.893.0",
+ "@aws-sdk/util-endpoints": "3.895.0",
+ "@smithy/core": "^3.11.1",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients": {
+ "version": "3.895.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.895.0.tgz",
+ "integrity": "sha512-8w1ihfYgvds6kfal/qJXQQrHRsKYh2nujSyzWMo2TMKMze9WPZA93G4mRbRtKtbSuQ66mVWePH8Cksq35ABu2Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.894.0",
+ "@aws-sdk/middleware-host-header": "3.893.0",
+ "@aws-sdk/middleware-logger": "3.893.0",
+ "@aws-sdk/middleware-recursion-detection": "3.893.0",
+ "@aws-sdk/middleware-user-agent": "3.895.0",
+ "@aws-sdk/region-config-resolver": "3.893.0",
+ "@aws-sdk/types": "3.893.0",
+ "@aws-sdk/util-endpoints": "3.895.0",
+ "@aws-sdk/util-user-agent-browser": "3.893.0",
+ "@aws-sdk/util-user-agent-node": "3.895.0",
+ "@smithy/config-resolver": "^4.2.2",
+ "@smithy/core": "^3.11.1",
+ "@smithy/fetch-http-handler": "^5.2.1",
+ "@smithy/hash-node": "^4.1.1",
+ "@smithy/invalid-dependency": "^4.1.1",
+ "@smithy/middleware-content-length": "^4.1.1",
+ "@smithy/middleware-endpoint": "^4.2.3",
+ "@smithy/middleware-retry": "^4.2.4",
+ "@smithy/middleware-serde": "^4.1.1",
+ "@smithy/middleware-stack": "^4.1.1",
+ "@smithy/node-config-provider": "^4.2.2",
+ "@smithy/node-http-handler": "^4.2.1",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/smithy-client": "^4.6.3",
+ "@smithy/types": "^4.5.0",
+ "@smithy/url-parser": "^4.1.1",
+ "@smithy/util-base64": "^4.1.0",
+ "@smithy/util-body-length-browser": "^4.1.0",
+ "@smithy/util-body-length-node": "^4.1.0",
+ "@smithy/util-defaults-mode-browser": "^4.1.3",
+ "@smithy/util-defaults-mode-node": "^4.1.3",
+ "@smithy/util-endpoints": "^3.1.2",
+ "@smithy/util-middleware": "^4.1.1",
+ "@smithy/util-retry": "^4.1.2",
+ "@smithy/util-utf8": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/region-config-resolver": {
+ "version": "3.893.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.893.0.tgz",
+ "integrity": "sha512-/cJvh3Zsa+Of0Zbg7vl9wp/kZtdb40yk/2+XcroAMVPO9hPvmS9r/UOm6tO7FeX4TtkRFwWaQJiTZTgSdsPY+Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/node-config-provider": "^4.2.2",
+ "@smithy/types": "^4.5.0",
+ "@smithy/util-config-provider": "^4.1.0",
+ "@smithy/util-middleware": "^4.1.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.894.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.894.0.tgz",
+ "integrity": "sha512-Te5b3fSbatkZrh3eYNmpOadZFKsCLNSwiolQKQeEeKHxdnqORwYXa+0ypcTHle6ukic+tFRRd9n3NuMVo9uiVg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-sdk-s3": "3.894.0",
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/signature-v4": "^5.2.1",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/token-providers": {
+ "version": "3.895.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.895.0.tgz",
+ "integrity": "sha512-vJqrEHFFGRZ3ok5T+jII00sa2DQ3HdVkTBIfM0DcrcPssqDV18VKdA767qiBdIEN/cygjdBg8Ri/cuq6ER9BeQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.894.0",
+ "@aws-sdk/nested-clients": "3.895.0",
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/property-provider": "^4.1.1",
+ "@smithy/shared-ini-file-loader": "^4.2.0",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/types": {
+ "version": "3.893.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.893.0.tgz",
+ "integrity": "sha512-Aht1nn5SnA0N+Tjv0dzhAY7CQbxVtmq1bBR6xI0MhG7p2XYVh1wXuKTzrldEvQWwA3odOYunAfT9aBiKZx9qIg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-arn-parser": {
+ "version": "3.893.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz",
+ "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.895.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.895.0.tgz",
+ "integrity": "sha512-MhxBvWbwxmKknuggO2NeMwOVkHOYL98pZ+1ZRI5YwckoCL3AvISMnPJgfN60ww6AIXHGpkp+HhpFdKOe8RHSEg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/types": "^4.5.0",
+ "@smithy/url-parser": "^4.1.1",
+ "@smithy/util-endpoints": "^3.1.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-locate-window": {
+ "version": "3.893.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz",
+ "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-user-agent-browser": {
+ "version": "3.893.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.893.0.tgz",
+ "integrity": "sha512-PE9NtbDBW6Kgl1bG6A5fF3EPo168tnkj8TgMcT0sg4xYBWsBpq0bpJZRh+Jm5Bkwiw9IgTCLjEU7mR6xWaMB9w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/types": "^4.5.0",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-sdk/util-user-agent-node": {
+ "version": "3.895.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.895.0.tgz",
+ "integrity": "sha512-lLRC7BAFOPtJk4cZC0Q0MZBMCGF109QpGnug3L3n/2TJW02Sinz9lzA0ykBpYXe9j60LjIYSENCg+F4DZE5vxg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-user-agent": "3.895.0",
+ "@aws-sdk/types": "3.893.0",
+ "@smithy/node-config-provider": "^4.2.2",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "aws-crt": ">=1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "aws-crt": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@aws-sdk/xml-builder": {
+ "version": "3.894.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.894.0.tgz",
+ "integrity": "sha512-E6EAMc9dT1a2DOdo4zyOf3fp5+NJ2wI+mcm7RaW1baFIWDwcb99PpvWoV7YEiK7oaBDshuOEGWKUSYXdW+JYgA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.5.0",
+ "fast-xml-parser": "5.2.5",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/xml-builder/node_modules/fast-xml-parser": {
+ "version": "5.2.5",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
+ "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "strnum": "^2.1.0"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
+ "node_modules/@aws/lambda-invoke-store": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz",
+ "integrity": "sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -258,6 +1009,448 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
+ "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz",
+ "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz",
+ "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz",
+ "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz",
+ "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz",
+ "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz",
+ "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz",
+ "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz",
+ "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz",
+ "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz",
+ "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz",
+ "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz",
+ "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz",
+ "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz",
+ "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz",
+ "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz",
+ "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz",
+ "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz",
+ "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz",
+ "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz",
+ "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz",
+ "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz",
+ "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz",
@@ -1211,6 +2404,626 @@
"dev": true,
"license": "(Unlicense OR Apache-2.0)"
},
+ "node_modules/@smithy/abort-controller": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.1.1.tgz",
+ "integrity": "sha512-vkzula+IwRvPR6oKQhMYioM3A/oX/lFCZiwuxkQbRhqJS2S4YRY2k7k/SyR2jMf3607HLtbEwlRxi0ndXHMjRg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/config-resolver": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.2.2.tgz",
+ "integrity": "sha512-IT6MatgBWagLybZl1xQcURXRICvqz1z3APSCAI9IqdvfCkrA7RaQIEfgC6G/KvfxnDfQUDqFV+ZlixcuFznGBQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.2.2",
+ "@smithy/types": "^4.5.0",
+ "@smithy/util-config-provider": "^4.1.0",
+ "@smithy/util-middleware": "^4.1.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/core": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.12.0.tgz",
+ "integrity": "sha512-zJeAgogZfbwlPGL93y4Z/XNeIN37YCreRUd6YMIRvaq+6RnBK8PPYYIQ85Is/GglPh3kNImD5riDCXbVSDpCiQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/middleware-serde": "^4.1.1",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/types": "^4.5.0",
+ "@smithy/util-base64": "^4.1.0",
+ "@smithy/util-body-length-browser": "^4.1.0",
+ "@smithy/util-middleware": "^4.1.1",
+ "@smithy/util-stream": "^4.3.2",
+ "@smithy/util-utf8": "^4.1.0",
+ "@smithy/uuid": "^1.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/credential-provider-imds": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.1.2.tgz",
+ "integrity": "sha512-JlYNq8TShnqCLg0h+afqe2wLAwZpuoSgOyzhYvTgbiKBWRov+uUve+vrZEQO6lkdLOWPh7gK5dtb9dS+KGendg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.2.2",
+ "@smithy/property-provider": "^4.1.1",
+ "@smithy/types": "^4.5.0",
+ "@smithy/url-parser": "^4.1.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/fetch-http-handler": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.2.1.tgz",
+ "integrity": "sha512-5/3wxKNtV3wO/hk1is+CZUhL8a1yy/U+9u9LKQ9kZTkMsHaQjJhc3stFfiujtMnkITjzWfndGA2f7g9Uh9vKng==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/querystring-builder": "^4.1.1",
+ "@smithy/types": "^4.5.0",
+ "@smithy/util-base64": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/hash-node": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.1.1.tgz",
+ "integrity": "sha512-H9DIU9WBLhYrvPs9v4sYvnZ1PiAI0oc8CgNQUJ1rpN3pP7QADbTOUjchI2FB764Ub0DstH5xbTqcMJu1pnVqxA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.5.0",
+ "@smithy/util-buffer-from": "^4.1.0",
+ "@smithy/util-utf8": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/invalid-dependency": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.1.1.tgz",
+ "integrity": "sha512-1AqLyFlfrrDkyES8uhINRlJXmHA2FkG+3DY8X+rmLSqmFwk3DJnvhyGzyByPyewh2jbmV+TYQBEfngQax8IFGg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/is-array-buffer": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.1.0.tgz",
+ "integrity": "sha512-ePTYUOV54wMogio+he4pBybe8fwg4sDvEVDBU8ZlHOZXbXK3/C0XfJgUCu6qAZcawv05ZhZzODGUerFBPsPUDQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-content-length": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.1.1.tgz",
+ "integrity": "sha512-9wlfBBgTsRvC2JxLJxv4xDGNBrZuio3AgSl0lSFX7fneW2cGskXTYpFxCdRYD2+5yzmsiTuaAJD1Wp7gWt9y9w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-endpoint": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.2.4.tgz",
+ "integrity": "sha512-FZ4hzupOmthm8Q8ujYrd0I+/MHwVMuSTdkDtIQE0xVuvJt9pLT6Q+b0p4/t+slDyrpcf+Wj7SN+ZqT5OryaaZg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.12.0",
+ "@smithy/middleware-serde": "^4.1.1",
+ "@smithy/node-config-provider": "^4.2.2",
+ "@smithy/shared-ini-file-loader": "^4.2.0",
+ "@smithy/types": "^4.5.0",
+ "@smithy/url-parser": "^4.1.1",
+ "@smithy/util-middleware": "^4.1.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-retry": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.3.0.tgz",
+ "integrity": "sha512-qhEX9745fAxZvtLM4bQJAVC98elWjiMO2OiHl1s6p7hUzS4QfZO1gXUYNwEK8m0J6NoCD5W52ggWxbIDHI0XSg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.2.2",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/service-error-classification": "^4.1.2",
+ "@smithy/smithy-client": "^4.6.4",
+ "@smithy/types": "^4.5.0",
+ "@smithy/util-middleware": "^4.1.1",
+ "@smithy/util-retry": "^4.1.2",
+ "@smithy/uuid": "^1.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-serde": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.1.1.tgz",
+ "integrity": "sha512-lh48uQdbCoj619kRouev5XbWhCwRKLmphAif16c4J6JgJ4uXjub1PI6RL38d3BLliUvSso6klyB/LTNpWSNIyg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-stack": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.1.1.tgz",
+ "integrity": "sha512-ygRnniqNcDhHzs6QAPIdia26M7e7z9gpkIMUe/pK0RsrQ7i5MblwxY8078/QCnGq6AmlUUWgljK2HlelsKIb/A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/node-config-provider": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.2.2.tgz",
+ "integrity": "sha512-SYGTKyPvyCfEzIN5rD8q/bYaOPZprYUPD2f5g9M7OjaYupWOoQFYJ5ho+0wvxIRf471i2SR4GoiZ2r94Jq9h6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/property-provider": "^4.1.1",
+ "@smithy/shared-ini-file-loader": "^4.2.0",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/node-http-handler": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.2.1.tgz",
+ "integrity": "sha512-REyybygHlxo3TJICPF89N2pMQSf+p+tBJqpVe1+77Cfi9HBPReNjTgtZ1Vg73exq24vkqJskKDpfF74reXjxfw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/abort-controller": "^4.1.1",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/querystring-builder": "^4.1.1",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/property-provider": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.1.1.tgz",
+ "integrity": "sha512-gm3ZS7DHxUbzC2wr8MUCsAabyiXY0gaj3ROWnhSx/9sPMc6eYLMM4rX81w1zsMaObj2Lq3PZtNCC1J6lpEY7zg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/protocol-http": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.2.1.tgz",
+ "integrity": "sha512-T8SlkLYCwfT/6m33SIU/JOVGNwoelkrvGjFKDSDtVvAXj/9gOT78JVJEas5a+ETjOu4SVvpCstKgd0PxSu/aHw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/querystring-builder": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.1.1.tgz",
+ "integrity": "sha512-J9b55bfimP4z/Jg1gNo+AT84hr90p716/nvxDkPGCD4W70MPms0h8KF50RDRgBGZeL83/u59DWNqJv6tEP/DHA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.5.0",
+ "@smithy/util-uri-escape": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/querystring-parser": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.1.1.tgz",
+ "integrity": "sha512-63TEp92YFz0oQ7Pj9IuI3IgnprP92LrZtRAkE3c6wLWJxfy/yOPRt39IOKerVr0JS770olzl0kGafXlAXZ1vng==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/service-error-classification": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.1.2.tgz",
+ "integrity": "sha512-Kqd8wyfmBWHZNppZSMfrQFpc3M9Y/kjyN8n8P4DqJJtuwgK1H914R471HTw7+RL+T7+kI1f1gOnL7Vb5z9+NgQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.5.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/shared-ini-file-loader": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.2.0.tgz",
+ "integrity": "sha512-OQTfmIEp2LLuWdxa8nEEPhZmiOREO6bcB6pjs0AySf4yiZhl6kMOfqmcwcY8BaBPX+0Tb+tG7/Ia/6mwpoZ7Pw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/signature-v4": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.2.1.tgz",
+ "integrity": "sha512-M9rZhWQLjlQVCCR37cSjHfhriGRN+FQ8UfgrYNufv66TJgk+acaggShl3KS5U/ssxivvZLlnj7QH2CUOKlxPyA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^4.1.0",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/types": "^4.5.0",
+ "@smithy/util-hex-encoding": "^4.1.0",
+ "@smithy/util-middleware": "^4.1.1",
+ "@smithy/util-uri-escape": "^4.1.0",
+ "@smithy/util-utf8": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/smithy-client": {
+ "version": "4.6.4",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.6.4.tgz",
+ "integrity": "sha512-qL7O3VDyfzCSN9r+sdbQXGhaHtrfSJL30En6Jboj0I3bobf2g1/T0eP2L4qxqrEW26gWhJ4THI4ElVVLjYyBHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.12.0",
+ "@smithy/middleware-endpoint": "^4.2.4",
+ "@smithy/middleware-stack": "^4.1.1",
+ "@smithy/protocol-http": "^5.2.1",
+ "@smithy/types": "^4.5.0",
+ "@smithy/util-stream": "^4.3.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/types": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.5.0.tgz",
+ "integrity": "sha512-RkUpIOsVlAwUIZXO1dsz8Zm+N72LClFfsNqf173catVlvRZiwPy0x2u0JLEA4byreOPKDZPGjmPDylMoP8ZJRg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/url-parser": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.1.1.tgz",
+ "integrity": "sha512-bx32FUpkhcaKlEoOMbScvc93isaSiRM75pQ5IgIBaMkT7qMlIibpPRONyx/0CvrXHzJLpOn/u6YiDX2hcvs7Dg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/querystring-parser": "^4.1.1",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-base64": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.1.0.tgz",
+ "integrity": "sha512-RUGd4wNb8GeW7xk+AY5ghGnIwM96V0l2uzvs/uVHf+tIuVX2WSvynk5CxNoBCsM2rQRSZElAo9rt3G5mJ/gktQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^4.1.0",
+ "@smithy/util-utf8": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-body-length-browser": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.1.0.tgz",
+ "integrity": "sha512-V2E2Iez+bo6bUMOTENPr6eEmepdY8Hbs+Uc1vkDKgKNA/brTJqOW/ai3JO1BGj9GbCeLqw90pbbH7HFQyFotGQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-body-length-node": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.1.0.tgz",
+ "integrity": "sha512-BOI5dYjheZdgR9XiEM3HJcEMCXSoqbzu7CzIgYrx0UtmvtC3tC2iDGpJLsSRFffUpy8ymsg2ARMP5fR8mtuUQQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-buffer-from": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.1.0.tgz",
+ "integrity": "sha512-N6yXcjfe/E+xKEccWEKzK6M+crMrlwaCepKja0pNnlSkm6SjAeLKKA++er5Ba0I17gvKfN/ThV+ZOx/CntKTVw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-config-provider": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.1.0.tgz",
+ "integrity": "sha512-swXz2vMjrP1ZusZWVTB/ai5gK+J8U0BWvP10v9fpcFvg+Xi/87LHvHfst2IgCs1i0v4qFZfGwCmeD/KNCdJZbQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-defaults-mode-browser": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.1.4.tgz",
+ "integrity": "sha512-mLDJ1s4eA3vwOGaQOEPlg5LB4LdZUUMpB5UMOMofeGhWqiS7WR7dTpLiNi9zVn+YziKUd3Af5NLfxDs7NJqmIw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/property-provider": "^4.1.1",
+ "@smithy/smithy-client": "^4.6.4",
+ "@smithy/types": "^4.5.0",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-defaults-mode-node": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.1.4.tgz",
+ "integrity": "sha512-pjX2iMTcOASaSanAd7bu6i3fcMMezr3NTr8Rh64etB0uHRZi+Aw86DoCxPESjY4UTIuA06hhqtTtw95o//imYA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/config-resolver": "^4.2.2",
+ "@smithy/credential-provider-imds": "^4.1.2",
+ "@smithy/node-config-provider": "^4.2.2",
+ "@smithy/property-provider": "^4.1.1",
+ "@smithy/smithy-client": "^4.6.4",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-endpoints": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.1.2.tgz",
+ "integrity": "sha512-+AJsaaEGb5ySvf1SKMRrPZdYHRYSzMkCoK16jWnIMpREAnflVspMIDeCVSZJuj+5muZfgGpNpijE3mUNtjv01Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.2.2",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-hex-encoding": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.1.0.tgz",
+ "integrity": "sha512-1LcueNN5GYC4tr8mo14yVYbh/Ur8jHhWOxniZXii+1+ePiIbsLZ5fEI0QQGtbRRP5mOhmooos+rLmVASGGoq5w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-middleware": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.1.1.tgz",
+ "integrity": "sha512-CGmZ72mL29VMfESz7S6dekqzCh8ZISj3B+w0g1hZFXaOjGTVaSqfAEFAq8EGp8fUL+Q2l8aqNmt8U1tglTikeg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-retry": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.1.2.tgz",
+ "integrity": "sha512-NCgr1d0/EdeP6U5PSZ9Uv5SMR5XRRYoVr1kRVtKZxWL3tixEL3UatrPIMFZSKwHlCcp2zPLDvMubVDULRqeunA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/service-error-classification": "^4.1.2",
+ "@smithy/types": "^4.5.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-stream": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.3.2.tgz",
+ "integrity": "sha512-Ka+FA2UCC/Q1dEqUanCdpqwxOFdf5Dg2VXtPtB1qxLcSGh5C1HdzklIt18xL504Wiy9nNUKwDMRTVCbKGoK69g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/fetch-http-handler": "^5.2.1",
+ "@smithy/node-http-handler": "^4.2.1",
+ "@smithy/types": "^4.5.0",
+ "@smithy/util-base64": "^4.1.0",
+ "@smithy/util-buffer-from": "^4.1.0",
+ "@smithy/util-hex-encoding": "^4.1.0",
+ "@smithy/util-utf8": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-uri-escape": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.1.0.tgz",
+ "integrity": "sha512-b0EFQkq35K5NHUYxU72JuoheM6+pytEVUGlTwiFxWFpmddA+Bpz3LgsPRIpBk8lnPE47yT7AF2Egc3jVnKLuPg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-utf8": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.1.0.tgz",
+ "integrity": "sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^4.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/uuid": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.0.0.tgz",
+ "integrity": "sha512-OlA/yZHh0ekYFnbUkmYBDQPE6fGfdrvgz39ktp8Xf+FA6BfxLejPTMDOG0Nfk5/rDySAz1dRbFf24zaAFYVXlQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@szmarczak/http-timer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
@@ -1234,6 +3047,27 @@
"node": ">=10.13.0"
}
},
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1241,12 +3075,44 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/express": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz",
+ "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^5.0.0",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz",
+ "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
"node_modules/@types/http-cache-semantics": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"license": "MIT"
},
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
@@ -1261,6 +3127,31 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/jsonwebtoken": {
+ "version": "9.0.10",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
+ "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "24.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
@@ -1270,12 +3161,67 @@
"undici-types": "~7.10.0"
}
},
+ "node_modules/@types/nodemailer": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.1.tgz",
+ "integrity": "sha512-UfHAghPmGZVzaL8x9y+mKZMWyHC399+iq0MOmya5tIyenWX3lcdSb60vOmp0DocR6gCDTYTozv/ULQnREyyjkg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@aws-sdk/client-sesv2": "^3.839.0",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/ping": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/@types/ping/-/ping-0.4.4.tgz",
+ "integrity": "sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/relateurl": {
"version": "0.2.33",
"resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz",
"integrity": "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==",
"license": "MIT"
},
+ "node_modules/@types/send": {
+ "version": "0.17.5",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
+ "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.8",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz",
+ "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "*"
+ }
+ },
"node_modules/@types/triple-beam": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
@@ -1629,6 +3575,13 @@
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
+ "node_modules/bowser": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz",
+ "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -3163,6 +5116,48 @@
"node": ">= 0.4"
}
},
+ "node_modules/esbuild": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
+ "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.10",
+ "@esbuild/android-arm": "0.25.10",
+ "@esbuild/android-arm64": "0.25.10",
+ "@esbuild/android-x64": "0.25.10",
+ "@esbuild/darwin-arm64": "0.25.10",
+ "@esbuild/darwin-x64": "0.25.10",
+ "@esbuild/freebsd-arm64": "0.25.10",
+ "@esbuild/freebsd-x64": "0.25.10",
+ "@esbuild/linux-arm": "0.25.10",
+ "@esbuild/linux-arm64": "0.25.10",
+ "@esbuild/linux-ia32": "0.25.10",
+ "@esbuild/linux-loong64": "0.25.10",
+ "@esbuild/linux-mips64el": "0.25.10",
+ "@esbuild/linux-ppc64": "0.25.10",
+ "@esbuild/linux-riscv64": "0.25.10",
+ "@esbuild/linux-s390x": "0.25.10",
+ "@esbuild/linux-x64": "0.25.10",
+ "@esbuild/netbsd-arm64": "0.25.10",
+ "@esbuild/netbsd-x64": "0.25.10",
+ "@esbuild/openbsd-arm64": "0.25.10",
+ "@esbuild/openbsd-x64": "0.25.10",
+ "@esbuild/openharmony-arm64": "0.25.10",
+ "@esbuild/sunos-x64": "0.25.10",
+ "@esbuild/win32-arm64": "0.25.10",
+ "@esbuild/win32-ia32": "0.25.10",
+ "@esbuild/win32-x64": "0.25.10"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -4079,6 +6074,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/get-tsconfig": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
+ "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -7578,6 +9586,16 @@
"node": ">=4"
}
},
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
"node_modules/responselike": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz",
@@ -8551,6 +10569,26 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
+ "node_modules/tsx": {
+ "version": "4.20.5",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz",
+ "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.25.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
@@ -8611,6 +10649,20 @@
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
+ "node_modules/typescript": {
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
diff --git a/server/package.json b/server/package.json
index d879776a8..3bd79722a 100755
--- a/server/package.json
+++ b/server/package.json
@@ -6,7 +6,9 @@
"type": "module",
"scripts": {
"test": "c8 mocha",
- "dev": "nodemon src/index.js",
+ "dev": "nodemon --exec tsx src/index.js",
+ "start": "nodemon ./dist/index.js",
+ "build": "tsc",
"lint": "eslint .",
"lint-fix": "eslint --fix .",
"format": "prettier --write .",
@@ -52,6 +54,10 @@
},
"devDependencies": {
"@eslint/js": "^9.17.0",
+ "@types/express": "5.0.3",
+ "@types/jsonwebtoken": "9.0.10",
+ "@types/nodemailer": "7.0.1",
+ "@types/ping": "0.4.4",
"c8": "10.1.3",
"chai": "5.2.0",
"eslint": "^9.17.0",
@@ -61,6 +67,8 @@
"mocha": "11.1.0",
"nodemon": "3.1.9",
"prettier": "^3.3.3",
- "sinon": "19.0.2"
+ "sinon": "19.0.2",
+ "tsx": "4.20.5",
+ "typescript": "5.9.2"
}
}
diff --git a/server/src/app.js b/server/src/app.js
index 9cabda6b6..4578de896 100644
--- a/server/src/app.js
+++ b/server/src/app.js
@@ -1,16 +1,16 @@
import express from "express";
import path from "path";
-import { responseHandler } from "./middleware/responseHandler.js";
+import { responseHandler } from "./middleware/v1/responseHandler.js";
import cors from "cors";
import helmet from "helmet";
import compression from "compression";
import cookieParser from "cookie-parser";
-import languageMiddleware from "./middleware/languageMiddleware.js";
+import languageMiddleware from "./middleware/v1/languageMiddleware.js";
import swaggerUi from "swagger-ui-express";
-import { handleErrors } from "./middleware/handleErrors.js";
+import { handleErrors } from "./middleware/v1/handleErrors.js";
import { setupRoutes } from "./config/routes.js";
-import { generalApiLimiter } from "./middleware/rateLimiter.js";
-import { sanitizeBody, sanitizeQuery } from "./middleware/sanitization.js";
+import { generalApiLimiter } from "./middleware/v1/rateLimiter.js";
+import { sanitizeBody, sanitizeQuery } from "./middleware/v1/sanitization.js";
export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => {
const allowedOrigin = envSettings.clientHost;
@@ -27,7 +27,7 @@ export const createApp = ({ services, controllers, envSettings, frontendPath, op
cors({
origin: allowedOrigin,
methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS",
- allowedHeaders: "*",
+ allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
})
);
diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js
index 693e35ab8..927acd1a2 100644
--- a/server/src/config/controllers.js
+++ b/server/src/config/controllers.js
@@ -1,24 +1,32 @@
-import { createCommonDependencies } from "../controllers/baseController.js";
+import { createCommonDependencies } from "../controllers/v1/baseController.js";
// Services
-// Controllers
-import MonitorController from "../controllers/monitorController.js";
-import AuthController from "../controllers/authController.js";
-import SettingsController from "../controllers/settingsController.js";
-import CheckController from "../controllers/checkController.js";
-import InviteController from "../controllers/inviteController.js";
-import MaintenanceWindowController from "../controllers/maintenanceWindowController.js";
-import QueueController from "../controllers/queueController.js";
-import LogController from "../controllers/logController.js";
-import StatusPageController from "../controllers/statusPageController.js";
-import NotificationController from "../controllers/notificationController.js";
-import DiagnosticController from "../controllers/diagnosticController.js";
+// V1 Controllers
+import MonitorController from "../controllers/v1/monitorController.js";
+import AuthController from "../controllers/v1/authController.js";
+import SettingsController from "../controllers/v1/settingsController.js";
+import CheckController from "../controllers/v1/checkController.js";
+import InviteController from "../controllers/v1/inviteController.js";
+import MaintenanceWindowController from "../controllers/v1/maintenanceWindowController.js";
+import QueueController from "../controllers/v1/queueController.js";
+import LogController from "../controllers/v1/logController.js";
+import StatusPageController from "../controllers/v1/statusPageController.js";
+import NotificationController from "../controllers/v1/notificationController.js";
+import DiagnosticController from "../controllers/v1/diagnosticController.js";
+// V2 Controllers
+import AuthControllerV2 from "../controllers/v2/AuthController.js";
+import InviteControllerV2 from "../controllers/v2/InviteController.js";
+import MaintenanceControllerV2 from "../controllers/v2/MaintenanceController.js";
+import MonitorControllerV2 from "../controllers/v2/MonitorController.js";
+import NotificationChannelControllerV2 from "../controllers/v2/NotificationChannelController.js";
+import QueueControllerV2 from "../controllers/v2/QueueController.js";
export const initializeControllers = (services) => {
const controllers = {};
const commonDependencies = createCommonDependencies(services.db, services.errorService, services.logger, services.stringService);
+ // V1
controllers.authController = new AuthController(commonDependencies, {
settingsService: services.settingsService,
emailService: services.emailService,
@@ -62,5 +70,13 @@ export const initializeControllers = (services) => {
diagnosticService: services.diagnosticService,
});
+ //V2
+ controllers.authControllerV2 = new AuthControllerV2(services.authServiceV2, services.inviteServiceV2);
+ controllers.inviteControllerV2 = new InviteControllerV2(services.inviteServiceV2);
+ controllers.maintenanceControllerV2 = new MaintenanceControllerV2(services.maintenanceServiceV2);
+ controllers.monitorControllerV2 = new MonitorControllerV2(services.monitorServiceV2, services.checkServiceV2);
+ controllers.notificationChannelControllerV2 = new NotificationChannelControllerV2(services.notificationChannelServiceV2);
+ controllers.queueControllerV2 = new QueueControllerV2(services.jobQueueV2);
+
return controllers;
};
diff --git a/server/src/config/index.ts b/server/src/config/index.ts
new file mode 100644
index 000000000..7ace6f99c
--- /dev/null
+++ b/server/src/config/index.ts
@@ -0,0 +1,25 @@
+import dotenv from "dotenv";
+
+export interface IEevConfig {
+ NODE_ENV: string;
+ JWT_SECRET: string;
+ PORT: number;
+ PAGESPEED_API_KEY: string;
+ SMTP_HOST: string;
+ SMTP_PORT: number;
+ SMTP_USER: string;
+ SMTP_PASS: string;
+}
+
+dotenv.config();
+
+export const config: IEevConfig = {
+ NODE_ENV: process.env.NODE_ENV || "development",
+ JWT_SECRET: process.env.JWT_SECRET || "your_jwt_secret",
+ PORT: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
+ PAGESPEED_API_KEY: process.env.PAGESPEED_API_KEY || "",
+ SMTP_HOST: process.env.SMTP_HOST || "smtp.example.com",
+ SMTP_PORT: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587,
+ SMTP_USER: process.env.SMTP_USER || "user@example.com",
+ SMTP_PASS: process.env.SMTP_PASS || "your_smtp_password",
+};
diff --git a/server/src/config/routes.js b/server/src/config/routes.js
index 7056393f9..13b369262 100644
--- a/server/src/config/routes.js
+++ b/server/src/config/routes.js
@@ -1,19 +1,28 @@
-import { verifyJWT } from "../middleware/verifyJWT.js";
-import { authApiLimiter } from "../middleware/rateLimiter.js";
+import { verifyJWT } from "../middleware/v1/verifyJWT.js";
+import { authApiLimiter } from "../middleware/v1/rateLimiter.js";
-import AuthRoutes from "../routes/authRoute.js";
-import InviteRoutes from "../routes/inviteRoute.js";
-import MonitorRoutes from "../routes/monitorRoute.js";
-import CheckRoutes from "../routes/checkRoute.js";
-import SettingsRoutes from "../routes/settingsRoute.js";
-import MaintenanceWindowRoutes from "../routes/maintenanceWindowRoute.js";
-import StatusPageRoutes from "../routes/statusPageRoute.js";
-import QueueRoutes from "../routes/queueRoute.js";
-import LogRoutes from "../routes/logRoutes.js";
-import DiagnosticRoutes from "../routes/diagnosticRoute.js";
-import NotificationRoutes from "../routes/notificationRoute.js";
+import AuthRoutes from "../routes/v1/authRoute.js";
+import InviteRoutes from "../routes/v1//inviteRoute.js";
+import MonitorRoutes from "../routes/v1/monitorRoute.js";
+import CheckRoutes from "../routes/v1/checkRoute.js";
+import SettingsRoutes from "../routes/v1/settingsRoute.js";
+import MaintenanceWindowRoutes from "../routes/v1/maintenanceWindowRoute.js";
+import StatusPageRoutes from "../routes/v1/statusPageRoute.js";
+import QueueRoutes from "../routes/v1/queueRoute.js";
+import LogRoutes from "../routes/v1/logRoutes.js";
+import DiagnosticRoutes from "../routes/v1//diagnosticRoute.js";
+import NotificationRoutes from "../routes/v1/notificationRoute.js";
+
+//V2
+import AuthRoutesV2 from "../routes/v2/auth.js";
+import InviteRoutesV2 from "../routes/v2/invite.js";
+import MaintenanceRoutesV2 from "../routes/v2/maintenance.js";
+import MonitorRoutesV2 from "../routes/v2/monitors.js";
+import NotificationChannelRoutesV2 from "../routes/v2/notificationChannels.js";
+import QueueRoutesV2 from "../routes/v2/queue.js";
export const setupRoutes = (app, controllers) => {
+ // V1
const authRoutes = new AuthRoutes(controllers.authController);
const monitorRoutes = new MonitorRoutes(controllers.monitorController);
const settingsRoutes = new SettingsRoutes(controllers.settingsController);
@@ -37,4 +46,19 @@ export const setupRoutes = (app, controllers) => {
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter());
+
+ // V2
+ const authRoutesV2 = new AuthRoutesV2(controllers.authControllerV2);
+ const inviteRoutesV2 = new InviteRoutesV2(controllers.inviteControllerV2);
+ const maintenanceRoutesV2 = new MaintenanceRoutesV2(controllers.maintenanceControllerV2);
+ const monitorRoutesV2 = new MonitorRoutesV2(controllers.monitorControllerV2);
+ const notificationChannelRoutesV2 = new NotificationChannelRoutesV2(controllers.notificationChannelControllerV2);
+ const queueRoutesV2 = new QueueRoutesV2(controllers.queueControllerV2);
+
+ app.use("/api/v2/auth", authApiLimiter, authRoutesV2.getRouter());
+ app.use("/api/v2/invite", inviteRoutesV2.getRouter());
+ app.use("/api/v2/maintenance", maintenanceRoutesV2.getRouter());
+ app.use("/api/v2/monitors", monitorRoutesV2.getRouter());
+ app.use("/api/v2/notification-channels", notificationChannelRoutesV2.getRouter());
+ app.use("/api/v2/queue", queueRoutesV2.getRouter());
};
diff --git a/server/src/config/services.js b/server/src/config/services.js
index 08c2525f3..c1679868d 100644
--- a/server/src/config/services.js
+++ b/server/src/config/services.js
@@ -1,22 +1,22 @@
-import ServiceRegistry from "../service/system/serviceRegistry.js";
-import TranslationService from "../service/system/translationService.js";
-import StringService from "../service/system/stringService.js";
-import MongoDB from "../db/mongo/MongoDB.js";
-import NetworkService from "../service/infrastructure/networkService.js";
-import EmailService from "../service/infrastructure/emailService.js";
-import BufferService from "../service/infrastructure/bufferService.js";
-import StatusService from "../service/infrastructure/statusService.js";
-import NotificationUtils from "../service/infrastructure/notificationUtils.js";
-import NotificationService from "../service/infrastructure/notificationService.js";
-import ErrorService from "../service/infrastructure/errorService.js";
-import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
-import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
-import UserService from "../service/business/userService.js";
-import CheckService from "../service/business/checkService.js";
-import DiagnosticService from "../service/business/diagnosticService.js";
-import InviteService from "../service/business/inviteService.js";
-import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
-import MonitorService from "../service/business/monitorService.js";
+import ServiceRegistry from "../service/v1/system/serviceRegistry.js";
+import TranslationService from "../service/v1/system/translationService.js";
+import StringService from "../service/v1/system/stringService.js";
+import MongoDB from "../db/v1/MongoDB.js";
+import NetworkService from "../service/v1/infrastructure/networkService.js";
+import EmailService from "../service/v1/infrastructure/emailService.js";
+import BufferService from "../service/v1/infrastructure/bufferService.js";
+import StatusService from "../service/v1/infrastructure/statusService.js";
+import NotificationUtils from "../service/v1/infrastructure/notificationUtils.js";
+import NotificationService from "../service/v1/infrastructure/notificationService.js";
+import ErrorService from "../service/v1/infrastructure/errorService.js";
+import SuperSimpleQueueHelper from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
+import SuperSimpleQueue from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
+import UserService from "../service/v1/business/userService.js";
+import CheckService from "../service/v1/business/checkService.js";
+import DiagnosticService from "../service/v1/business/diagnosticService.js";
+import InviteService from "../service/v1/business/inviteService.js";
+import MaintenanceWindowService from "../service/v1/business/maintenanceWindowService.js";
+import MonitorService from "../service/v1/business/monitorService.js";
import papaparse from "papaparse";
import axios from "axios";
import got from "got";
@@ -46,27 +46,49 @@ import { GenerateAvatarImage } from "../utils/imageProcessing.js";
import { ParseBoolean } from "../utils/utils.js";
// Models
-import Check from "../db/models/Check.js";
-import Monitor from "../db/models/Monitor.js";
-import User from "../db/models/User.js";
-import InviteToken from "../db/models/InviteToken.js";
-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 Notification from "../db/models/Notification.js";
-import RecoveryToken from "../db/models/RecoveryToken.js";
-import AppSettings from "../db/models/AppSettings.js";
+import Check from "../db/v1/models/Check.js";
+import Monitor from "../db/v1/models/Monitor.js";
+import User from "../db/v1/models/User.js";
+import InviteToken from "../db/v1/models/InviteToken.js";
+import StatusPage from "../db/v1/models/StatusPage.js";
+import Team from "../db/v1/models/Team.js";
+import MaintenanceWindow from "../db/v1/models/MaintenanceWindow.js";
+import MonitorStats from "../db/v1/models/MonitorStats.js";
+import Notification from "../db/v1/models/Notification.js";
+import RecoveryToken from "../db/v1/models/RecoveryToken.js";
+import AppSettings from "../db/v1/models/AppSettings.js";
-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 MaintenanceWindowModule from "../db/mongo/modules/maintenanceWindowModule.js";
-import MonitorModule from "../db/mongo/modules/monitorModule.js";
-import NotificationModule from "../db/mongo/modules/notificationModule.js";
-import RecoveryModule from "../db/mongo/modules/recoveryModule.js";
-import SettingsModule from "../db/mongo/modules/settingsModule.js";
+import InviteModule from "../db/v1/modules/inviteModule.js";
+import CheckModule from "../db/v1/modules/checkModule.js";
+import StatusPageModule from "../db/v1/modules/statusPageModule.js";
+import UserModule from "../db/v1/modules/userModule.js";
+import MaintenanceWindowModule from "../db/v1/modules/maintenanceWindowModule.js";
+import MonitorModule from "../db/v1/modules/monitorModule.js";
+import NotificationModule from "../db/v1/modules/notificationModule.js";
+import RecoveryModule from "../db/v1/modules/recoveryModule.js";
+import SettingsModule from "../db/v1/modules/settingsModule.js";
+
+// V2 Business
+import AuthServiceV2 from "../service/v2/business/AuthService.js";
+import CheckServiceV2 from "../service/v2/business/CheckService.js";
+import InviteServiceV2 from "../service/v2/business/InviteService.js";
+import MaintenanceServiceV2 from "../service/v2/business/MaintenanceService.js";
+import MonitorServiceV2 from "../service/v2/business/MonitorService.js";
+import MonitorStatsServiceV2 from "../service/v2/business/MonitorStatsService.js";
+import NotificationChannelServiceV2 from "../service/v2/business/NotificationChannelService.js";
+import QueueServiceV2 from "../service/v2/business/QueueService.js";
+import UserServiceV2 from "../service/v2/business/UserService.js";
+
+// V2 Infra
+import DiscordServiceV2 from "../service/v2/infrastructure/NotificationServices/Discord.js";
+import EmailServiceV2 from "../service/v2/infrastructure/NotificationServices/Email.js";
+import SlackServiceV2 from "../service/v2/infrastructure/NotificationServices/Slack.js";
+import WebhookServiceV2 from "../service/v2/infrastructure/NotificationServices/Webhook.js";
+import JobGeneratorV2 from "../service/v2/infrastructure/JobGenerator.js";
+import JobQueueV2 from "../service/v2/infrastructure/JobQueue.js";
+import NetworkServiceV2 from "../service/v2/infrastructure/NetworkService.js";
+import NotificationServiceV2 from "../service/v2/infrastructure/NotificationService.js";
+import StatusServiceV2 from "../service/v2/infrastructure/StatusService.js";
export const initializeServices = async ({ logger, envSettings, settingsService }) => {
const serviceRegistry = new ServiceRegistry({ logger });
@@ -208,7 +230,35 @@ export const initializeServices = async ({ logger, envSettings, settingsService
games,
});
+ // V2 Services
+ const checkServiceV2 = new CheckServiceV2();
+ const inviteServiceV2 = new InviteServiceV2();
+ const maintenanceServiceV2 = new MaintenanceServiceV2();
+ const monitorStatsServiceV2 = new MonitorStatsServiceV2();
+ const notificationChannelServiceV2 = new NotificationChannelServiceV2();
+ const userServiceV2 = new UserServiceV2();
+ const discordServiceV2 = new DiscordServiceV2();
+ const emailServiceV2 = new EmailServiceV2(userServiceV2);
+ const slackServiceV2 = new SlackServiceV2();
+ const webhookServiceV2 = new WebhookServiceV2();
+ const networkServiceV2 = new NetworkServiceV2();
+ const statusServiceV2 = new StatusServiceV2();
+ const notificationServiceV2 = new NotificationServiceV2(userServiceV2);
+ const jobGeneratorV2 = new JobGeneratorV2(
+ networkServiceV2,
+ checkServiceV2,
+ monitorStatsServiceV2,
+ statusServiceV2,
+ notificationServiceV2,
+ maintenanceServiceV2
+ );
+ const jobQueueV2 = await JobQueueV2.create(jobGeneratorV2);
+ const authServiceV2 = new AuthServiceV2(jobQueueV2);
+ const monitorServiceV2 = new MonitorServiceV2(jobQueueV2);
+ const queueServiceV2 = new QueueServiceV2(jobQueueV2);
+
const services = {
+ //v1
settingsService,
translationService,
stringService,
@@ -227,6 +277,25 @@ export const initializeServices = async ({ logger, envSettings, settingsService
monitorService,
errorService,
logger,
+ //v2
+ jobQueueV2,
+ authServiceV2,
+ checkServiceV2,
+ inviteServiceV2,
+ maintenanceServiceV2,
+ monitorServiceV2,
+ monitorStatsServiceV2,
+ notificationChannelServiceV2,
+ queueServiceV2,
+ userServiceV2,
+ discordServiceV2,
+ emailServiceV2,
+ slackServiceV2,
+ webhookServiceV2,
+ networkServiceV2,
+ statusServiceV2,
+ notificationServiceV2,
+ jobGeneratorV2,
};
Object.values(services).forEach((service) => {
diff --git a/server/src/controllers/announcementsController.js b/server/src/controllers/v1/announcementsController.js
similarity index 96%
rename from server/src/controllers/announcementsController.js
rename to server/src/controllers/v1/announcementsController.js
index 430859791..0425b44aa 100755
--- a/server/src/controllers/announcementsController.js
+++ b/server/src/controllers/v1/announcementsController.js
@@ -1,4 +1,4 @@
-import { createAnnouncementValidation } from "../validation/joi.js";
+import { createAnnouncementValidation } from "../../validation/joi.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "announcementController";
diff --git a/server/src/controllers/authController.js b/server/src/controllers/v1/authController.js
similarity index 95%
rename from server/src/controllers/authController.js
rename to server/src/controllers/v1/authController.js
index c186e8423..e52de3849 100755
--- a/server/src/controllers/authController.js
+++ b/server/src/controllers/v1/authController.js
@@ -10,10 +10,10 @@ import {
editUserByIdParamValidation,
editUserByIdBodyValidation,
editSuperadminUserByIdBodyValidation,
-} from "../validation/joi.js";
+ editUserPasswordByIdBodyValidation,
+} from "../../validation/joi.js";
const SERVICE_NAME = "authController";
-
/**
* Authentication Controller
*
@@ -436,7 +436,7 @@ class AuthController extends BaseController {
async (req, res) => {
const roles = req?.user?.role;
if (!roles.includes("superadmin")) {
- throw createError("Unauthorized", 403);
+ throw this.errorService.createError("Unauthorized", 403);
}
const userId = req.params.userId;
@@ -456,6 +456,23 @@ class AuthController extends BaseController {
SERVICE_NAME,
"editUserById"
);
+ editUserPasswordById = this.asyncHandler(
+ async (req, res) => {
+ const roles = req?.user?.role;
+ if (!roles.includes("superadmin")) {
+ throw this.errorService.createError("Unauthorized", 403);
+ }
+
+ const userId = req.params.userId;
+ await editUserByIdParamValidation.validateAsync(req.params);
+ await editUserPasswordByIdBodyValidation.validateAsync(req.body);
+ const updatedPassword = req.body.password;
+ await this.userService.setPasswordByUserId(userId, updatedPassword);
+ return res.success({ msg: "Password reset successfully" });
+ },
+ SERVICE_NAME,
+ "editUserPasswordById"
+ );
}
export default AuthController;
diff --git a/server/src/controllers/baseController.js b/server/src/controllers/v1/baseController.js
similarity index 97%
rename from server/src/controllers/baseController.js
rename to server/src/controllers/v1/baseController.js
index 910cf57b4..e8612cec5 100644
--- a/server/src/controllers/baseController.js
+++ b/server/src/controllers/v1/baseController.js
@@ -1,4 +1,4 @@
-import { AppError } from "../service/infrastructure/errorService.js";
+import { AppError } from "../../service/v1/infrastructure/errorService.js";
export const createCommonDependencies = (db, errorService, logger, stringService) => {
return {
diff --git a/server/src/controllers/checkController.js b/server/src/controllers/v1/checkController.js
similarity index 99%
rename from server/src/controllers/checkController.js
rename to server/src/controllers/v1/checkController.js
index 4069df3ff..ea293d74d 100755
--- a/server/src/controllers/checkController.js
+++ b/server/src/controllers/v1/checkController.js
@@ -9,7 +9,7 @@ import {
ackCheckBodyValidation,
ackAllChecksParamValidation,
ackAllChecksBodyValidation,
-} from "../validation/joi.js";
+} from "../../validation/joi.js";
const SERVICE_NAME = "checkController";
diff --git a/server/src/controllers/controllerUtils.js b/server/src/controllers/v1/controllerUtils.js
similarity index 100%
rename from server/src/controllers/controllerUtils.js
rename to server/src/controllers/v1/controllerUtils.js
diff --git a/server/src/controllers/diagnosticController.js b/server/src/controllers/v1/diagnosticController.js
similarity index 100%
rename from server/src/controllers/diagnosticController.js
rename to server/src/controllers/v1/diagnosticController.js
diff --git a/server/src/controllers/inviteController.js b/server/src/controllers/v1/inviteController.js
similarity index 99%
rename from server/src/controllers/inviteController.js
rename to server/src/controllers/v1/inviteController.js
index a377234c0..1ec586f31 100755
--- a/server/src/controllers/inviteController.js
+++ b/server/src/controllers/v1/inviteController.js
@@ -1,4 +1,4 @@
-import { inviteBodyValidation, inviteVerificationBodyValidation } from "../validation/joi.js";
+import { inviteBodyValidation, inviteVerificationBodyValidation } from "../../validation/joi.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "inviteController";
diff --git a/server/src/controllers/logController.js b/server/src/controllers/v1/logController.js
similarity index 100%
rename from server/src/controllers/logController.js
rename to server/src/controllers/v1/logController.js
diff --git a/server/src/controllers/maintenanceWindowController.js b/server/src/controllers/v1/maintenanceWindowController.js
old mode 100755
new mode 100644
similarity index 99%
rename from server/src/controllers/maintenanceWindowController.js
rename to server/src/controllers/v1/maintenanceWindowController.js
index 2fdb1ee15..be373ace5
--- a/server/src/controllers/maintenanceWindowController.js
+++ b/server/src/controllers/v1/maintenanceWindowController.js
@@ -6,7 +6,7 @@ import {
getMaintenanceWindowsByMonitorIdParamValidation,
getMaintenanceWindowsByTeamIdQueryValidation,
deleteMaintenanceWindowByIdParamValidation,
-} from "../validation/joi.js";
+} from "../../validation/joi.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "maintenanceWindowController";
diff --git a/server/src/controllers/monitorController.js b/server/src/controllers/v1/monitorController.js
old mode 100755
new mode 100644
similarity index 99%
rename from server/src/controllers/monitorController.js
rename to server/src/controllers/v1/monitorController.js
index 0f40a4a01..462d5a278
--- a/server/src/controllers/monitorController.js
+++ b/server/src/controllers/v1/monitorController.js
@@ -11,7 +11,7 @@ import {
getCertificateParamValidation,
getHardwareDetailsByIdParamValidation,
getHardwareDetailsByIdQueryValidation,
-} from "../validation/joi.js";
+} from "../../validation/joi.js";
import sslChecker from "ssl-checker";
import { fetchMonitorCertificate } from "./controllerUtils.js";
import BaseController from "./baseController.js";
diff --git a/server/src/controllers/notificationController.js b/server/src/controllers/v1/notificationController.js
old mode 100755
new mode 100644
similarity index 98%
rename from server/src/controllers/notificationController.js
rename to server/src/controllers/v1/notificationController.js
index e00dd5b6f..37c2b4e60
--- a/server/src/controllers/notificationController.js
+++ b/server/src/controllers/v1/notificationController.js
@@ -1,4 +1,4 @@
-import { createNotificationBodyValidation } from "../validation/joi.js";
+import { createNotificationBodyValidation } from "../../validation/joi.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "NotificationController";
diff --git a/server/src/controllers/queueController.js b/server/src/controllers/v1/queueController.js
similarity index 100%
rename from server/src/controllers/queueController.js
rename to server/src/controllers/v1/queueController.js
diff --git a/server/src/controllers/settingsController.js b/server/src/controllers/v1/settingsController.js
old mode 100755
new mode 100644
similarity index 95%
rename from server/src/controllers/settingsController.js
rename to server/src/controllers/v1/settingsController.js
index 7da724d23..3db73a05a
--- a/server/src/controllers/settingsController.js
+++ b/server/src/controllers/v1/settingsController.js
@@ -1,5 +1,5 @@
-import { updateAppSettingsBodyValidation } from "../validation/joi.js";
-import { sendTestEmailBodyValidation } from "../validation/joi.js";
+import { updateAppSettingsBodyValidation } from "../../validation/joi.js";
+import { sendTestEmailBodyValidation } from "../../validation/joi.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "SettingsController";
diff --git a/server/src/controllers/statusPageController.js b/server/src/controllers/v1/statusPageController.js
old mode 100755
new mode 100644
similarity index 99%
rename from server/src/controllers/statusPageController.js
rename to server/src/controllers/v1/statusPageController.js
index a46688590..de6a1ca7c
--- a/server/src/controllers/statusPageController.js
+++ b/server/src/controllers/v1/statusPageController.js
@@ -1,4 +1,4 @@
-import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "../validation/joi.js";
+import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "../../validation/joi.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "statusPageController";
diff --git a/server/src/controllers/v2/AuthController.ts b/server/src/controllers/v2/AuthController.ts
new file mode 100644
index 000000000..3afed5312
--- /dev/null
+++ b/server/src/controllers/v2/AuthController.ts
@@ -0,0 +1,150 @@
+import { Request, Response, NextFunction } from "express";
+import { encode, decode } from "../../utils/JWTUtils.js";
+import AuthService from "../../service/v2/business/AuthService.js";
+import ApiError from "../../utils/ApiError.js";
+import InviteService from "../../service/v2/business/InviteService.js";
+import { IInvite } from "../../db/v2/models/index.js";
+
+class AuthController {
+ private authService: AuthService;
+ private inviteService: InviteService;
+ constructor(authService: AuthService, inviteService: InviteService) {
+ this.authService = authService;
+ this.inviteService = inviteService;
+ }
+
+ register = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const { email, firstName, lastName, password } = req.body;
+
+ if (!email || !firstName || !lastName || !password) {
+ throw new Error("Email, firstName, lastName, and password are required");
+ }
+
+ const result = await this.authService.register({
+ email,
+ firstName,
+ lastName,
+ password,
+ });
+
+ const token = encode(result);
+
+ res.cookie("token", token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "strict",
+ maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
+ });
+
+ res.status(201).json({
+ message: "User created successfully",
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ registerWithInvite = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const token = req.params.token;
+ if (!token) {
+ throw new ApiError("Invite token is required", 400);
+ }
+
+ const invite: IInvite = await this.inviteService.get(token);
+
+ const { firstName, lastName, password } = req.body;
+ const email = invite?.email;
+ const roles = invite?.roles;
+
+ if (!email || !firstName || !lastName || !password || !roles || roles.length === 0) {
+ throw new Error("Email, firstName, lastName, password, and roles are required");
+ }
+
+ const result = await this.authService.registerWithInvite({
+ email,
+ firstName,
+ lastName,
+ password,
+ roles,
+ });
+
+ if (!result) {
+ throw new Error("Registration failed");
+ }
+
+ await this.inviteService.delete(invite._id.toString());
+
+ const jwt = encode(result);
+
+ res.cookie("token", jwt, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "strict",
+ maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
+ });
+
+ res.status(201).json({ message: "User created successfully" });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ login = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const { email, password } = req.body;
+ // Validation
+ if (!email || !password) {
+ return res.status(400).json({ message: "Email and password are required" });
+ }
+ const result = await this.authService.login({ email, password });
+
+ const token = encode(result);
+
+ res.cookie("token", token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "strict",
+ maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week
+ });
+
+ res.status(200).json({
+ message: "Login successful",
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ logout = (req: Request, res: Response) => {
+ res.clearCookie("token", {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "strict",
+ });
+ res.status(200).json({ message: "Logout successful" });
+ };
+
+ me = (req: Request, res: Response, next: NextFunction) => {
+ return res.status(200).json({ message: "OK" });
+ };
+
+ cleanup = async (req: Request, res: Response) => {
+ try {
+ await this.authService.cleanup();
+ res.status(200).json({ message: "Cleanup successful" });
+ } catch (error) {}
+ };
+
+ cleanMonitors = async (req: Request, res: Response) => {
+ try {
+ await this.authService.cleanMonitors();
+ res.status(200).json({ message: "Monitors cleanup successful" });
+ } catch (error) {
+ res.status(500).json({ message: "Internal server error" });
+ }
+ };
+}
+
+export default AuthController;
diff --git a/server/src/controllers/v2/InviteController.ts b/server/src/controllers/v2/InviteController.ts
new file mode 100644
index 000000000..4d72bf8d4
--- /dev/null
+++ b/server/src/controllers/v2/InviteController.ts
@@ -0,0 +1,62 @@
+import { Request, Response, NextFunction } from "express";
+import InviteService from "../../service/v2/business/InviteService.js";
+
+class InviteController {
+ private inviteService: InviteService;
+ constructor(inviteService: InviteService) {
+ this.inviteService = inviteService;
+ }
+
+ create = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const tokenizedUser = req.user;
+ if (!tokenizedUser) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+ const invite = await this.inviteService.create(tokenizedUser, req.body);
+ res.status(201).json({ message: "OK", data: invite });
+ } catch (error: any) {
+ next(error);
+ }
+ };
+
+ getAll = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const invites = await this.inviteService.getAll();
+ res.status(200).json({
+ message: "OK",
+ data: invites,
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ get = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const token = req.params.token;
+ if (!token) {
+ return res.status(400).json({ message: "Token parameter is required" });
+ }
+ const invite = await this.inviteService.get(token);
+ res.status(200).json({ message: "OK", data: invite });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ delete = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const id = req.params.id;
+ if (!id) {
+ return res.status(400).json({ message: "ID parameter is required" });
+ }
+ await this.inviteService.delete(id);
+ res.status(204).json({ message: "OK" });
+ } catch (error: any) {
+ next(error);
+ }
+ };
+}
+
+export default InviteController;
diff --git a/server/src/controllers/v2/MaintenanceController.ts b/server/src/controllers/v2/MaintenanceController.ts
new file mode 100644
index 000000000..7c6be11d5
--- /dev/null
+++ b/server/src/controllers/v2/MaintenanceController.ts
@@ -0,0 +1,96 @@
+import { Request, Response, NextFunction } from "express";
+import MaintenanceService from "../../service/v2/business/MaintenanceService.js";
+
+class MaintenanceController {
+ private maintenanceService: MaintenanceService;
+ constructor(maintenanceService: MaintenanceService) {
+ this.maintenanceService = maintenanceService;
+ }
+
+ create = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const tokenizedUser = req.user;
+ if (!tokenizedUser) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+ const maintenance = await this.maintenanceService.create(tokenizedUser, req.body);
+ res.status(201).json({ message: "OK", data: maintenance });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ getAll = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const maintenances = await this.maintenanceService.getAll();
+ res.status(200).json({
+ message: "OK",
+ data: maintenances,
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ toggleActive = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const tokenizedUser = req.user;
+ if (!tokenizedUser) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+ const id = req.params.id;
+ if (!id) {
+ return res.status(400).json({ message: "ID parameter is required" });
+ }
+ const maintenance = await this.maintenanceService.toggleActive(tokenizedUser, id);
+ res.status(200).json({ message: "OK", data: maintenance });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ update = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const tokenizedUser = req.user;
+ if (!tokenizedUser) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+ const id = req.params.id;
+ if (!id) {
+ return res.status(400).json({ message: "ID parameter is required" });
+ }
+ const updatedMaintenance = await this.maintenanceService.update(tokenizedUser, id, req.body);
+ res.status(200).json({ message: "OK", data: updatedMaintenance });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ get = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const id = req.params.id;
+ if (!id) {
+ return res.status(400).json({ message: "ID parameter is required" });
+ }
+ const maintenance = await this.maintenanceService.get(id);
+ res.status(200).json({ message: "OK", data: maintenance });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ delete = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const id = req.params.id;
+ if (!id) {
+ return res.status(400).json({ message: "ID parameter is required" });
+ }
+ await this.maintenanceService.delete(id);
+ res.status(204).json({ message: "OK" });
+ } catch (error) {
+ next(error);
+ }
+ };
+}
+
+export default MaintenanceController;
diff --git a/server/src/controllers/v2/MonitorController.ts b/server/src/controllers/v2/MonitorController.ts
new file mode 100644
index 000000000..7c0c63d2c
--- /dev/null
+++ b/server/src/controllers/v2/MonitorController.ts
@@ -0,0 +1,191 @@
+import { Request, Response, NextFunction } from "express";
+import ApiError from "../../utils/ApiError.js";
+import MonitorService from "../../service/v2/business/MonitorService.js";
+import { MonitorType } from "../../db/v2/models/monitors/Monitor.js";
+import CheckService from "../../service/v2/business/CheckService.js";
+class MonitorController {
+ private monitorService: MonitorService;
+ private checkService: CheckService;
+ constructor(monitorService: MonitorService, checkService: CheckService) {
+ this.monitorService = monitorService;
+ this.checkService = checkService;
+ }
+
+ create = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const tokenizedUser = req.user;
+ if (!tokenizedUser) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+
+ const monitor = await this.monitorService.create(tokenizedUser, req.body);
+ res.status(201).json({
+ message: "Monitor created successfully",
+ data: monitor,
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ getAll = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const tokenizedUser = req.user;
+ if (!tokenizedUser) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+
+ let monitors;
+ if (req.query.embedChecks === "true") {
+ const page = Math.max(1, Number(req.query.page) || 1);
+ const limit = Math.max(1, Number(req.query.limit) || 10);
+ const type: MonitorType[] = req.query.type as MonitorType[];
+
+ monitors = await this.monitorService.getAllEmbedChecks(page, limit, type);
+ } else {
+ monitors = await this.monitorService.getAll();
+ }
+
+ res.status(200).json({
+ message: "Monitors retrieved successfully",
+ data: monitors,
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ getChecks = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const tokenizedUser = req.user;
+ if (!tokenizedUser) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+
+ const id = req.params.id;
+ if (!id) {
+ throw new ApiError("Monitor ID is required", 400);
+ }
+
+ const page = Number(req.query.page);
+ const rowsPerPage = Number(req.query.rowsPerPage);
+
+ if (isNaN(page)) throw new ApiError("Page query parameter must be a number", 400);
+ if (isNaN(rowsPerPage)) throw new ApiError("rowsPerPage query parameter must be a number", 400);
+
+ if (page < 0) throw new ApiError("Page must be greater than 0", 400);
+ if (rowsPerPage < 0) throw new ApiError("rowsPerPage must be greater than 0", 400);
+
+ const { count, checks } = await this.checkService.getChecks(id, page, rowsPerPage);
+ res.status(200).json({
+ message: "Checks retrieved successfully",
+ data: { count, checks },
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ toggleActive = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const tokenizedUser = req.user;
+ if (!tokenizedUser) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+
+ const id = req.params.id;
+ if (!id) {
+ throw new ApiError("Monitor ID is required", 400);
+ }
+
+ const monitor = await this.monitorService.toggleActive(id, tokenizedUser);
+ res.status(200).json({
+ message: "Monitor paused/unpaused successfully",
+ data: monitor,
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ get = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const tokenizedUser = req.user;
+ if (!tokenizedUser) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+
+ const id = req.params.id;
+ if (!id) {
+ throw new ApiError("Monitor ID is required", 400);
+ }
+
+ const range = req.query.range;
+ if (!range || typeof range !== "string") throw new ApiError("Range query parameter is required", 400);
+
+ let monitor;
+
+ const status = req.query.status;
+ if (status && typeof status !== "string") {
+ throw new ApiError("Status query parameter must be a string", 400);
+ }
+
+ if (req.query.embedChecks === "true") {
+ monitor = await this.monitorService.getEmbedChecks(id, range, status);
+ } else {
+ monitor = await this.monitorService.get(id);
+ }
+
+ res.status(200).json({
+ message: "Monitor retrieved successfully",
+ data: monitor,
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ update = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const tokenizedUser = req.user;
+ if (!tokenizedUser) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+
+ const id = req.params.id;
+ if (!id) {
+ throw new ApiError("Monitor ID is required", 400);
+ }
+
+ const monitor = await this.monitorService.update(tokenizedUser, id, req.body);
+ res.status(200).json({
+ message: "Monitor updated successfully",
+ data: monitor,
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ delete = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const tokenizedUser = req.user;
+ if (!tokenizedUser) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+ const id = req.params.id;
+ if (!id) {
+ throw new ApiError("Monitor ID is required", 400);
+ }
+ await this.monitorService.delete(id);
+
+ res.status(200).json({
+ message: "Monitor deleted successfully",
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+}
+
+export default MonitorController;
diff --git a/server/src/controllers/v2/NotificationChannelController.ts b/server/src/controllers/v2/NotificationChannelController.ts
new file mode 100644
index 000000000..e13f572f4
--- /dev/null
+++ b/server/src/controllers/v2/NotificationChannelController.ts
@@ -0,0 +1,96 @@
+import { Request, Response, NextFunction } from "express";
+import NotificationService from "../../service/v2/business/NotificationChannelService.js";
+
+class NotificationChannelController {
+ private notificationService: NotificationService;
+ constructor(notificationService: NotificationService) {
+ this.notificationService = notificationService;
+ }
+
+ create = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const tokenizedUser = req.user;
+ if (!tokenizedUser) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+ const channel = await this.notificationService.create(tokenizedUser, req.body);
+ res.status(201).json({ message: "OK", data: channel });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ getAll = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const notificationChannels = await this.notificationService.getAll();
+ res.status(200).json({
+ message: "OK",
+ data: notificationChannels,
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ toggleActive = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const tokenizedUser = req.user;
+ if (!tokenizedUser) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+ const id = req.params.id;
+ if (!id) {
+ return res.status(400).json({ message: "ID parameter is required" });
+ }
+ const notificationChannel = await this.notificationService.toggleActive(tokenizedUser, id);
+ res.status(200).json({ message: "OK", data: notificationChannel });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ update = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const tokenizedUser = req.user;
+ if (!tokenizedUser) {
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+ const id = req.params.id;
+ if (!id) {
+ return res.status(400).json({ message: "ID parameter is required" });
+ }
+ const updatedChannel = await this.notificationService.update(tokenizedUser, id, req.body);
+ res.status(200).json({ message: "OK", data: updatedChannel });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ get = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const id = req.params.id;
+ if (!id) {
+ return res.status(400).json({ message: "ID parameter is required" });
+ }
+ const notificationChannel = await this.notificationService.get(id);
+ res.status(200).json({ message: "OK", data: notificationChannel });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ delete = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const id = req.params.id;
+ if (!id) {
+ return res.status(400).json({ message: "ID parameter is required" });
+ }
+ await this.notificationService.delete(id);
+ res.status(204).json({ message: "OK" });
+ } catch (error) {
+ next(error);
+ }
+ };
+}
+
+export default NotificationChannelController;
diff --git a/server/src/controllers/v2/QueueController.ts b/server/src/controllers/v2/QueueController.ts
new file mode 100644
index 000000000..510703894
--- /dev/null
+++ b/server/src/controllers/v2/QueueController.ts
@@ -0,0 +1,29 @@
+import { Request, Response, NextFunction } from "express";
+import QueueService from "../../service/v2/business/QueueService.js";
+class QueueController {
+ private queueService: QueueService;
+ constructor(queueService: QueueService) {
+ this.queueService = queueService;
+ }
+
+ getJobs = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const jobs = await this.queueService.getJobs();
+ res.status(200).json({ message: "ok", data: jobs });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ getMetrics = async (req: Request, res: Response, next: NextFunction) => {
+ const metrics = await this.queueService.getMetrics();
+ res.status(200).json({ message: "ok", data: metrics });
+ };
+
+ flush = async (req: Request, res: Response, next: NextFunction) => {
+ const result = await this.queueService.flush();
+ res.status(200).json({ message: "ok", flushed: result });
+ };
+}
+
+export default QueueController;
diff --git a/server/src/db/mongo/MongoDB.js b/server/src/db/v1/MongoDB.js
similarity index 97%
rename from server/src/db/mongo/MongoDB.js
rename to server/src/db/v1/MongoDB.js
index 74fcbd89f..3bdae3525 100755
--- a/server/src/db/mongo/MongoDB.js
+++ b/server/src/db/v1/MongoDB.js
@@ -1,5 +1,5 @@
import mongoose from "mongoose";
-import AppSettings from "../models/AppSettings.js";
+import AppSettings from "./models/AppSettings.js";
import { runMigrations } from "./migration/index.js";
class MongoDB {
static SERVICE_NAME = "MongoDB";
diff --git a/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js b/server/src/db/v1/migration/0001_migrateStatusWindowThreshold.ts
similarity index 92%
rename from server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js
rename to server/src/db/v1/migration/0001_migrateStatusWindowThreshold.ts
index 8f6988de4..857d36381 100644
--- a/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js
+++ b/server/src/db/v1/migration/0001_migrateStatusWindowThreshold.ts
@@ -1,4 +1,4 @@
-import Monitor from "../../models/Monitor.js";
+import Monitor from "../models/Monitor.js";
async function migrateStatusWindowThreshold() {
try {
const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } });
diff --git a/server/src/db/mongo/migration/index.js b/server/src/db/v1/migration/index.ts
similarity index 100%
rename from server/src/db/mongo/migration/index.js
rename to server/src/db/v1/migration/index.ts
diff --git a/server/src/db/models/AppSettings.js b/server/src/db/v1/models/AppSettings.js
similarity index 100%
rename from server/src/db/models/AppSettings.js
rename to server/src/db/v1/models/AppSettings.js
diff --git a/server/src/db/models/Check.js b/server/src/db/v1/models/Check.js
similarity index 100%
rename from server/src/db/models/Check.js
rename to server/src/db/v1/models/Check.js
diff --git a/server/src/db/models/InviteToken.js b/server/src/db/v1/models/InviteToken.js
similarity index 100%
rename from server/src/db/models/InviteToken.js
rename to server/src/db/v1/models/InviteToken.js
diff --git a/server/src/db/models/MaintenanceWindow.js b/server/src/db/v1/models/MaintenanceWindow.js
similarity index 100%
rename from server/src/db/models/MaintenanceWindow.js
rename to server/src/db/v1/models/MaintenanceWindow.js
diff --git a/server/src/db/models/Monitor.js b/server/src/db/v1/models/Monitor.js
similarity index 100%
rename from server/src/db/models/Monitor.js
rename to server/src/db/v1/models/Monitor.js
diff --git a/server/src/db/models/MonitorStats.js b/server/src/db/v1/models/MonitorStats.js
similarity index 100%
rename from server/src/db/models/MonitorStats.js
rename to server/src/db/v1/models/MonitorStats.js
diff --git a/server/src/db/models/Notification.js b/server/src/db/v1/models/Notification.js
similarity index 100%
rename from server/src/db/models/Notification.js
rename to server/src/db/v1/models/Notification.js
diff --git a/server/src/db/models/RecoveryToken.js b/server/src/db/v1/models/RecoveryToken.js
similarity index 100%
rename from server/src/db/models/RecoveryToken.js
rename to server/src/db/v1/models/RecoveryToken.js
diff --git a/server/src/db/models/StatusPage.js b/server/src/db/v1/models/StatusPage.js
similarity index 100%
rename from server/src/db/models/StatusPage.js
rename to server/src/db/v1/models/StatusPage.js
diff --git a/server/src/db/models/Team.js b/server/src/db/v1/models/Team.js
similarity index 100%
rename from server/src/db/models/Team.js
rename to server/src/db/v1/models/Team.js
diff --git a/server/src/db/models/User.js b/server/src/db/v1/models/User.js
similarity index 98%
rename from server/src/db/models/User.js
rename to server/src/db/v1/models/User.js
index 9368d98b1..bf3012b26 100755
--- a/server/src/db/models/User.js
+++ b/server/src/db/v1/models/User.js
@@ -1,6 +1,5 @@
import mongoose from "mongoose";
import bcrypt from "bcryptjs";
-import logger from "../../utils/logger.js";
import Monitor from "./Monitor.js";
import Team from "./Team.js";
import Notification from "./Notification.js";
diff --git a/server/src/db/mongo/modules/checkModule.js b/server/src/db/v1/modules/checkModule.js
similarity index 100%
rename from server/src/db/mongo/modules/checkModule.js
rename to server/src/db/v1/modules/checkModule.js
diff --git a/server/src/db/mongo/modules/checkModuleQueries.js b/server/src/db/v1/modules/checkModuleQueries.js
similarity index 100%
rename from server/src/db/mongo/modules/checkModuleQueries.js
rename to server/src/db/v1/modules/checkModuleQueries.js
diff --git a/server/src/db/mongo/modules/inviteModule.js b/server/src/db/v1/modules/inviteModule.js
similarity index 100%
rename from server/src/db/mongo/modules/inviteModule.js
rename to server/src/db/v1/modules/inviteModule.js
diff --git a/server/src/db/mongo/modules/maintenanceWindowModule.js b/server/src/db/v1/modules/maintenanceWindowModule.js
similarity index 100%
rename from server/src/db/mongo/modules/maintenanceWindowModule.js
rename to server/src/db/v1/modules/maintenanceWindowModule.js
diff --git a/server/src/db/mongo/modules/monitorModule.js b/server/src/db/v1/modules/monitorModule.js
similarity index 99%
rename from server/src/db/mongo/modules/monitorModule.js
rename to server/src/db/v1/modules/monitorModule.js
index 8beb9316c..f795a7f2e 100755
--- a/server/src/db/mongo/modules/monitorModule.js
+++ b/server/src/db/v1/modules/monitorModule.js
@@ -232,7 +232,7 @@ class MonitorModule {
const formatLookup = {
recent: "%Y-%m-%dT%H:%M:00Z",
day: "%Y-%m-%dT%H:00:00Z",
- week: "%Y-%m-%dT%H:00:00Z",
+ week: "%Y-%m-%dT00:00:00Z",
month: "%Y-%m-%dT00:00:00Z",
};
@@ -308,7 +308,7 @@ class MonitorModule {
const formatLookup = {
recent: "%Y-%m-%dT%H:%M:00Z",
day: "%Y-%m-%dT%H:00:00Z",
- week: "%Y-%m-%dT%H:00:00Z",
+ week: "%Y-%m-%dT00:00:00Z",
month: "%Y-%m-%dT00:00:00Z",
};
const dateString = formatLookup[dateRange];
diff --git a/server/src/db/mongo/modules/monitorModuleQueries.js b/server/src/db/v1/modules/monitorModuleQueries.js
similarity index 100%
rename from server/src/db/mongo/modules/monitorModuleQueries.js
rename to server/src/db/v1/modules/monitorModuleQueries.js
diff --git a/server/src/db/mongo/modules/notificationModule.js b/server/src/db/v1/modules/notificationModule.js
similarity index 100%
rename from server/src/db/mongo/modules/notificationModule.js
rename to server/src/db/v1/modules/notificationModule.js
diff --git a/server/src/db/mongo/modules/recoveryModule.js b/server/src/db/v1/modules/recoveryModule.js
similarity index 100%
rename from server/src/db/mongo/modules/recoveryModule.js
rename to server/src/db/v1/modules/recoveryModule.js
diff --git a/server/src/db/mongo/modules/settingsModule.js b/server/src/db/v1/modules/settingsModule.js
similarity index 100%
rename from server/src/db/mongo/modules/settingsModule.js
rename to server/src/db/v1/modules/settingsModule.js
diff --git a/server/src/db/mongo/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js
similarity index 100%
rename from server/src/db/mongo/modules/statusPageModule.js
rename to server/src/db/v1/modules/statusPageModule.js
diff --git a/server/src/db/mongo/modules/userModule.js b/server/src/db/v1/modules/userModule.js
similarity index 100%
rename from server/src/db/mongo/modules/userModule.js
rename to server/src/db/v1/modules/userModule.js
diff --git a/server/src/db/mongo/utils/seedDb.js b/server/src/db/v1/utils/seedDb.js
similarity index 95%
rename from server/src/db/mongo/utils/seedDb.js
rename to server/src/db/v1/utils/seedDb.js
index 224744a51..cdfe3675c 100755
--- a/server/src/db/mongo/utils/seedDb.js
+++ b/server/src/db/v1/utils/seedDb.js
@@ -1,5 +1,5 @@
-import Monitor from "../../models/Monitor.js";
-import Check from "../../models/Check.js";
+import Monitor from "../models/Monitor.js";
+import Check from "../models/Check.js";
import { logger } from "../../../utils/logger.js";
const generateRandomUrl = () => {
diff --git a/server/src/db/v2/index.ts b/server/src/db/v2/index.ts
new file mode 100644
index 000000000..2cd194283
--- /dev/null
+++ b/server/src/db/v2/index.ts
@@ -0,0 +1,25 @@
+import mongoose from "mongoose";
+
+const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/checkmate";
+
+export const connectDatabase = async (): Promise => {
+ try {
+ await mongoose.connect(MONGODB_URI);
+ console.log("Connected to MongoDB");
+ return true;
+ } catch (error) {
+ console.error("MongoDB connection error:", error);
+ process.exit(1);
+ }
+};
+
+export const disconnectDatabase = async (): Promise => {
+ try {
+ await mongoose.disconnect();
+ console.log("Disconnected from MongoDB");
+ return true;
+ } catch (error) {
+ console.error("MongoDB disconnection error:", error);
+ return false;
+ }
+};
diff --git a/server/src/db/v2/models/auth/Role.ts b/server/src/db/v2/models/auth/Role.ts
new file mode 100644
index 000000000..1031de8b2
--- /dev/null
+++ b/server/src/db/v2/models/auth/Role.ts
@@ -0,0 +1,50 @@
+import mongoose, { Schema, Document, Types } from "mongoose";
+
+export interface IRole extends Document {
+ _id: Types.ObjectId;
+ name: string;
+ description?: string;
+ permissions: string[];
+ isSystem: boolean;
+ isActive: boolean;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+const roleSchema = new Schema(
+ {
+ name: {
+ type: String,
+ required: true,
+ trim: true,
+ maxlength: 50,
+ },
+ description: {
+ type: String,
+ trim: true,
+ maxlength: 200,
+ },
+
+ permissions: [
+ {
+ type: String,
+ required: true,
+ },
+ ],
+ isSystem: {
+ type: Boolean,
+ default: false,
+ },
+ isActive: {
+ type: Boolean,
+ default: true,
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+roleSchema.index({ name: 1 }, { unique: true });
+
+export const Role = mongoose.model("Role_v2", roleSchema);
diff --git a/server/src/db/v2/models/auth/User.ts b/server/src/db/v2/models/auth/User.ts
new file mode 100644
index 000000000..25b9794c7
--- /dev/null
+++ b/server/src/db/v2/models/auth/User.ts
@@ -0,0 +1,102 @@
+import mongoose, { Schema, Document, Types } from "mongoose";
+
+export interface ITokenizedUser {
+ sub: string;
+ roles: string[];
+}
+export interface IUser extends Document {
+ _id: Types.ObjectId;
+ email: string;
+ firstName: string;
+ lastName: string;
+ passwordHash: string;
+ roles: Types.ObjectId[];
+ profile: {
+ avatar?: string;
+ bio?: string;
+ timezone: string;
+ locale: string;
+ };
+ preferences: {
+ theme: "light" | "dark" | "system";
+ };
+ lastLoginAt?: Date;
+ isActive: boolean;
+ isVerified: boolean;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+const userSchema = new Schema(
+ {
+ email: {
+ type: String,
+ required: true,
+ unique: true,
+ trim: true,
+ lowercase: true,
+ },
+ firstName: {
+ type: String,
+ required: true,
+ trim: true,
+ maxlength: 50,
+ },
+ lastName: {
+ type: String,
+ required: true,
+ trim: true,
+ maxlength: 50,
+ },
+ passwordHash: {
+ type: String,
+ required: true,
+ },
+ roles: [
+ {
+ type: Schema.Types.ObjectId,
+ ref: "Role_v2",
+ },
+ ],
+ profile: {
+ avatar: {
+ type: String,
+ },
+ bio: {
+ type: String,
+ maxlength: 200,
+ },
+ timezone: {
+ type: String,
+ default: "UTC",
+ },
+ locale: {
+ type: String,
+ default: "en",
+ },
+ },
+ preferences: {
+ theme: {
+ type: String,
+ enum: ["light", "dark", "system"],
+ default: "system",
+ },
+ },
+ lastLoginAt: {
+ type: Date,
+ },
+ isActive: {
+ type: Boolean,
+ default: true,
+ },
+ isVerified: {
+ type: Boolean,
+ default: false,
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+export const User = mongoose.model("User_v2", userSchema);
diff --git a/server/src/db/v2/models/checks/Check.ts b/server/src/db/v2/models/checks/Check.ts
new file mode 100644
index 000000000..83bbc9f91
--- /dev/null
+++ b/server/src/db/v2/models/checks/Check.ts
@@ -0,0 +1,297 @@
+import mongoose, { Schema, Document, Types } from "mongoose";
+import { MonitorType, MonitorTypes, MonitorStatus, MonitorStatuses } from "../monitors/Monitor.js";
+import type { Response } from "got";
+export type GotTimings = Response["timings"];
+export interface ITimingPhases {
+ wait: number;
+ dns: number;
+ tcp: number;
+ tls: number;
+ request: number;
+ firstByte: number;
+ download: number;
+ total: number;
+}
+
+export interface ICpuInfo {
+ physical_core: number;
+ logical_core: number;
+ frequency: number;
+ current_frequency: number;
+ temperature: number[]; // per-core temps
+ free_percent: number;
+ usage_percent: number;
+}
+
+export interface IMemoryInfo {
+ total_bytes: number;
+ available_bytes: number;
+ used_bytes: number;
+ usage_percent: number;
+}
+
+export interface IHostInfo {
+ os?: string;
+ platform?: string;
+ kernel_version?: string;
+ pretty_name?: string;
+}
+
+export interface IDiskInfo {
+ device: string;
+ total_bytes: number;
+ free_bytes: number;
+ used_bytes: number;
+ usage_percent: number;
+ total_inodes?: number;
+ free_inodes?: number;
+ used_inodes?: number;
+ inodes_usage_percent?: number;
+ read_bytes?: number;
+ write_bytes?: number;
+ read_time?: number;
+ write_time?: number;
+}
+
+export interface INetInfo {
+ name: string;
+ bytes_sent: number;
+ bytes_recv: number;
+ packets_sent: number;
+ packets_recv: number;
+ err_in: number;
+ err_out: number;
+ drop_in: number;
+ drop_out: number;
+ fifo_in: number;
+ fifo_out: number;
+}
+
+export interface ICaptureInfo {
+ version?: string;
+ mode?: string;
+}
+
+export interface ISystemInfo {
+ cpu: ICpuInfo;
+ memory: IMemoryInfo;
+ disk: IDiskInfo[];
+ host: IHostInfo;
+ net: INetInfo[];
+}
+
+export interface ILighthouseAudit {
+ id?: string;
+ title?: string;
+ score?: number | null;
+ displayValue?: string;
+ numericValue?: number;
+ numericUnit?: string;
+}
+export interface ILighthouseCategories {
+ accessibility?: { score?: number | null };
+ "best-practices"?: { score?: number | null };
+ seo?: { score?: number | null };
+ performance?: { score?: number | null };
+}
+
+export interface ILighthouseResult {
+ categories?: ILighthouseCategories;
+ audits?: Record;
+}
+
+export interface ICheckLighthouseFields {
+ accessibility: number;
+ bestPractices: number;
+ seo: number;
+ performance: number;
+ audits: {
+ cls: ILighthouseAudit;
+ si: ILighthouseAudit;
+ fcp: ILighthouseAudit;
+ lcp: ILighthouseAudit;
+ tbt: ILighthouseAudit;
+ };
+}
+
+export interface ICheck extends Document {
+ _id: Types.ObjectId;
+ monitorId: Types.ObjectId;
+ type: MonitorType;
+ status: MonitorStatus;
+ message: string;
+ responseTime?: number; // in ms
+ timings?: GotTimings;
+ httpStatusCode?: number;
+ errorMessage?: string;
+ ack: boolean;
+ ackAt?: Date;
+ ackBy?: Types.ObjectId;
+ expiry: Date;
+ createdAt: Date;
+ updatedAt: Date;
+ system?: ISystemInfo;
+ capture?: ICaptureInfo;
+ lighthouse?: ICheckLighthouseFields;
+}
+
+const CheckSchema = new Schema(
+ {
+ monitorId: { type: Schema.Types.ObjectId, ref: "Monitor_v2", required: true },
+
+ type: {
+ type: String,
+ required: true,
+ enum: MonitorTypes,
+ },
+ status: {
+ type: String,
+ required: true,
+ enum: MonitorStatuses,
+ },
+ message: { type: String, trim: true },
+ responseTime: { type: Number },
+ timings: {
+ start: { type: Date },
+ socket: { type: Date },
+ lookup: { type: Date },
+ connect: { type: Date },
+ secureConnect: { type: Date },
+ response: { type: Date },
+ end: { type: Date },
+ phases: {
+ wait: { type: Number },
+ dns: { type: Number },
+ tcp: { type: Number },
+ tls: { type: Number },
+ request: { type: Number },
+ firstByte: { type: Number },
+ download: { type: Number },
+ total: { type: Number },
+ },
+ },
+ system: {
+ type: {
+ cpu: {
+ physical_core: { type: Number },
+ logical_core: { type: Number },
+ frequency: { type: Number },
+ current_frequency: { type: Number },
+ temperature: [{ type: Number }],
+ free_percent: { type: Number },
+ usage_percent: { type: Number },
+ },
+ memory: {
+ total_bytes: { type: Number },
+ available_bytes: { type: Number },
+ used_bytes: { type: Number },
+ usage_percent: { type: Number },
+ },
+ disk: [
+ {
+ device: { type: String },
+ total_bytes: { type: Number },
+ free_bytes: { type: Number },
+ used_bytes: { type: Number },
+ usage_percent: { type: Number },
+ total_inodes: { type: Number },
+ free_inodes: { type: Number },
+ used_inodes: { type: Number },
+ inodes_usage_percent: { type: Number },
+ read_bytes: { type: Number },
+ write_bytes: { type: Number },
+ read_time: { type: Number },
+ write_time: { type: Number },
+ },
+ ],
+ host: {
+ os: { type: String },
+ platform: { type: String },
+ kernel_version: { type: String },
+ pretty_name: { type: String },
+ },
+ net: [
+ {
+ name: { type: String },
+ bytes_sent: { type: Number },
+ bytes_recv: { type: Number },
+ packets_sent: { type: Number },
+ packets_recv: { type: Number },
+ err_in: { type: Number },
+ err_out: { type: Number },
+ drop_in: { type: Number },
+ drop_out: { type: Number },
+ fifo_in: { type: Number },
+ fifo_out: { type: Number },
+ },
+ ],
+ },
+ required: false,
+ },
+
+ capture: {
+ type: {
+ version: { type: String },
+ mode: { type: String },
+ },
+ required: false,
+ },
+ lighthouse: {
+ accessibility: { type: Number, required: false },
+ bestPractices: { type: Number, required: false },
+ seo: { type: Number, required: false },
+ performance: { type: Number, required: false },
+ audits: {
+ cls: {
+ type: Object,
+ },
+ si: {
+ type: Object,
+ },
+ fcp: {
+ type: Object,
+ },
+ lcp: {
+ type: Object,
+ },
+ tbt: {
+ type: Object,
+ },
+ },
+ type: {
+ accessibility: { type: Number },
+ bestPractices: { type: Number },
+ seo: { type: Number },
+ performance: { type: Number },
+ audits: {
+ cls: { type: Object },
+ si: { type: Object },
+ fcp: { type: Object },
+ lcp: { type: Object },
+ tbt: { type: Object },
+ },
+ },
+ required: false,
+ },
+
+ httpStatusCode: { type: Number },
+ errorMessage: { type: String, trim: true },
+ ack: { type: Boolean, required: true, default: false },
+ ackAt: { type: Date },
+ ackBy: { type: Schema.Types.ObjectId, ref: "User_v2" },
+ expiry: {
+ type: Date,
+ default: Date.now,
+ expires: 60 * 60 * 24 * 30,
+ },
+ },
+ { timestamps: true }
+);
+
+CheckSchema.index({ monitorId: 1, createdAt: -1 });
+CheckSchema.index({ status: 1 });
+CheckSchema.index({ status: 1, ack: 1 });
+CheckSchema.index({ ack: 1, ackAt: 1 });
+CheckSchema.index({ createdAt: -1 });
+
+export const Check = mongoose.model("Check_v2", CheckSchema);
diff --git a/server/src/db/v2/models/index.ts b/server/src/db/v2/models/index.ts
new file mode 100644
index 000000000..fbc05c10f
--- /dev/null
+++ b/server/src/db/v2/models/index.ts
@@ -0,0 +1,33 @@
+export { User } from "./auth/User.js";
+export type { IUser } from "./auth/User.js";
+export type { ITokenizedUser } from "./auth/User.js";
+export { Role } from "./auth/Role.js";
+export type { IRole } from "./auth/Role.js";
+export { connectDatabase, disconnectDatabase } from "../index.js";
+export { Monitor } from "./monitors/Monitor.js";
+export { MonitorStatuses } from "./monitors/Monitor.js";
+export type { IMonitor } from "./monitors/Monitor.js";
+export { Check } from "./checks/Check.js";
+export type {
+ ICheck,
+ ISystemInfo,
+ ICaptureInfo,
+ INetInfo,
+ IDiskInfo,
+ IHostInfo,
+ IMemoryInfo,
+ ICpuInfo,
+ ILighthouseAudit,
+ ITimingPhases,
+ ILighthouseCategories,
+ ILighthouseResult,
+ ICheckLighthouseFields,
+} from "./checks/Check.js";
+export type { IMonitorStats } from "./monitors/MonitorStats.js";
+export { MonitorStats } from "./monitors/MonitorStats.js";
+export type { INotificationChannel } from "./notification-channel/NotificationChannel.js";
+export { NotificationChannel } from "./notification-channel/NotificationChannel.js";
+export type { IMaintenance } from "./maintenance/Maintenance.js";
+export { Maintenance } from "./maintenance/Maintenance.js";
+export type { IInvite } from "./invite/Invite.js";
+export { Invite } from "./invite/Invite.js";
diff --git a/server/src/db/v2/models/invite/Invite.ts b/server/src/db/v2/models/invite/Invite.ts
new file mode 100644
index 000000000..2bc569900
--- /dev/null
+++ b/server/src/db/v2/models/invite/Invite.ts
@@ -0,0 +1,43 @@
+import mongoose, { Schema, Document, Types } from "mongoose";
+
+export interface IInvite extends Document {
+ _id: Types.ObjectId;
+ email: string;
+ tokenHash: string;
+ roles: Types.ObjectId[];
+ createdBy: Types.ObjectId;
+ updatedBy: Types.ObjectId;
+ expiry: Date;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+const InviteSchema = new Schema(
+ {
+ email: {
+ type: String,
+ required: true,
+ unique: true,
+ trim: true,
+ lowercase: true,
+ },
+ roles: [
+ {
+ type: Schema.Types.ObjectId,
+ ref: "Role_v2",
+ required: true,
+ },
+ ],
+ tokenHash: { type: String, required: true, unique: true },
+ expiry: {
+ type: Date,
+ default: Date.now,
+ expires: 60 * 60 * 24,
+ },
+ createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
+ updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
+ },
+ { timestamps: true }
+);
+
+export const Invite = mongoose.model("Invite_v2", InviteSchema);
diff --git a/server/src/db/v2/models/maintenance/Maintenance.ts b/server/src/db/v2/models/maintenance/Maintenance.ts
new file mode 100644
index 000000000..c2ce33a2b
--- /dev/null
+++ b/server/src/db/v2/models/maintenance/Maintenance.ts
@@ -0,0 +1,39 @@
+import mongoose, { Schema, Document, Types } from "mongoose";
+
+export interface IMaintenance extends Document {
+ _id: Types.ObjectId;
+ name: string;
+ isActive: boolean;
+ monitors: Types.ObjectId[];
+ startTime: Date;
+ endTime: Date;
+ createdBy: Types.ObjectId;
+ updatedBy: Types.ObjectId;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+const MaintenanceSchema = new Schema(
+ {
+ name: { type: String, required: true, trim: true },
+ isActive: { type: Boolean, required: true, default: true },
+ monitors: [
+ {
+ type: Schema.Types.ObjectId,
+ ref: "Monitor_v2",
+ required: true,
+ },
+ ],
+ startTime: { type: Date, required: true },
+ endTime: { type: Date, required: true },
+ createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
+ updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
+ },
+ { timestamps: true }
+);
+
+MaintenanceSchema.index({ isActive: 1 });
+MaintenanceSchema.index({ startTime: 1 });
+MaintenanceSchema.index({ endTime: 1 });
+
+export const Maintenance = mongoose.model("Maintenance_v2", MaintenanceSchema);
diff --git a/server/src/db/v2/models/monitors/Monitor.ts b/server/src/db/v2/models/monitors/Monitor.ts
new file mode 100644
index 000000000..52cbae9d8
--- /dev/null
+++ b/server/src/db/v2/models/monitors/Monitor.ts
@@ -0,0 +1,96 @@
+import mongoose, { Schema, Document, Types } from "mongoose";
+import { Check, MonitorStats } from "../index.js";
+
+export const MonitorTypes = ["http", "https", "ping", "infrastructure", "pagespeed"] as const;
+export type MonitorType = (typeof MonitorTypes)[number];
+
+export const MonitorStatuses = ["up", "down", "paused", "initializing"] as const;
+export type MonitorStatus = (typeof MonitorStatuses)[number];
+export interface IMonitor extends Document {
+ _id: Types.ObjectId;
+ name: string;
+ url: string;
+ secret?: string;
+ type: MonitorType;
+ interval: number; // in ms
+ isActive: boolean;
+ status: MonitorStatus;
+ n: number; // Number of consecutive successes required to change status
+ lastCheckedAt?: Date;
+ latestChecks: {
+ status: MonitorStatus;
+ responseTime: number;
+ checkedAt: Date;
+ }[];
+ notificationChannels?: Types.ObjectId[];
+ createdBy: Types.ObjectId;
+ updatedBy: Types.ObjectId;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+const MonitorSchema = new Schema(
+ {
+ name: { type: String, required: true, trim: true, maxlength: 100 },
+ url: { type: String, required: true, trim: true },
+ secret: { type: String, required: false },
+ type: {
+ type: String,
+ required: true,
+ enum: MonitorTypes,
+ },
+ interval: { type: Number, required: true, default: 60000 },
+ isActive: { type: Boolean, required: true, default: true },
+ status: {
+ type: String,
+ required: true,
+ default: "initializing",
+ enum: MonitorStatuses,
+ },
+ n: { type: Number, required: true, default: 1 },
+ lastCheckedAt: { type: Date },
+ latestChecks: {
+ type: [
+ {
+ status: {
+ type: String,
+ required: true,
+ enum: MonitorStatuses,
+ },
+ responseTime: { type: Number, required: true },
+ checkedAt: { type: Date, required: true },
+ },
+ ],
+ default: [],
+ },
+ notificationChannels: {
+ type: [{ type: Schema.Types.ObjectId, ref: "NotificationChannel_v2" }],
+ default: [],
+ },
+
+ createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
+ updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
+ },
+ { timestamps: true }
+);
+
+MonitorSchema.pre("deleteOne", { document: true, query: false }, async function (next) {
+ try {
+ const monitorId = this._id;
+ await Check.deleteMany({ monitorId });
+ await MonitorStats.deleteMany({ monitorId });
+ next();
+ } catch (error: any) {
+ next(error);
+ }
+});
+
+MonitorSchema.index({ isActive: 1 });
+MonitorSchema.index({ status: 1 });
+MonitorSchema.index({ type: 1 });
+MonitorSchema.index({ lastCheckedAt: 1 });
+MonitorSchema.index({ isActive: 1, status: 1 });
+MonitorSchema.index({ createdBy: 1 });
+MonitorSchema.index({ updatedBy: 1 });
+
+export const Monitor = mongoose.model("Monitor_v2", MonitorSchema);
diff --git a/server/src/db/v2/models/monitors/MonitorStats.ts b/server/src/db/v2/models/monitors/MonitorStats.ts
new file mode 100644
index 000000000..56e76f9ab
--- /dev/null
+++ b/server/src/db/v2/models/monitors/MonitorStats.ts
@@ -0,0 +1,77 @@
+import mongoose, { Schema, Document, Types } from "mongoose";
+import { MonitorStatus, MonitorStatuses } from "./Monitor.js";
+
+export interface IMonitorStats extends mongoose.Document {
+ monitorId: mongoose.Types.ObjectId;
+ avgResponseTime: number;
+ maxResponseTime: number;
+ totalChecks: number;
+ totalUpChecks: number;
+ totalDownChecks: number;
+ uptimePercentage: number;
+ lastCheckTimestamp: number;
+ lastResponseTime: number;
+ timeOfLastFailure: number;
+ currentStreak: number;
+ currentStreakStatus: MonitorStatus;
+ currentStreakStartedAt: number;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+const MonitorStatsSchema = new Schema(
+ {
+ monitorId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "Monitor_v2",
+ immutable: true,
+ index: true,
+ },
+ avgResponseTime: {
+ type: Number,
+ default: 0,
+ },
+ maxResponseTime: {
+ type: Number,
+ default: 0,
+ },
+ lastResponseTime: {
+ type: Number,
+ default: 0,
+ },
+ totalChecks: {
+ type: Number,
+ default: 0,
+ },
+ totalUpChecks: {
+ type: Number,
+ default: 0,
+ },
+ totalDownChecks: {
+ type: Number,
+ default: 0,
+ },
+ uptimePercentage: {
+ type: Number,
+ default: 0,
+ },
+ lastCheckTimestamp: {
+ type: Number,
+ default: 0,
+ },
+ timeOfLastFailure: {
+ type: Number,
+ default: 0,
+ },
+ currentStreak: { type: Number, required: false, default: 0 },
+ currentStreakStatus: {
+ type: String,
+ required: false,
+ enum: MonitorStatuses,
+ },
+ currentStreakStartedAt: { type: Number, required: false },
+ },
+ { timestamps: true }
+);
+
+export const MonitorStats = mongoose.model("MonitorStats_v2", MonitorStatsSchema);
diff --git a/server/src/db/v2/models/notification-channel/NotificationChannel.ts b/server/src/db/v2/models/notification-channel/NotificationChannel.ts
new file mode 100644
index 000000000..e77639608
--- /dev/null
+++ b/server/src/db/v2/models/notification-channel/NotificationChannel.ts
@@ -0,0 +1,50 @@
+import mongoose, { Schema, Document, Types } from "mongoose";
+export const ChannelTypes = ["email", "slack", "discord", "webhook"] as const;
+export type ChannelType = (typeof ChannelTypes)[number];
+
+export interface INotificationChannelConfig {
+ url?: string; // For webhook, slack, discord
+ emailAddress?: string; // For email
+}
+
+export interface INotificationChannel {
+ _id: Types.ObjectId;
+ name: string;
+ type: ChannelType;
+ config: INotificationChannelConfig;
+ isActive: boolean;
+ createdBy: Types.ObjectId;
+ updatedBy: Types.ObjectId;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+const NotificationChannelConfigSchema = new Schema(
+ {
+ url: { type: String, required: false },
+ emailAddress: { type: String, required: false },
+ },
+ { _id: false, strict: "throw" }
+);
+
+const NotificationChannelSchema = new Schema(
+ {
+ name: { type: String, required: true, trim: true },
+ type: {
+ type: String,
+ required: true,
+ enum: ChannelTypes,
+ },
+ config: { type: NotificationChannelConfigSchema, required: true },
+ isActive: { type: Boolean, required: true, default: true },
+ createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
+ updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
+ },
+ { timestamps: true }
+);
+
+NotificationChannelSchema.index({ isActive: 1 });
+NotificationChannelSchema.index({ type: 1 });
+NotificationChannelSchema.index({ type: 1, isActive: 1 });
+
+export const NotificationChannel = mongoose.model("NotificationChannel_v2", NotificationChannelSchema);
diff --git a/server/src/index.js b/server/src/index.js
index d1ff649eb..e8b756d0e 100755
--- a/server/src/index.js
+++ b/server/src/index.js
@@ -7,8 +7,8 @@ import path from "path";
import fs from "fs";
import Logger from "./utils/logger.js";
-import SettingsService from "./service/system/settingsService.js";
-import AppSettings from "./db/models/AppSettings.js";
+import SettingsService from "./service/v1/system/settingsService.js";
+import AppSettings from "./db/v1/models/AppSettings.js";
const SERVICE_NAME = "Server";
let logger;
diff --git a/server/src/locales/en.json b/server/src/locales/en.json
index 74296d0a5..1345f84a8 100755
--- a/server/src/locales/en.json
+++ b/server/src/locales/en.json
@@ -149,10 +149,12 @@
"statusPageByTeamId": "Got status pages by team id successfully",
"httpNetworkError": "Network error",
"httpNotJson": "Response data is not json",
+ "httpExpectedValueFail": "Expected value did not match",
"httpJsonPathError": "Failed to parse json data",
"httpEmptyResult": "Result is empty",
"httpMatchSuccess": "Response data match successfully",
"httpMatchFail": "Failed to match response data",
+ "httpJsonPathFail": "JSONPath did not match expected value",
"webhookSendSuccess": "Webhook notification sent successfully",
"telegramRequiresBotTokenAndChatId": "Telegram notifications require both botToken and chatId",
"webhookUrlRequired": "Webhook URL is required",
@@ -162,5 +164,18 @@
"monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n",
"sendTestEmail": "Test email sent successfully",
"errorForValidEmailAddress": "A valid recipient email address is required.",
- "testEmailSubject": "Test E-mail from Checkmate"
+ "testEmailSubject": "Test E-mail from Checkmate",
+ "discordNotification": {
+ "monitor": "Monitor",
+ "status": "Status",
+ "statusCode": "Status Code",
+ "time": "Time",
+ "up": "UP",
+ "down": "DOWN",
+ "checkmate": "Checkmate",
+ "url": "URL",
+ "unknown": "Unknown",
+ "uptimeAlert": "Uptime Alert: One of your monitors is back online",
+ "downtimeAlert": "Downtime Alert: One of your monitors went offline"
+ }
}
diff --git a/server/src/middleware/handleErrors.js b/server/src/middleware/v1/handleErrors.js
similarity index 71%
rename from server/src/middleware/handleErrors.js
rename to server/src/middleware/v1/handleErrors.js
index b0ded5ab7..43671fbe2 100755
--- a/server/src/middleware/handleErrors.js
+++ b/server/src/middleware/v1/handleErrors.js
@@ -1,6 +1,6 @@
-import { logger } from "../utils/logger.js";
-import ServiceRegistry from "../service/system/serviceRegistry.js";
-import StringService from "../service/system/stringService.js";
+import { logger } from "../../utils/logger.js";
+import ServiceRegistry from "../../service/v1/system/serviceRegistry.js";
+import StringService from "../../service/v1/system/stringService.js";
const handleErrors = (error, req, res, next) => {
const status = error.status || 500;
diff --git a/server/src/middleware/isAllowed.js b/server/src/middleware/v1/isAllowed.js
similarity index 87%
rename from server/src/middleware/isAllowed.js
rename to server/src/middleware/v1/isAllowed.js
index 5d61974be..cc240f531 100755
--- a/server/src/middleware/isAllowed.js
+++ b/server/src/middleware/v1/isAllowed.js
@@ -1,9 +1,9 @@
import jwt from "jsonwebtoken";
const TOKEN_PREFIX = "Bearer ";
const SERVICE_NAME = "allowedRoles";
-import ServiceRegistry from "../service/system/serviceRegistry.js";
-import StringService from "../service/system/stringService.js";
-import SettingsService from "../service/system/settingsService.js";
+import ServiceRegistry from "../../service/v1/system/serviceRegistry.js";
+import StringService from "../../service/v1/system/stringService.js";
+import SettingsService from "../../service/v1/system/settingsService.js";
const isAllowed = (allowedRoles) => {
return (req, res, next) => {
diff --git a/server/src/middleware/languageMiddleware.js b/server/src/middleware/v1/languageMiddleware.js
similarity index 93%
rename from server/src/middleware/languageMiddleware.js
rename to server/src/middleware/v1/languageMiddleware.js
index 6293dc837..819858a21 100755
--- a/server/src/middleware/languageMiddleware.js
+++ b/server/src/middleware/v1/languageMiddleware.js
@@ -1,4 +1,4 @@
-import { logger } from "../utils/logger.js";
+import { logger } from "../../utils/logger.js";
const languageMiddleware = (stringService, translationService) => async (req, res, next) => {
try {
diff --git a/server/src/middleware/rateLimiter.js b/server/src/middleware/v1/rateLimiter.js
similarity index 100%
rename from server/src/middleware/rateLimiter.js
rename to server/src/middleware/v1/rateLimiter.js
diff --git a/server/src/middleware/responseHandler.js b/server/src/middleware/v1/responseHandler.js
similarity index 100%
rename from server/src/middleware/responseHandler.js
rename to server/src/middleware/v1/responseHandler.js
diff --git a/server/src/middleware/sanitization.js b/server/src/middleware/v1/sanitization.js
similarity index 100%
rename from server/src/middleware/sanitization.js
rename to server/src/middleware/v1/sanitization.js
diff --git a/server/src/middleware/verifyJWT.js b/server/src/middleware/v1/verifyJWT.js
similarity index 87%
rename from server/src/middleware/verifyJWT.js
rename to server/src/middleware/v1/verifyJWT.js
index d01db0a97..b57391dfd 100755
--- a/server/src/middleware/verifyJWT.js
+++ b/server/src/middleware/v1/verifyJWT.js
@@ -1,7 +1,7 @@
import jwt from "jsonwebtoken";
-import ServiceRegistry from "../service/system/serviceRegistry.js";
-import SettingsService from "../service/system/settingsService.js";
-import StringService from "../service/system/stringService.js";
+import ServiceRegistry from "../../service/v1/system/serviceRegistry.js";
+import SettingsService from "../../service/v1/system/settingsService.js";
+import StringService from "../../service/v1/system/stringService.js";
const SERVICE_NAME = "verifyJWT";
const TOKEN_PREFIX = "Bearer ";
diff --git a/server/src/middleware/verifyOwnership.js b/server/src/middleware/v1/verifyOwnership.js
similarity index 88%
rename from server/src/middleware/verifyOwnership.js
rename to server/src/middleware/v1/verifyOwnership.js
index 4431b0c26..741fb6436 100755
--- a/server/src/middleware/verifyOwnership.js
+++ b/server/src/middleware/v1/verifyOwnership.js
@@ -1,6 +1,6 @@
-import { logger } from "../utils/logger.js";
-import ServiceRegistry from "../service/system/serviceRegistry.js";
-import StringService from "../service/system/stringService.js";
+import { logger } from "../../utils/logger.js";
+import ServiceRegistry from "../../service/v1/system/serviceRegistry.js";
+import StringService from "../../service/v1/system/stringService.js";
import { ObjectId } from "mongodb";
const SERVICE_NAME = "verifyOwnership";
diff --git a/server/src/middleware/verifySuperAdmin.js b/server/src/middleware/v1/verifySuperAdmin.js
similarity index 97%
rename from server/src/middleware/verifySuperAdmin.js
rename to server/src/middleware/v1/verifySuperAdmin.js
index a5a93eede..41ac0a3e0 100755
--- a/server/src/middleware/verifySuperAdmin.js
+++ b/server/src/middleware/v1/verifySuperAdmin.js
@@ -1,5 +1,5 @@
const jwt = require("jsonwebtoken");
-const logger = require("../utils/logger");
+const logger = require("../../utils/logger.js");
const SERVICE_NAME = "verifyAdmin";
const TOKEN_PREFIX = "Bearer ";
import ServiceRegistry from "../service/serviceRegistry.js";
diff --git a/server/src/middleware/verifyTeamAccess.js b/server/src/middleware/v1/verifyTeamAccess.js
similarity index 100%
rename from server/src/middleware/verifyTeamAccess.js
rename to server/src/middleware/v1/verifyTeamAccess.js
diff --git a/server/src/middleware/v2/VerifyPermissions.ts b/server/src/middleware/v2/VerifyPermissions.ts
new file mode 100644
index 000000000..64d573e00
--- /dev/null
+++ b/server/src/middleware/v2/VerifyPermissions.ts
@@ -0,0 +1,77 @@
+import { Request, Response, NextFunction } from "express";
+import ApiError from "../../utils/ApiError.js";
+import { User, IUser, Role, IRole } from "../../db/v2/models/index.js";
+
+const rolesCache = new Map();
+// const CACHE_TTL = 30 * 60 * 1000; // 30 minutes
+const CACHE_TTL = 1; // 30 minutes
+const MAX_CACHE_SIZE = 1000;
+
+const getCachedRoles = async (userId: string) => {
+ if (rolesCache.size >= MAX_CACHE_SIZE) {
+ const oldestKey = rolesCache.keys().next().value;
+ if (!oldestKey) return null;
+ rolesCache.delete(oldestKey);
+ }
+
+ const cached = rolesCache.get(userId);
+
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
+ return cached.roles;
+ }
+
+ const user: IUser | null = await User.findById(userId);
+ if (!user) {
+ return null;
+ }
+
+ const roles = await Role.find({ _id: { $in: user.roles } });
+ rolesCache.set(userId, { roles, timestamp: Date.now() });
+ return roles;
+};
+
+const hasPermission = (roles: IRole[], requiredPermissions: string[]) => {
+ const userPermissions = [...new Set(roles.flatMap((role) => role.permissions))];
+
+ if (userPermissions.includes("*")) return true;
+
+ const matches = (requiredPermission: string, userPermission: string) => {
+ if (userPermission === requiredPermission) return true;
+ if (userPermission.endsWith(".*")) {
+ const prefix = userPermission.slice(0, -2);
+ return requiredPermission.startsWith(prefix + ".");
+ }
+ return false;
+ };
+
+ return requiredPermissions.every((requiredPermission) => {
+ return userPermissions.some((userPermission) => matches(requiredPermission, userPermission));
+ });
+};
+
+const verifyPermission = (resourceActions: string[]) => {
+ return async (req: Request, res: Response, next: NextFunction) => {
+ const tokenizedUser = req.user;
+
+ if (!tokenizedUser) {
+ throw new ApiError("No user", 400);
+ }
+
+ const userId = tokenizedUser.sub;
+ if (!userId) {
+ throw new ApiError("No user ID", 400);
+ }
+
+ const userRoles = await getCachedRoles(userId);
+ if (!userRoles) {
+ throw new ApiError("User roles not found", 400);
+ }
+ const allowed = hasPermission(userRoles, resourceActions);
+ if (!allowed) {
+ throw new ApiError("Insufficient permissions", 403);
+ }
+ next();
+ };
+};
+
+export { verifyPermission };
diff --git a/server/src/middleware/v2/VerifyToken.ts b/server/src/middleware/v2/VerifyToken.ts
new file mode 100644
index 000000000..7dabd51cc
--- /dev/null
+++ b/server/src/middleware/v2/VerifyToken.ts
@@ -0,0 +1,21 @@
+import { Request, Response, NextFunction } from "express";
+import { decode } from "../../utils/JWTUtils.js";
+import ApiError from "../../utils/ApiError.js";
+const verifyToken = (req: Request, res: Response, next: NextFunction) => {
+ const token = req.cookies.token;
+
+ if (!token) {
+ const error = new ApiError("No token provided", 401);
+ return next(error);
+ }
+
+ try {
+ const decoded = decode(token);
+ req.user = decoded;
+ next();
+ } catch (error) {
+ next(error);
+ }
+};
+
+export { verifyToken };
diff --git a/server/src/routes/announcementsRoute.js b/server/src/routes/v1/announcementsRoute.js
similarity index 100%
rename from server/src/routes/announcementsRoute.js
rename to server/src/routes/v1/announcementsRoute.js
diff --git a/server/src/routes/authRoute.js b/server/src/routes/v1/authRoute.js
similarity index 83%
rename from server/src/routes/authRoute.js
rename to server/src/routes/v1/authRoute.js
index e790c60b0..63b4c4201 100755
--- a/server/src/routes/authRoute.js
+++ b/server/src/routes/v1/authRoute.js
@@ -1,9 +1,7 @@
import { Router } from "express";
-import { verifyJWT } from "../middleware/verifyJWT.js";
-import { verifyOwnership } from "../middleware/verifyOwnership.js";
-import { isAllowed } from "../middleware/isAllowed.js";
+import { verifyJWT } from "../../middleware/v1/verifyJWT.js";
+import { isAllowed } from "../../middleware/v1/isAllowed.js";
import multer from "multer";
-import User from "../db/models/User.js";
const upload = multer();
@@ -27,6 +25,7 @@ class AuthRoutes {
this.router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getAllUsers);
this.router.get("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.getUserById);
this.router.put("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserById);
+ this.router.put("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserPasswordById);
this.router.put("/user", verifyJWT, upload.single("profileImage"), this.authController.editUser);
this.router.delete("/user", verifyJWT, this.authController.deleteUser);
diff --git a/server/src/routes/checkRoute.js b/server/src/routes/v1/checkRoute.js
similarity index 76%
rename from server/src/routes/checkRoute.js
rename to server/src/routes/v1/checkRoute.js
index 8bdb2d4fb..78f6b8a42 100755
--- a/server/src/routes/checkRoute.js
+++ b/server/src/routes/v1/checkRoute.js
@@ -1,9 +1,6 @@
import { Router } from "express";
-import { verifyOwnership } from "../middleware/verifyOwnership.js";
-import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js";
-import { isAllowed } from "../middleware/isAllowed.js";
-import Monitor from "../db/models/Monitor.js";
-import Check from "../db/models/Check.js";
+
+import { isAllowed } from "../../middleware/v1/isAllowed.js";
class CheckRoutes {
constructor(checkController) {
diff --git a/server/src/routes/diagnosticRoute.js b/server/src/routes/v1/diagnosticRoute.js
similarity index 76%
rename from server/src/routes/diagnosticRoute.js
rename to server/src/routes/v1/diagnosticRoute.js
index a15f5a038..2cf481e08 100755
--- a/server/src/routes/diagnosticRoute.js
+++ b/server/src/routes/v1/diagnosticRoute.js
@@ -1,6 +1,6 @@
import { Router } from "express";
-import { verifyJWT } from "../middleware/verifyJWT.js";
-import { isAllowed } from "../middleware/isAllowed.js";
+import { verifyJWT } from "../../middleware/v1/verifyJWT.js";
+import { isAllowed } from "../../middleware/v1/isAllowed.js";
class DiagnosticRoutes {
constructor(diagnosticController) {
diff --git a/server/src/routes/inviteRoute.js b/server/src/routes/v1/inviteRoute.js
similarity index 82%
rename from server/src/routes/inviteRoute.js
rename to server/src/routes/v1/inviteRoute.js
index d495c5476..a47550536 100755
--- a/server/src/routes/inviteRoute.js
+++ b/server/src/routes/v1/inviteRoute.js
@@ -1,6 +1,6 @@
import { Router } from "express";
-import { verifyJWT } from "../middleware/verifyJWT.js";
-import { isAllowed } from "../middleware/isAllowed.js";
+import { verifyJWT } from "../../middleware/v1/verifyJWT.js";
+import { isAllowed } from "../../middleware/v1/isAllowed.js";
class InviteRoutes {
constructor(inviteController) {
diff --git a/server/src/routes/logRoutes.js b/server/src/routes/v1/logRoutes.js
similarity index 84%
rename from server/src/routes/logRoutes.js
rename to server/src/routes/v1/logRoutes.js
index 867826dde..43da8b75a 100755
--- a/server/src/routes/logRoutes.js
+++ b/server/src/routes/v1/logRoutes.js
@@ -1,5 +1,5 @@
import { Router } from "express";
-import { isAllowed } from "../middleware/isAllowed.js";
+import { isAllowed } from "../../middleware/v1/isAllowed.js";
class LogRoutes {
constructor(logController) {
this.router = Router();
diff --git a/server/src/routes/maintenanceWindowRoute.js b/server/src/routes/v1/maintenanceWindowRoute.js
similarity index 91%
rename from server/src/routes/maintenanceWindowRoute.js
rename to server/src/routes/v1/maintenanceWindowRoute.js
index 774434ed6..3df040994 100755
--- a/server/src/routes/maintenanceWindowRoute.js
+++ b/server/src/routes/v1/maintenanceWindowRoute.js
@@ -1,5 +1,5 @@
import { Router } from "express";
-import MaintenanceWindow from "../db/models/MaintenanceWindow.js";
+import MaintenanceWindow from "../../db/v1/models/MaintenanceWindow.js";
class MaintenanceWindowRoutes {
constructor(maintenanceWindowController) {
this.router = Router();
diff --git a/server/src/routes/monitorRoute.js b/server/src/routes/v1/monitorRoute.js
similarity index 94%
rename from server/src/routes/monitorRoute.js
rename to server/src/routes/v1/monitorRoute.js
index 043d361d1..107964f6b 100755
--- a/server/src/routes/monitorRoute.js
+++ b/server/src/routes/v1/monitorRoute.js
@@ -1,7 +1,7 @@
import { Router } from "express";
-import { isAllowed } from "../middleware/isAllowed.js";
+import { isAllowed } from "../../middleware/v1/isAllowed.js";
import multer from "multer";
-import { fetchMonitorCertificate } from "../controllers/controllerUtils.js";
+import { fetchMonitorCertificate } from "../../controllers/v1/controllerUtils.js";
const upload = multer({
storage: multer.memoryStorage(), // Store file in memory as Buffer
});
diff --git a/server/src/routes/notificationRoute.js b/server/src/routes/v1/notificationRoute.js
similarity index 100%
rename from server/src/routes/notificationRoute.js
rename to server/src/routes/v1/notificationRoute.js
diff --git a/server/src/routes/queueRoute.js b/server/src/routes/v1/queueRoute.js
similarity index 93%
rename from server/src/routes/queueRoute.js
rename to server/src/routes/v1/queueRoute.js
index 55e829b91..c6cfa05da 100755
--- a/server/src/routes/queueRoute.js
+++ b/server/src/routes/v1/queueRoute.js
@@ -1,5 +1,5 @@
import { Router } from "express";
-import { isAllowed } from "../middleware/isAllowed.js";
+import { isAllowed } from "../../middleware/v1/isAllowed.js";
class QueueRoutes {
constructor(queueController) {
this.router = Router();
diff --git a/server/src/routes/settingsRoute.js b/server/src/routes/v1/settingsRoute.js
similarity index 90%
rename from server/src/routes/settingsRoute.js
rename to server/src/routes/v1/settingsRoute.js
index 88eb67e65..e88186af2 100755
--- a/server/src/routes/settingsRoute.js
+++ b/server/src/routes/v1/settingsRoute.js
@@ -1,5 +1,5 @@
import { Router } from "express";
-import { isAllowed } from "../middleware/isAllowed.js";
+import { isAllowed } from "../../middleware/v1/isAllowed.js";
class SettingsRoutes {
constructor(settingsController) {
diff --git a/server/src/routes/statusPageRoute.js b/server/src/routes/v1/statusPageRoute.js
similarity index 93%
rename from server/src/routes/statusPageRoute.js
rename to server/src/routes/v1/statusPageRoute.js
index 53eb21fba..2c3b0b265 100755
--- a/server/src/routes/statusPageRoute.js
+++ b/server/src/routes/v1/statusPageRoute.js
@@ -1,5 +1,5 @@
import { Router } from "express";
-import { verifyJWT } from "../middleware/verifyJWT.js";
+import { verifyJWT } from "../../middleware/v1/verifyJWT.js";
import multer from "multer";
const upload = multer();
diff --git a/server/src/routes/v2/auth.ts b/server/src/routes/v2/auth.ts
new file mode 100644
index 000000000..919d0fd76
--- /dev/null
+++ b/server/src/routes/v2/auth.ts
@@ -0,0 +1,33 @@
+import { Router } from "express";
+
+import express from "express";
+import AuthController from "../../controllers/v2/AuthController.js";
+import { verifyToken } from "../../middleware/v2/VerifyToken.js";
+
+const router = express.Router();
+
+class AuthRoutes {
+ private controller: AuthController;
+ private router: Router;
+ constructor(authController: AuthController) {
+ this.controller = authController;
+ this.router = Router();
+ this.initRoutes();
+ }
+
+ initRoutes = () => {
+ this.router.post("/register", this.controller.register);
+ this.router.post("/register/invite/:token", this.controller.registerWithInvite);
+ this.router.post("/login", this.controller.login);
+ this.router.post("/logout", this.controller.logout);
+ this.router.get("/me", verifyToken, this.controller.me);
+ this.router.post("/cleanup", this.controller.cleanup);
+ this.router.post("/cleanup-monitors", this.controller.cleanMonitors);
+ };
+
+ getRouter() {
+ return this.router;
+ }
+}
+
+export default AuthRoutes;
diff --git a/server/src/routes/v2/invite.ts b/server/src/routes/v2/invite.ts
new file mode 100644
index 000000000..8066d0c77
--- /dev/null
+++ b/server/src/routes/v2/invite.ts
@@ -0,0 +1,30 @@
+import { Router } from "express";
+import InviteController from "../../controllers/v2/InviteController.js";
+import { verifyToken } from "../../middleware/v2/VerifyToken.js";
+import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js";
+
+class InviteRoutes {
+ private router;
+ private controller;
+ constructor(inviteController: InviteController) {
+ this.router = Router();
+ this.controller = inviteController;
+ this.initRoutes();
+ }
+
+ initRoutes = () => {
+ this.router.post("/", verifyToken, verifyPermission(["invite.create"]), this.controller.create);
+
+ this.router.get("/", verifyToken, verifyPermission(["invite.view"]), this.controller.getAll);
+
+ this.router.get("/:token", verifyToken, verifyPermission(["invite.view"]), this.controller.get);
+
+ this.router.delete("/:id", verifyToken, verifyPermission(["invite.delete"]), this.controller.delete);
+ };
+
+ getRouter() {
+ return this.router;
+ }
+}
+
+export default InviteRoutes;
diff --git a/server/src/routes/v2/maintenance.ts b/server/src/routes/v2/maintenance.ts
new file mode 100644
index 000000000..62bc5f1cf
--- /dev/null
+++ b/server/src/routes/v2/maintenance.ts
@@ -0,0 +1,34 @@
+import { Router } from "express";
+import MaintenanceController from "../../controllers/v2/MaintenanceController.js";
+import { verifyToken } from "../../middleware/v2/VerifyToken.js";
+import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js";
+
+class MaintenanceRoutes {
+ private router;
+ private controller;
+ constructor(maintenanceController: MaintenanceController) {
+ this.router = Router();
+ this.controller = maintenanceController;
+ this.initRoutes();
+ }
+
+ initRoutes = () => {
+ this.router.post("/", verifyToken, verifyPermission(["maintenance.create"]), this.controller.create);
+
+ this.router.get("/", verifyToken, verifyPermission(["maintenance.view"]), this.controller.getAll);
+
+ this.router.patch("/:id/active", verifyToken, verifyPermission(["maintenance.update"]), this.controller.toggleActive);
+
+ this.router.patch("/:id", verifyToken, verifyPermission(["maintenance.update"]), this.controller.update);
+
+ this.router.get("/:id", verifyToken, verifyPermission(["maintenance.view"]), this.controller.get);
+
+ this.router.delete("/:id", verifyToken, verifyPermission(["maintenance.delete"]), this.controller.delete);
+ };
+
+ getRouter() {
+ return this.router;
+ }
+}
+
+export default MaintenanceRoutes;
diff --git a/server/src/routes/v2/monitors.ts b/server/src/routes/v2/monitors.ts
new file mode 100644
index 000000000..61e5b61d6
--- /dev/null
+++ b/server/src/routes/v2/monitors.ts
@@ -0,0 +1,36 @@
+import { Router } from "express";
+import MonitorController from "../../controllers/v2/MonitorController.js";
+import { verifyToken } from "../../middleware/v2/VerifyToken.js";
+import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js";
+
+class MonitorRoutes {
+ private router;
+ private controller;
+ constructor(monitorController: MonitorController) {
+ this.router = Router();
+ this.controller = monitorController;
+ this.initRoutes();
+ }
+
+ initRoutes = () => {
+ this.router.post("/", verifyToken, verifyPermission(["monitors.create"]), this.controller.create);
+
+ this.router.get("/", verifyToken, verifyPermission(["monitors.view"]), this.controller.getAll);
+
+ this.router.get("/:id/checks", verifyToken, verifyPermission(["monitors.view"]), this.controller.getChecks);
+
+ this.router.patch("/:id/active", verifyToken, verifyPermission(["monitors.update"]), this.controller.toggleActive);
+
+ this.router.get("/:id", verifyToken, verifyPermission(["monitors.view"]), this.controller.get);
+
+ this.router.patch("/:id", verifyToken, verifyPermission(["monitors.update"]), this.controller.update);
+
+ this.router.delete("/:id", verifyToken, verifyPermission(["monitors.delete"]), this.controller.delete);
+ };
+
+ getRouter() {
+ return this.router;
+ }
+}
+
+export default MonitorRoutes;
diff --git a/server/src/routes/v2/notificationChannels.ts b/server/src/routes/v2/notificationChannels.ts
new file mode 100644
index 000000000..1db32902a
--- /dev/null
+++ b/server/src/routes/v2/notificationChannels.ts
@@ -0,0 +1,34 @@
+import { Router } from "express";
+import NotificationController from "../../controllers/v2/NotificationChannelController.js";
+import { verifyToken } from "../../middleware/v2/VerifyToken.js";
+import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js";
+
+class NotificationChannelRoutes {
+ private router;
+ private controller;
+ constructor(notificationController: NotificationController) {
+ this.router = Router();
+ this.controller = notificationController;
+ this.initRoutes();
+ }
+
+ initRoutes = () => {
+ this.router.post("/", verifyToken, verifyPermission(["notifications.create"]), this.controller.create);
+
+ this.router.get("/", verifyToken, verifyPermission(["notifications.view"]), this.controller.getAll);
+
+ this.router.patch("/:id/active", verifyToken, verifyPermission(["notifications.update"]), this.controller.toggleActive);
+
+ this.router.patch("/:id", verifyToken, verifyPermission(["notifications.update"]), this.controller.update);
+
+ this.router.get("/:id", verifyToken, verifyPermission(["notifications.view"]), this.controller.get);
+
+ this.router.delete("/:id", verifyToken, verifyPermission(["notifications.delete"]), this.controller.delete);
+ };
+
+ getRouter() {
+ return this.router;
+ }
+}
+
+export default NotificationChannelRoutes;
diff --git a/server/src/routes/v2/queue.ts b/server/src/routes/v2/queue.ts
new file mode 100644
index 000000000..428776e61
--- /dev/null
+++ b/server/src/routes/v2/queue.ts
@@ -0,0 +1,24 @@
+import QueueController from "../../controllers/v2/QueueController.js";
+import { Router } from "express";
+
+class QueueRoutes {
+ private router;
+ private controller;
+ constructor(queueController: QueueController) {
+ this.router = Router();
+ this.controller = queueController;
+ this.initRoutes();
+ }
+
+ initRoutes() {
+ this.router.get("/jobs", this.controller.getJobs);
+ this.router.get("/metrics", this.controller.getMetrics);
+ this.router.post("/flush", this.controller.flush);
+ }
+
+ getRouter() {
+ return this.router;
+ }
+}
+
+export default QueueRoutes;
diff --git a/server/src/service/business/checkService.js b/server/src/service/v1/business/checkService.js
similarity index 100%
rename from server/src/service/business/checkService.js
rename to server/src/service/v1/business/checkService.js
diff --git a/server/src/service/business/diagnosticService.js b/server/src/service/v1/business/diagnosticService.js
similarity index 100%
rename from server/src/service/business/diagnosticService.js
rename to server/src/service/v1/business/diagnosticService.js
diff --git a/server/src/service/business/inviteService.js b/server/src/service/v1/business/inviteService.js
similarity index 100%
rename from server/src/service/business/inviteService.js
rename to server/src/service/v1/business/inviteService.js
diff --git a/server/src/service/business/maintenanceWindowService.js b/server/src/service/v1/business/maintenanceWindowService.js
similarity index 100%
rename from server/src/service/business/maintenanceWindowService.js
rename to server/src/service/v1/business/maintenanceWindowService.js
diff --git a/server/src/service/business/monitorService.js b/server/src/service/v1/business/monitorService.js
similarity index 99%
rename from server/src/service/business/monitorService.js
rename to server/src/service/v1/business/monitorService.js
index 637870b71..061234102 100644
--- a/server/src/service/business/monitorService.js
+++ b/server/src/service/v1/business/monitorService.js
@@ -1,4 +1,4 @@
-import { createMonitorsBodyValidation } from "../../validation/joi.js";
+import { createMonitorsBodyValidation } from "../../../validation/joi.js";
const SERVICE_NAME = "MonitorService";
class MonitorService {
diff --git a/server/src/service/business/userService.js b/server/src/service/v1/business/userService.js
similarity index 97%
rename from server/src/service/business/userService.js
rename to server/src/service/v1/business/userService.js
index 1e9abe4b2..7ef188243 100644
--- a/server/src/service/business/userService.js
+++ b/server/src/service/v1/business/userService.js
@@ -238,5 +238,9 @@ class UserService {
editUserById = async (userId, user) => {
await this.db.userModule.editUserById(userId, user);
};
+ setPasswordByUserId = async (userId, password) => {
+ const updatedUser = await this.db.userModule.updateUser({ userId: userId, user: { password: password }, file: null });
+ return updatedUser;
+ };
}
export default UserService;
diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js
similarity index 100%
rename from server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js
rename to server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js
diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js
similarity index 84%
rename from server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js
rename to server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js
index ac8da9169..2833a59f5 100644
--- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js
+++ b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js
@@ -5,11 +5,11 @@ class SuperSimpleQueueHelper {
/**
* @param {{
- * db: import("../database").Database,
- * logger: import("../logger").Logger,
- * networkService: import("../networkService").NetworkService,
- * statusService: import("../statusService").StatusService,
- * notificationService: import("../notificationService").NotificationService
+ * db: import("../database.js").Database,
+ * logger: import("../logger.js").Logger,
+ * networkService: import("../networkService.js").NetworkService,
+ * statusService: import("../statusService.js").StatusService,
+ * notificationService: import("../notificationService.js").NotificationService
* }}
*/
constructor({ db, logger, networkService, statusService, notificationService }) {
@@ -28,11 +28,12 @@ class SuperSimpleQueueHelper {
return async (monitor) => {
try {
const monitorId = monitor._id;
+ const teamId = monitor.teamId;
if (!monitorId) {
throw new Error("No monitor id");
}
- const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId);
+ const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId);
if (maintenanceWindowActive) {
this.logger.info({
message: `Monitor ${monitorId} is in maintenance window`,
@@ -77,8 +78,11 @@ class SuperSimpleQueueHelper {
};
};
- async isInMaintenanceWindow(monitorId) {
- const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId(monitorId);
+ async isInMaintenanceWindow(monitorId, teamId) {
+ const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({
+ monitorId: monitorId.toString(),
+ teamId: teamId.toString(),
+ });
// Check for active maintenance window:
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
if (window.active) {
diff --git a/server/src/service/infrastructure/bufferService.js b/server/src/service/v1/infrastructure/bufferService.js
similarity index 92%
rename from server/src/service/infrastructure/bufferService.js
rename to server/src/service/v1/infrastructure/bufferService.js
index ed48cc654..459d25af3 100755
--- a/server/src/service/infrastructure/bufferService.js
+++ b/server/src/service/v1/infrastructure/bufferService.js
@@ -64,11 +64,11 @@ class BufferService {
});
}
this.buffer = [];
- this.logger.debug({
- message: `Flushed ${items} items`,
- service: this.SERVICE_NAME,
- method: "flushBuffer",
- });
+ // this.logger.debug({
+ // message: `Flushed ${items} items`,
+ // service: this.SERVICE_NAME,
+ // method: "flushBuffer",
+ // });
items = 0;
}
}
diff --git a/server/src/service/infrastructure/emailService.js b/server/src/service/v1/infrastructure/emailService.js
similarity index 100%
rename from server/src/service/infrastructure/emailService.js
rename to server/src/service/v1/infrastructure/emailService.js
diff --git a/server/src/service/infrastructure/errorService.js b/server/src/service/v1/infrastructure/errorService.js
similarity index 100%
rename from server/src/service/infrastructure/errorService.js
rename to server/src/service/v1/infrastructure/errorService.js
diff --git a/server/src/service/infrastructure/networkService.js b/server/src/service/v1/infrastructure/networkService.js
similarity index 87%
rename from server/src/service/infrastructure/networkService.js
rename to server/src/service/v1/infrastructure/networkService.js
index 50cf16b85..a92b6544b 100644
--- a/server/src/service/infrastructure/networkService.js
+++ b/server/src/service/v1/infrastructure/networkService.js
@@ -1,5 +1,3 @@
-import { match } from "assert";
-
const SERVICE_NAME = "NetworkService";
class NetworkService {
@@ -72,7 +70,12 @@ class NetworkService {
throw new Error("Monitor URL is required");
}
- const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(monitor.url));
+ const rawUrl = monitor.url;
+ const sanitizedHost = rawUrl
+ .replace(/^https?:\/\//, "")
+ .replace(/\/.*$/, "")
+ .replace(/:.*/, "");
+ const { response, error } = await this.timeRequest(() => this.ping.promise.probe(sanitizedHost));
if (!response) {
if (error) {
@@ -86,7 +89,7 @@ class NetworkService {
type: "ping",
status: response.alive,
code: 200,
- responseTime,
+ responseTime: response.time,
message: "Success",
payload: response,
};
@@ -107,7 +110,7 @@ class NetworkService {
}
async requestHttp(monitor) {
- const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
+ const { url, secret, _id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
const httpResponse = {
monitorId: _id,
teamId: teamId,
@@ -156,33 +159,61 @@ class NetworkService {
return httpResponse;
}
- if (expectedValue) {
+ if (expectedValue && !jsonPath) {
let ok = false;
if (matchMethod === "equal") ok = payload === expectedValue;
- if (matchMethod === "include") ok = payload.includes(expectedValue);
- if (matchMethod === "regex") ok = new RegExp(expectedValue).test(payload);
+ if (matchMethod === "include" && typeof payload === "string") ok = payload.includes(expectedValue);
+ if (matchMethod === "regex" && typeof payload === "string") ok = new RegExp(expectedValue).test(payload);
if (ok === true) {
return httpResponse;
} else {
httpResponse.code = 500;
httpResponse.status = false;
+ httpResponse.message = this.stringService.httpExpectedValueFail;
return httpResponse;
}
}
if (jsonPath) {
const contentType = response.headers["content-type"];
-
const isJson = contentType?.includes("application/json");
if (!isJson) {
httpResponse.status = false;
httpResponse.message = this.stringService.httpNotJson;
return httpResponse;
}
-
try {
- this.jmespath.search(payload, jsonPath);
+ const extracted = this.jmespath.search(payload, jsonPath);
+ if (expectedValue) {
+ let ok = false;
+ if (matchMethod === "equal") ok = extracted === expectedValue;
+ if (matchMethod === "include" && typeof extracted === "string") ok = extracted.includes(expectedValue);
+ if (matchMethod === "regex" && typeof extracted === "string") ok = new RegExp(expectedValue).test(extracted);
+
+ if (ok) {
+ httpResponse.extracted = extracted;
+ return httpResponse;
+ } else {
+ httpResponse.status = false;
+ httpResponse.code = 500;
+ httpResponse.message = this.stringService.httpJsonPathFail;
+ httpResponse.extracted = extracted;
+ return httpResponse;
+ }
+ } else {
+ const isFalsey = extracted === false || extracted === "false" || extracted === undefined || extracted === null;
+ if (!isFalsey) {
+ httpResponse.extracted = extracted;
+ return httpResponse;
+ } else {
+ httpResponse.status = false;
+ httpResponse.code = 500;
+ httpResponse.message = this.stringService.httpJsonPathFail;
+ httpResponse.extracted = extracted;
+ return httpResponse;
+ }
+ }
} catch {
httpResponse.status = false;
httpResponse.message = this.stringService.httpJsonPathError;
@@ -377,7 +408,7 @@ class NetworkService {
message: this.stringService.portSuccess,
monitorId: monitor._id,
type: monitor.type,
- responseTime,
+ responseTime: responseTime,
};
if (error) {
diff --git a/server/src/service/infrastructure/notificationService.js b/server/src/service/v1/infrastructure/notificationService.js
similarity index 82%
rename from server/src/service/infrastructure/notificationService.js
rename to server/src/service/v1/infrastructure/notificationService.js
index 8c34519f3..6c5b8b25e 100644
--- a/server/src/service/infrastructure/notificationService.js
+++ b/server/src/service/v1/infrastructure/notificationService.js
@@ -16,7 +16,7 @@ class NotificationService {
return NotificationService.SERVICE_NAME;
}
- sendNotification = async ({ notification, subject, content, html }) => {
+ sendNotification = async ({ notification, subject, content, html, discordContent = null, webhookBody = null }) => {
const { type, address } = notification;
if (type === "email") {
@@ -28,9 +28,11 @@ class NotificationService {
// Create a body for webhooks
let body = { text: content };
if (type === "discord") {
- body = { content };
+ body = !discordContent ? { content } : discordContent;
+ }
+ if (type === "webhook") {
+ body = !webhookBody ? { content } : webhookBody;
}
-
if (type === "slack" || type === "discord" || type === "webhook") {
const response = await this.networkService.requestWebhook(type, address, body);
return response.status;
@@ -55,7 +57,6 @@ class NotificationService {
const notificationIDs = networkResponse.monitor?.notifications ?? [];
if (notificationIDs.length === 0) return false;
-
if (networkResponse.monitor.type === "hardware") {
const thresholds = networkResponse?.monitor?.thresholds;
@@ -63,30 +64,29 @@ class NotificationService {
const metrics = networkResponse?.payload?.data ?? null;
if (metrics === null) return false; // No metrics, we're done
- const alerts = await this.notificationUtils.buildHardwareAlerts(networkResponse);
+ const [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse);
if (alerts.length === 0) return false;
const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts);
- const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts);
-
- const success = await this.notifyAll({ notificationIDs, subject, html, content });
+ const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor);
+ const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor);
+ const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody });
return success;
}
// Status monitors
const { subject, html } = await this.notificationUtils.buildStatusEmail(networkResponse);
- const content = await this.notificationUtils.buildWebhookMessage(networkResponse);
- const success = this.notifyAll({ notificationIDs, subject, html, content });
+ const [content, discordContent] = await this.notificationUtils.buildWebhookMessage(networkResponse);
+ const success = this.notifyAll({ notificationIDs, subject, html, content, discordContent });
return success;
}
- async notifyAll({ notificationIDs, subject, html, content }) {
+ async notifyAll({ notificationIDs, subject, html, content, discordContent = null, webhookBody = null }) {
const notifications = await this.db.notificationModule.getNotificationsByIds(notificationIDs);
-
// Map each notification to a test promise
const promises = notifications.map(async (notification) => {
try {
- await this.sendNotification({ notification, subject, content, html });
+ await this.sendNotification({ notification, subject, content, html, discordContent, webhookBody });
return true;
} catch (err) {
return false;
diff --git a/server/src/service/infrastructure/notificationUtils.js b/server/src/service/v1/infrastructure/notificationUtils.js
similarity index 64%
rename from server/src/service/infrastructure/notificationUtils.js
rename to server/src/service/v1/infrastructure/notificationUtils.js
index 22c7f4df7..d6b92cc0e 100644
--- a/server/src/service/infrastructure/notificationUtils.js
+++ b/server/src/service/v1/infrastructure/notificationUtils.js
@@ -70,7 +70,25 @@ class NotificationUtils {
.replace("{time}", formattedTime)
.replace("{code}", code || "Unknown");
}
- return messageText;
+ const dn = this.stringService.discordNotification;
+ let discordMessageText = {
+ embeds: [
+ {
+ title: status ? dn.uptimeAlert : dn.downtimeAlert,
+ color: status ? 5763719 : 15548997,
+
+ fields: [
+ { name: dn.monitor, value: monitor?.name ?? dn.unknown, inline: true },
+ { name: dn.status, value: status ? dn.up : dn.down, inline: true },
+ { name: dn.statusCode, value: String(code ?? dn.unknown), inline: true },
+ { name: dn.time, value: formattedTime, inline: true },
+ { name: dn.url, value: monitor?.url ?? dn.unknown, inline: false },
+ ],
+ footer: { text: dn.checkmate },
+ },
+ ],
+ };
+ return [messageText, discordMessageText];
};
buildHardwareAlerts = async (networkResponse) => {
@@ -88,6 +106,43 @@ class NotificationUtils {
};
const alertsToSend = [];
+ const discordEmbeds = [];
+ const monitorInfoFields = [
+ { name: "Monitor", value: monitor.name, inline: true },
+ { name: "URL", value: monitor.url, inline: false },
+ ];
+ const formatDiscordAlert = {
+ cpu: () => ({
+ title: "CPU alert",
+ description: `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
+ color: 15548997,
+ fields: monitorInfoFields,
+ footer: { text: "Checkmate" },
+ }),
+
+ memory: () => ({
+ title: "Memory alert",
+ description: `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
+ color: 15548997,
+ fields: monitorInfoFields,
+ footer: { text: "Checkmate" },
+ }),
+
+ disk: () => ({
+ title: "Disk alert",
+ description: `Your current disk usage is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
+ color: 15548997,
+ footer: { text: "Checkmate" },
+ fields: [
+ ...monitorInfoFields,
+ ...(Array.isArray(disk) ? disk : []).map((d, idx) => ({
+ name: `Disk ${idx}`,
+ value: `${(d?.usage_percent * 100).toFixed(0)}%`,
+ inline: true,
+ })),
+ ],
+ }),
+ };
const alertTypes = ["cpu", "memory", "disk"];
for (const type of alertTypes) {
// Iterate over each alert type to see if any need to be decremented
@@ -108,11 +163,13 @@ class NotificationUtils {
.join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
};
alertsToSend.push(formatAlert[type]());
+ discordEmbeds.push(formatDiscordAlert[type]());
}
}
}
await monitor.save();
- return alertsToSend;
+ const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null;
+ return [alertsToSend, discordPayload];
};
buildHardwareEmail = async (networkResponse, alerts) => {
@@ -124,8 +181,15 @@ class NotificationUtils {
return { subject, html };
};
- buildHardwareNotificationMessage = (alerts) => {
- return alerts.map((alert) => alert).join("\n");
+ buildHardwareNotificationMessage = (alerts, monitor) => {
+ const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`];
+ const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts] : [];
+ return alertText.map((alert) => alert).join("\n");
+ };
+ buildHardwareWebhookBody = (alerts, monitor) => {
+ const content = alerts.map((alert) => alert).join("\n");
+ const body = { text: content, name: monitor.name, url: monitor.url };
+ return body;
};
}
diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/v1/infrastructure/statusService.js
similarity index 99%
rename from server/src/service/infrastructure/statusService.js
rename to server/src/service/v1/infrastructure/statusService.js
index 72fa359d7..248d1c910 100755
--- a/server/src/service/infrastructure/statusService.js
+++ b/server/src/service/v1/infrastructure/statusService.js
@@ -1,4 +1,4 @@
-import MonitorStats from "../../db/models/MonitorStats.js";
+import MonitorStats from "../../../db/v1/models/MonitorStats.js";
const SERVICE_NAME = "StatusService";
class StatusService {
diff --git a/server/src/service/system/serviceRegistry.js b/server/src/service/v1/system/serviceRegistry.js
similarity index 100%
rename from server/src/service/system/serviceRegistry.js
rename to server/src/service/v1/system/serviceRegistry.js
diff --git a/server/src/service/system/settingsService.js b/server/src/service/v1/system/settingsService.js
similarity index 100%
rename from server/src/service/system/settingsService.js
rename to server/src/service/v1/system/settingsService.js
diff --git a/server/src/service/system/stringService.js b/server/src/service/v1/system/stringService.js
similarity index 99%
rename from server/src/service/system/stringService.js
rename to server/src/service/v1/system/stringService.js
index 5df3410f9..231550be7 100755
--- a/server/src/service/system/stringService.js
+++ b/server/src/service/v1/system/stringService.js
@@ -216,6 +216,10 @@ class StringService {
return this.translationService.getTranslation("monitorDownAlert");
}
+ get discordNotification() {
+ return this.translationService.getTranslation("discordNotification");
+ }
+
getWebhookUnsupportedPlatform(platform) {
return this.translationService.getTranslation("webhookUnsupportedPlatform").replace("{platform}", platform);
}
diff --git a/server/src/service/system/translationService.js b/server/src/service/v1/system/translationService.js
similarity index 100%
rename from server/src/service/system/translationService.js
rename to server/src/service/v1/system/translationService.js
diff --git a/server/src/service/v2/business/AuthService.ts b/server/src/service/v2/business/AuthService.ts
new file mode 100644
index 000000000..67ece226e
--- /dev/null
+++ b/server/src/service/v2/business/AuthService.ts
@@ -0,0 +1,210 @@
+import bcrypt from "bcryptjs";
+import { User, Role, ITokenizedUser, Monitor, Check, NotificationChannel } from "../../../db/v2/models/index.js";
+import ApiError from "../../../utils/ApiError.js";
+import { Types } from "mongoose";
+import { IJobQueue } from "../infrastructure/JobQueue.js";
+
+const SERVICE_NAME = "AuthServiceV2";
+
+export const PERMISSIONS = {
+ users: {
+ all: "users.*",
+ create: "users.create",
+ view: "users.view",
+ update: "users.update",
+ delete: "users.delete",
+ },
+
+ monitors: {
+ all: "monitors.*",
+ create: "monitors.create",
+ view: "monitors.view",
+ update: "monitors.update",
+ delete: "monitors.delete",
+ },
+ notifications: {
+ all: "notifications.*",
+ create: "notifications.create",
+ view: "notifications.view",
+ update: "notifications.update",
+ delete: "notifications.delete",
+ },
+ checks: {
+ all: "checks.*",
+ create: "checks.create",
+ view: "checks.view",
+ update: "checks.update",
+ delete: "checks.delete",
+ },
+ statusPages: {
+ all: "statusPages.*",
+ create: "statusPages.create",
+ view: "statusPages.view",
+ update: "statusPages.update",
+ delete: "statusPages.delete",
+ },
+};
+
+const DEFAULT_ROLES = [
+ {
+ name: "SuperAdmin",
+ description: "Super admin with all permissions",
+ permissions: ["*"],
+ isSystem: true,
+ },
+ {
+ name: "Admin",
+ description: "Admin with full permissions",
+ permissions: [PERMISSIONS.monitors.all, PERMISSIONS.users.all],
+ isSystem: true,
+ },
+ {
+ name: "Manager",
+ description: "Can manage users",
+ permissions: [PERMISSIONS.users.create, PERMISSIONS.users.update, PERMISSIONS.monitors.all],
+ isSystem: true,
+ },
+ {
+ name: "Member",
+ description: "Basic team member",
+ permissions: [PERMISSIONS.users.update, PERMISSIONS.monitors.create, PERMISSIONS.monitors.view, PERMISSIONS.monitors.update],
+ isSystem: true,
+ },
+];
+
+export type RegisterData = {
+ email: string;
+ firstName: string;
+ lastName: string;
+ password: string;
+ roles?: Types.ObjectId[]; // Optional roles for invite-based registration
+};
+
+export type LoginData = {
+ email: string;
+ password: string;
+};
+
+export type AuthResult = ITokenizedUser;
+
+export interface IAuthService {
+ register(signupData: RegisterData): Promise;
+ registerWithInvite(signupData: RegisterData): Promise;
+ login(loginData: LoginData): Promise;
+ cleanup(): Promise;
+ cleanMonitors(): Promise;
+}
+
+class AuthService implements IAuthService {
+ static SERVICE_NAME = SERVICE_NAME;
+
+ private jobQueue: IJobQueue;
+ constructor(jobQueue: IJobQueue) {
+ this.jobQueue = jobQueue;
+ }
+
+ async register(signupData: RegisterData): Promise {
+ const userCount = await User.countDocuments();
+
+ if (userCount > 0) {
+ throw new Error("Registration is closed. Please request an invite.");
+ }
+
+ const { email, firstName, lastName, password } = signupData;
+
+ // Create all default roles
+ const rolePromises = DEFAULT_ROLES.map((roleData) =>
+ new Role({
+ ...roleData,
+ }).save()
+ );
+ const roles = await Promise.all(rolePromises);
+
+ // Hash password and create user
+ const saltRounds = 12;
+ const passwordHash = await bcrypt.hash(password, saltRounds);
+
+ // Find admin role and assign to first user
+ const superAdminRole = roles.find((role) => role.name === "SuperAdmin");
+
+ const user = new User({
+ email,
+ firstName,
+ lastName,
+ passwordHash,
+ roles: [superAdminRole!._id],
+ });
+
+ const savedUser = await user.save();
+ return {
+ sub: savedUser._id.toString(),
+ roles: savedUser.roles.map((role) => role.toString()),
+ };
+ }
+
+ async registerWithInvite(signupData: RegisterData): Promise {
+ const { email, firstName, lastName, password, roles } = signupData;
+
+ const saltRounds = 12;
+ const passwordHash = await bcrypt.hash(password, saltRounds);
+ const user = new User({
+ email,
+ firstName,
+ lastName,
+ passwordHash,
+ roles: roles || [],
+ });
+ try {
+ const savedUser = await user.save();
+ return {
+ sub: savedUser._id.toString(),
+ roles: savedUser.roles.map((role) => role.toString()),
+ };
+ } catch (error: any) {
+ if (error?.code === 11000) {
+ const dupError = new ApiError("Email already in use", 409);
+ dupError.stack = error?.stack;
+ throw dupError;
+ }
+ throw error;
+ }
+ }
+
+ async login(loginData: LoginData): Promise {
+ const { email, password } = loginData;
+
+ // Find user by email
+ const user = await User.findOne({ email });
+
+ if (!user) {
+ throw new Error("Invalid email or password");
+ }
+
+ // Check password
+ const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
+ if (!isPasswordValid) {
+ throw new Error("Invalid email or password");
+ }
+
+ return {
+ sub: user._id.toString(),
+ roles: user.roles.map((role) => role.toString()),
+ };
+ }
+
+ async cleanup() {
+ await User.deleteMany({});
+ await Role.deleteMany({});
+ await Monitor.deleteMany({});
+ await Check.deleteMany({});
+ await NotificationChannel.deleteMany({});
+ await this.jobQueue.flush();
+ }
+
+ async cleanMonitors() {
+ await Monitor.deleteMany({});
+ await Check.deleteMany({});
+ }
+}
+
+export default AuthService;
diff --git a/server/src/service/v2/business/CheckService.ts b/server/src/service/v2/business/CheckService.ts
new file mode 100644
index 000000000..54593ed2b
--- /dev/null
+++ b/server/src/service/v2/business/CheckService.ts
@@ -0,0 +1,147 @@
+import { json } from "stream/consumers";
+import { ICheck, Check, Monitor } from "../../../db/v2/models/index.js";
+import type { ISystemInfo, ICaptureInfo } from "../../../db/v2/models/index.js";
+import { MonitorType } from "../../../db/v2/models/monitors/Monitor.js";
+import { StatusResponse } from "../infrastructure/NetworkService.js";
+import type { ICapturePayload, ILighthousePayload } from "../infrastructure/NetworkService.js";
+import mongoose from "mongoose";
+import { stat } from "fs";
+
+const SERVICE_NAME = "CheckServiceV2";
+export interface ICheckService {
+ buildCheck: (statusResponse: StatusResponse, type: MonitorType) => Promise;
+ cleanupOrphanedChecks: () => Promise;
+}
+
+class CheckService implements ICheckService {
+ static SERVICE_NAME = SERVICE_NAME;
+
+ private isCapturePayload = (payload: any): payload is ICapturePayload => {
+ if (!payload || typeof payload !== "object") return false;
+
+ if (!("data" in payload) || typeof payload.data !== "object") {
+ return false;
+ }
+
+ const data = payload.data as Partial;
+ if (!data.cpu || typeof data.cpu !== "object" || typeof data.cpu.usage_percent !== "number") {
+ return false;
+ }
+
+ if (!data.memory || typeof data.memory !== "object" || typeof data.memory.usage_percent !== "number") {
+ return false;
+ }
+
+ if (data.disk && !Array.isArray(data.disk)) {
+ return false;
+ }
+ if (data.net && !Array.isArray(data.net)) {
+ return false;
+ }
+
+ if (!("capture" in payload) || typeof payload.capture !== "object") return false;
+ const capture = payload.capture as Record;
+ if (typeof capture.version !== "string" || typeof capture.mode !== "string") return false;
+
+ return true;
+ };
+
+ private isPagespeedPayload = (payload: any): payload is ILighthousePayload => {
+ if (!payload || typeof payload !== "object") return false;
+
+ if (!("lighthouseResult" in payload) || typeof payload.lighthouseResult !== "object") {
+ return false;
+ }
+ return true;
+ };
+
+ private buildBaseCheck = (statusResponse: StatusResponse) => {
+ const monitorId = new mongoose.Types.ObjectId(statusResponse.monitorId);
+ const check = new Check({
+ monitorId: monitorId,
+ type: statusResponse?.type,
+ status: statusResponse?.status,
+ httpStatusCode: statusResponse?.code,
+ message: statusResponse?.message,
+ responseTime: statusResponse?.responseTime,
+ timings: statusResponse?.timings,
+ });
+ return check;
+ };
+
+ private buildInfrastructureCheck = (statusResponse: StatusResponse) => {
+ if (!this.isCapturePayload(statusResponse.payload)) {
+ throw new Error("Invalid payload for infrastructure monitor");
+ }
+ const check = this.buildBaseCheck(statusResponse);
+ check.system = statusResponse.payload.data;
+ check.capture = statusResponse.payload.capture;
+ return check;
+ };
+
+ private buildPagespeedCheck = (statusResponse: StatusResponse) => {
+ if (!this.isPagespeedPayload(statusResponse.payload)) {
+ throw new Error("Invalid payload for pagespeed monitor");
+ }
+ const check = this.buildBaseCheck(statusResponse);
+ const lighthouseResult = statusResponse?.payload?.lighthouseResult;
+ check.lighthouse = {
+ accessibility: lighthouseResult?.categories?.accessibility?.score || 0,
+ bestPractices: lighthouseResult?.categories?.["best-practices"]?.score || 0,
+ seo: lighthouseResult?.categories?.seo?.score || 0,
+ performance: lighthouseResult?.categories?.performance?.score || 0,
+ audits: {
+ cls: lighthouseResult?.audits?.["cumulative-layout-shift"] || {},
+ si: lighthouseResult?.audits?.["speed-index"] || {},
+ fcp: lighthouseResult?.audits?.["first-contentful-paint"] || {},
+ lcp: lighthouseResult?.audits?.["largest-contentful-paint"] || {},
+ tbt: lighthouseResult?.audits?.["total-blocking-time"] || {},
+ },
+ };
+ return check;
+ };
+
+ buildCheck = async (statusResponse: StatusResponse, type: MonitorType): Promise => {
+ switch (type) {
+ case "infrastructure":
+ return this.buildInfrastructureCheck(statusResponse as StatusResponse);
+
+ case "pagespeed":
+ return this.buildPagespeedCheck(statusResponse as StatusResponse);
+ case "http":
+ case "https":
+ return this.buildBaseCheck(statusResponse);
+
+ case "ping":
+ return this.buildBaseCheck(statusResponse);
+ default:
+ throw new Error(`Unsupported monitor type: ${type}`);
+ }
+ };
+
+ cleanupOrphanedChecks = async () => {
+ try {
+ const monitorIds = await Monitor.find().distinct("_id");
+ const result = await Check.deleteMany({
+ monitorId: { $nin: monitorIds },
+ });
+ console.log(`Deleted ${result.deletedCount} orphaned Checks.`);
+ return true;
+ } catch (error) {
+ console.error("Error cleaning up orphaned Checks:", error);
+ return false;
+ }
+ };
+
+ getChecks = async (monitorId: string, page: number, rowsPerPage: number) => {
+ const count = await Check.countDocuments({ monitorId: new mongoose.Types.ObjectId(monitorId) });
+ const checks = await Check.find({ monitorId: new mongoose.Types.ObjectId(monitorId) })
+ .sort({ createdAt: -1 })
+ .skip(page * rowsPerPage)
+ .limit(rowsPerPage)
+ .exec();
+ return { checks, count };
+ };
+}
+
+export default CheckService;
diff --git a/server/src/service/v2/business/InviteService.ts b/server/src/service/v2/business/InviteService.ts
new file mode 100644
index 000000000..b06b2a2c2
--- /dev/null
+++ b/server/src/service/v2/business/InviteService.ts
@@ -0,0 +1,63 @@
+import crypto from "node:crypto";
+import { ITokenizedUser, IInvite, Invite } from "../../../db/v2/models/index.js";
+import ApiError from "../../../utils/ApiError.js";
+
+const SERVICE_NAME = "InviteServiceV2";
+export interface IInviteService {
+ create: (tokenizedUser: ITokenizedUser, invite: IInvite) => Promise<{ token: string }>;
+ getAll: () => Promise;
+ get: (tokenHash: string) => Promise;
+ delete: (id: string) => Promise;
+}
+
+class InviteService implements IInviteService {
+ static SERVICE_NAME = SERVICE_NAME;
+ constructor() {}
+
+ create = async (tokenizedUser: ITokenizedUser, inviteData: IInvite) => {
+ const token = crypto.randomBytes(32).toString("hex");
+ const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
+ try {
+ const invite = await Invite.create({
+ ...inviteData,
+ tokenHash,
+ createdBy: tokenizedUser.sub,
+ updatedBy: tokenizedUser.sub,
+ });
+ if (!invite) {
+ throw new ApiError("Failed to create invite", 500);
+ }
+ return { token };
+ } catch (error: any) {
+ if (error?.code === 11000) {
+ const dupError = new ApiError("Invite with this email already exists", 409);
+ dupError.stack = error?.stack;
+ throw dupError;
+ }
+ throw error;
+ }
+ };
+
+ get = async (token: string) => {
+ const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
+ const invite = await Invite.findOne({ tokenHash });
+ if (!invite) {
+ throw new ApiError("Invite not found", 404);
+ }
+ return invite;
+ };
+
+ getAll = async () => {
+ return Invite.find();
+ };
+
+ delete = async (id: string) => {
+ const result = await Invite.deleteOne({ _id: id });
+ if (!result.deletedCount) {
+ throw new ApiError("Invite not found", 404);
+ }
+ return result.deletedCount === 1;
+ };
+}
+
+export default InviteService;
diff --git a/server/src/service/v2/business/MaintenanceService.ts b/server/src/service/v2/business/MaintenanceService.ts
new file mode 100644
index 000000000..55b2d787b
--- /dev/null
+++ b/server/src/service/v2/business/MaintenanceService.ts
@@ -0,0 +1,144 @@
+import { ITokenizedUser, IMaintenance, Maintenance } from "../../../db/v2/models/index.js";
+import ApiError from "../../../utils/ApiError.js";
+
+const SERVICE_NAME = "MaintenanceServiceV2";
+
+export interface IMaintenanceService {
+ create: (
+ tokenizedUser: ITokenizedUser,
+
+ maintenance: IMaintenance
+ ) => Promise;
+ getAll: () => Promise;
+ get: (id: string) => Promise;
+ toggleActive: (tokenizedUser: ITokenizedUser, id: string) => Promise;
+ update: (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => Promise;
+ delete: (id: string) => Promise;
+ isInMaintenance: (monitorId: string) => Promise;
+}
+
+type MaintenanceCache = Map;
+
+class MaintenanceService implements IMaintenanceService {
+ static SERVICE_NAME = SERVICE_NAME;
+ private maintenanceCache: MaintenanceCache;
+ private lastRefresh: number;
+ private CACHE_TTL_MS = 60 * 1000;
+
+ constructor() {
+ this.maintenanceCache = new Map();
+ this.lastRefresh = 0;
+ }
+
+ create = async (tokenizedUser: ITokenizedUser, maintenanceData: IMaintenance) => {
+ const maintenance = await Maintenance.create({
+ ...maintenanceData,
+ createdBy: tokenizedUser.sub,
+ updatedBy: tokenizedUser.sub,
+ });
+ return maintenance;
+ };
+
+ get = async (id: string) => {
+ const maintenance = await Maintenance.findById(id);
+ if (!maintenance) {
+ throw new ApiError("Maintenance not found", 404);
+ }
+ return maintenance;
+ };
+
+ getAll = async () => {
+ return Maintenance.find();
+ };
+
+ toggleActive = async (tokenizedUser: ITokenizedUser, id: string) => {
+ const updatedMaintenance = await Maintenance.findOneAndUpdate(
+ { _id: id },
+ [
+ {
+ $set: {
+ isActive: { $not: "$isActive" },
+ updatedBy: tokenizedUser.sub,
+ updatedAt: new Date(),
+ },
+ },
+ ],
+ { new: true }
+ );
+ if (!updatedMaintenance) {
+ throw new ApiError("Maintenance not found", 404);
+ }
+ return updatedMaintenance;
+ };
+
+ update = async (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => {
+ const allowedFields: (keyof IMaintenance)[] = ["name", "monitors", "startTime", "endTime", "isActive"];
+ const safeUpdate: Partial = {};
+ for (const field of allowedFields) {
+ if (updateData[field] !== undefined) {
+ (safeUpdate as any)[field] = updateData[field];
+ }
+ }
+
+ const updatedMaintenance = await Maintenance.findByIdAndUpdate(
+ id,
+ {
+ $set: {
+ ...safeUpdate,
+ updatedAt: new Date(),
+ updatedBy: tokenizedUser.sub,
+ },
+ },
+ { new: true, runValidators: true }
+ );
+
+ if (!updatedMaintenance) {
+ throw new ApiError("Failed to update maintenance", 500);
+ }
+
+ return updatedMaintenance;
+ };
+
+ delete = async (id: string) => {
+ const result = await Maintenance.deleteOne({ _id: id });
+ if (!result.deletedCount) {
+ throw new ApiError("Maintenance not found", 404);
+ }
+ return result.deletedCount === 1;
+ };
+
+ private refreshCache = async () => {
+ const now = new Date();
+
+ const activeMaintenances = await Maintenance.find({
+ isActive: true,
+ startTime: { $lte: now },
+ endTime: { $gte: now },
+ }).lean();
+
+ // Reset cache
+ const newCache = new Map();
+
+ for (const m of activeMaintenances) {
+ for (const monitorId of m.monitors) {
+ const key = monitorId.toString();
+ if (!newCache.has(key)) newCache.set(key, []);
+ newCache.get(key)!.push(m);
+ }
+ }
+
+ this.maintenanceCache = newCache;
+ this.lastRefresh = Date.now();
+ };
+
+ isInMaintenance = async (monitorId: string) => {
+ const now = Date.now();
+ if (now - this.lastRefresh > this.CACHE_TTL_MS) {
+ await this.refreshCache();
+ }
+ const maintenances = this.maintenanceCache.get(monitorId) || [];
+ return maintenances.length > 0;
+ };
+}
+
+export default MaintenanceService;
diff --git a/server/src/service/v2/business/MonitorService.ts b/server/src/service/v2/business/MonitorService.ts
new file mode 100644
index 000000000..c0570236b
--- /dev/null
+++ b/server/src/service/v2/business/MonitorService.ts
@@ -0,0 +1,468 @@
+import mongoose from "mongoose";
+
+import { IMonitor, Monitor, ITokenizedUser, MonitorStats, Check } from "../../../db/v2/models/index.js";
+import ApiError from "../../../utils/ApiError.js";
+import { IJobQueue } from "../infrastructure/JobQueue.js";
+import { MonitorWithChecksResponse } from "../../../types/index.js";
+import { MonitorStatus, MonitorType } from "../../../db/v2/models/monitors/Monitor.js";
+
+const SERVICE_NAME = "MonitorServiceV2";
+
+export interface IMonitorService {
+ create: (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => Promise;
+ getAll: () => Promise;
+ getAllEmbedChecks: (page: number, limit: number, type: MonitorType[]) => Promise;
+ get: (monitorId: string) => Promise;
+ getEmbedChecks: (monitorId: string, range: string, status?: string) => Promise;
+ toggleActive: (monitorId: string, tokenizedUser: ITokenizedUser) => Promise;
+ update: (tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial) => Promise;
+ delete: (monitorId: string) => Promise;
+}
+
+class MonitorService implements IMonitorService {
+ static SERVICE_NAME = SERVICE_NAME;
+ private jobQueue: IJobQueue;
+ constructor(jobQueue: IJobQueue) {
+ this.jobQueue = jobQueue;
+ }
+
+ create = async (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => {
+ const monitor = await Monitor.create({
+ ...monitorData,
+ createdBy: tokenizedUser.sub,
+ updatedBy: tokenizedUser.sub,
+ });
+ await MonitorStats.create({
+ monitorId: monitor._id,
+ currentStreakStartedAt: Date.now(),
+ });
+ await this.jobQueue.addJob(monitor);
+ return monitor;
+ };
+
+ getAll = async () => {
+ return Monitor.find();
+ };
+
+ getAllEmbedChecks = async (page: number, limit: number, type: MonitorType[] = []) => {
+ const skip = (page - 1) * limit;
+ let find = {};
+ if (type.length > 0) find = { type: { $in: type } };
+ const monitors = await Monitor.find(find).skip(skip).limit(limit);
+ return monitors;
+ };
+
+ get = async (monitorId: string) => {
+ const monitor = await Monitor.findById(monitorId);
+ if (!monitor) {
+ throw new ApiError("Monitor not found", 404);
+ }
+ return monitor;
+ };
+
+ private getStartDate(range: string): Date {
+ const now = new Date();
+ switch (range) {
+ case "2h":
+ return new Date(now.getTime() - 2 * 60 * 60 * 1000);
+ case "24h":
+ return new Date(now.getTime() - 24 * 60 * 60 * 1000);
+ case "7d":
+ return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
+ case "30d":
+ return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+ default:
+ throw new ApiError("Invalid range parameter", 400);
+ }
+ }
+
+ private getDateFormat(range: string): string {
+ switch (range) {
+ case "2h":
+ return "%Y-%m-%dT%H:%M:00Z";
+ case "24h":
+ case "7d":
+ return "%Y-%m-%dT%H:00:00Z";
+ case "30d":
+ return "%Y-%m-%d";
+ default:
+ throw new ApiError("Invalid range parameter", 400);
+ }
+ }
+
+ private getBaseGroup = (dateFormat: string): Record => {
+ return {
+ _id: { $dateToString: { format: dateFormat, date: "$createdAt" } },
+ count: { $sum: 1 },
+ avgResponseTime: { $avg: "$responseTime" },
+ };
+ };
+
+ private getBaseProjection = (): object => {
+ return { status: 1, responseTime: 1, createdAt: 1 };
+ };
+
+ private getPageSpeedGroup = (dateFormat: string): Record => {
+ return {
+ _id: { $dateToString: { format: dateFormat, date: "$createdAt" } },
+ count: { $sum: 1 },
+ avgResponseTime: { $avg: "$responseTime" },
+ accessibility: { $avg: "$lighthouse.accessibility" },
+ bestPractices: { $avg: "$lighthouse.bestPractices" },
+ seo: { $avg: "$lighthouse.seo" },
+ performance: { $avg: "$lighthouse.performance" },
+ cls: { $avg: "$lighthouse.audits.cls.score" },
+ si: { $avg: "$lighthouse.audits.si.score" },
+ fcp: { $avg: "$lighthouse.audits.fcp.score" },
+ lcp: { $avg: "$lighthouse.audits.lcp.score" },
+ tbt: { $avg: "$lighthouse.audits.tbt.score" },
+ };
+ };
+
+ private getPageSpeedProjection = (): object => {
+ const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 };
+ projectStage["lighthouse.accessibility"] = 1;
+ projectStage["lighthouse.seo"] = 1;
+ projectStage["lighthouse.bestPractices"] = 1;
+ projectStage["lighthouse.performance"] = 1;
+ projectStage["lighthouse.audits.cls.score"] = 1;
+ projectStage["lighthouse.audits.si.score"] = 1;
+ projectStage["lighthouse.audits.fcp.score"] = 1;
+ projectStage["lighthouse.audits.lcp.score"] = 1;
+ projectStage["lighthouse.audits.tbt.score"] = 1;
+ return projectStage;
+ };
+
+ private getInfraGroup = (dateFormat: string): Record => {
+ return {
+ _id: { $dateToString: { format: dateFormat, date: "$createdAt" } },
+ count: { $sum: 1 },
+ avgResponseTime: { $avg: "$responseTime" },
+ physicalCores: { $last: "$system.cpu.physical_core" },
+ logicalCores: { $last: "$system.cpu.logical_core" },
+ frequency: { $avg: "$system.cpu.frequency" },
+ currentFrequency: { $last: "$system.cpu.current_frequency" },
+ tempsArrays: { $push: "$system.cpu.temperature" },
+ freePercent: { $avg: "$system.cpu.free_percent" },
+ usedPercent: { $avg: "$system.cpu.usage_percent" },
+ total_bytes: { $last: "$system.memory.total_bytes" },
+ available_bytes: { $last: "$system.memory.available_bytes" },
+ used_bytes: { $last: "$system.memory.used_bytes" },
+ memory_usage_percent: { $avg: "$system.memory.usage_percent" },
+ disksArray: { $push: "$system.disk" },
+ os: { $last: "$system.host.os" },
+ platform: { $last: "$system.host.platform" },
+ kernel_version: { $last: "$system.host.kernel_version" },
+ pretty_name: { $last: "$system.host.pretty_name" },
+ netsArray: { $push: "$system.net" },
+ };
+ };
+
+ private getInfraProjection = (): object => {
+ const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 };
+ projectStage["system.cpu.physical_core"] = 1;
+ projectStage["system.cpu.logical_core"] = 1;
+ projectStage["system.cpu.frequency"] = 1;
+ projectStage["system.cpu.current_frequency"] = 1;
+ projectStage["system.cpu.temperature"] = 1;
+ projectStage["system.cpu.free_percent"] = 1;
+ projectStage["system.cpu.usage_percent"] = 1;
+ projectStage["system.memory.total_bytes"] = 1;
+ projectStage["system.memory.available_bytes"] = 1;
+ projectStage["system.memory.used_bytes"] = 1;
+ projectStage["system.memory.usage_percent"] = 1;
+ projectStage["system.disk"] = 1;
+ projectStage["system.host.os"] = 1;
+ projectStage["system.host.platform"] = 1;
+ projectStage["system.host.kernel_version"] = 1;
+ projectStage["system.host.pretty_name"] = 1;
+ projectStage["system.net"] = 1;
+ return projectStage;
+ };
+
+ private getFinalProjection = (type: string): object => {
+ if (type === "pagespeed") {
+ return {
+ _id: 1,
+ count: 1,
+ avgResponseTime: 1,
+ accessibility: "$accessibility",
+ seo: "$seo",
+ bestPractices: "$bestPractices",
+ performance: "$performance",
+ cls: "$cls",
+ si: "$si",
+ fcp: "$fcp",
+ lcp: "$lcp",
+ tbt: "$tbt",
+ };
+ }
+
+ if (type === "infrastructure") {
+ return {
+ _id: 1,
+ count: 1,
+ avgResponseTime: 1,
+ cpu: {
+ physicalCores: "$physicalCores",
+ logicalCores: "$logicalCores",
+ frequency: "$frequency",
+ currentFrequency: "$currentFrequency",
+ temperatures: {
+ $map: {
+ input: {
+ $range: [0, { $size: { $arrayElemAt: ["$tempsArrays", 0] } }],
+ },
+ as: "idx",
+ in: {
+ $avg: {
+ $map: {
+ input: "$tempsArrays",
+ as: "arr",
+ in: { $arrayElemAt: ["$$arr", "$$idx"] },
+ },
+ },
+ },
+ },
+ },
+ freePercent: "$freePercent",
+ usedPercent: "$usedPercent",
+ },
+ memory: {
+ total_bytes: "$total_bytes",
+ available_bytes: "$available_bytes",
+ used_bytes: "$used_bytes",
+ usage_percent: "$memory_usage_percent",
+ },
+ disks: {
+ $map: {
+ input: {
+ $range: [0, { $size: { $arrayElemAt: ["$disksArray", 0] } }],
+ },
+ as: "idx",
+ in: {
+ $let: {
+ vars: {
+ diskGroup: {
+ $map: {
+ input: "$disksArray",
+ as: "diskArr",
+ in: { $arrayElemAt: ["$$diskArr", "$$idx"] },
+ },
+ },
+ },
+ in: {
+ device: { $arrayElemAt: ["$$diskGroup.device", 0] },
+ total_bytes: { $avg: "$$diskGroup.total_bytes" },
+ free_bytes: { $avg: "$$diskGroup.free_bytes" },
+ used_bytes: { $avg: "$$diskGroup.used_bytes" },
+ usage_percent: { $avg: "$$diskGroup.usage_percent" },
+ total_inodes: { $avg: "$$diskGroup.total_inodes" },
+ free_inodes: { $avg: "$$diskGroup.free_inodes" },
+ used_inodes: { $avg: "$$diskGroup.used_inodes" },
+ inodes_usage_percent: {
+ $avg: "$$diskGroup.inodes_usage_percent",
+ },
+ read_bytes: { $avg: "$$diskGroup.read_bytes" },
+ write_bytes: { $avg: "$$diskGroup.write_bytes" },
+ read_time: { $avg: "$$diskGroup.read_time" },
+ write_time: { $avg: "$$diskGroup.write_time" },
+ },
+ },
+ },
+ },
+ },
+ host: {
+ os: "$os",
+ platform: "$platform",
+ kernel_version: "$kernel_version",
+ pretty_name: "$pretty_name",
+ },
+ net: {
+ $map: {
+ input: {
+ $range: [0, { $size: { $arrayElemAt: ["$netsArray", 0] } }],
+ },
+ as: "idx",
+ in: {
+ $let: {
+ vars: {
+ netGroup: {
+ $map: {
+ input: "$netsArray",
+ as: "netArr",
+ in: { $arrayElemAt: ["$$netArr", "$$idx"] },
+ },
+ },
+ },
+ in: {
+ name: { $arrayElemAt: ["$$netGroup.name", 0] },
+ bytes_sent: { $avg: "$$netGroup.bytes_sent" },
+ bytes_recv: { $avg: "$$netGroup.bytes_recv" },
+ packets_sent: { $avg: "$$netGroup.packets_sent" },
+ packets_recv: { $avg: "$$netGroup.packets_recv" },
+ err_in: { $avg: "$$netGroup.err_in" },
+ err_out: { $avg: "$$netGroup.err_out" },
+ drop_in: { $avg: "$$netGroup.drop_in" },
+ drop_out: { $avg: "$$netGroup.drop_out" },
+ fifo_in: { $avg: "$$netGroup.fifo_in" },
+ fifo_out: { $avg: "$$netGroup.fifo_out" },
+ },
+ },
+ },
+ },
+ },
+ };
+ }
+ return {};
+ };
+
+ getEmbedChecks = async (monitorId: string, range: string, status: string | undefined): Promise => {
+ const monitor = await Monitor.findById(monitorId);
+ if (!monitor) {
+ throw new ApiError("Monitor not found", 404);
+ }
+ const startDate = this.getStartDate(range);
+ const dateFormat = this.getDateFormat(range);
+
+ // Build match stage
+ const matchStage: {
+ monitorId: mongoose.Types.ObjectId;
+ createdAt: { $gte: Date };
+ status?: string;
+ } = {
+ monitorId: monitor._id,
+ createdAt: { $gte: startDate },
+ };
+
+ if (status) {
+ matchStage.status = status;
+ }
+
+ let groupClause;
+
+ if (monitor.type === "pagespeed") {
+ groupClause = this.getPageSpeedGroup(dateFormat);
+ } else if (monitor.type === "infrastructure") {
+ groupClause = this.getInfraGroup(dateFormat);
+ } else {
+ groupClause = this.getBaseGroup(dateFormat);
+ }
+
+ let projectStage;
+ if (monitor.type === "pagespeed") {
+ projectStage = this.getPageSpeedProjection();
+ } else if (monitor.type === "infrastructure") {
+ projectStage = this.getInfraProjection();
+ } else {
+ projectStage = this.getBaseProjection();
+ }
+
+ let finalProjection = {};
+ if (monitor.type === "pagespeed" || monitor.type === "infrastructure") {
+ finalProjection = this.getFinalProjection(monitor.type);
+ } else {
+ finalProjection = { _id: 1, count: 1, avgResponseTime: 1 };
+ }
+
+ const checks = await Check.aggregate([
+ {
+ $match: matchStage,
+ },
+ { $sort: { createdAt: 1 } },
+ { $project: projectStage },
+ { $group: groupClause },
+ { $sort: { _id: -1 } },
+ {
+ $project: finalProjection,
+ },
+ ]);
+
+ // Get monitor stats
+ const monitorStats = await MonitorStats.findOne({
+ monitorId: monitor._id,
+ }).lean();
+
+ if (!monitorStats) {
+ throw new ApiError("Monitor stats not found", 404);
+ }
+
+ return {
+ monitor: monitor.toObject(),
+ checks,
+ stats: monitorStats,
+ };
+ };
+
+ async toggleActive(id: string, tokenizedUser: ITokenizedUser) {
+ const pendingStatus: MonitorStatus = "initializing";
+ const updatedMonitor = await Monitor.findOneAndUpdate(
+ { _id: id },
+ [
+ {
+ $set: {
+ isActive: { $not: "$isActive" },
+ status: pendingStatus,
+ updatedBy: tokenizedUser.sub,
+ updatedAt: new Date(),
+ },
+ },
+ ],
+ { new: true }
+ );
+
+ if (!updatedMonitor) {
+ throw new ApiError("Monitor not found", 404);
+ }
+
+ await this.jobQueue.updateJob(updatedMonitor);
+
+ if (updatedMonitor?.isActive) {
+ await this.jobQueue.resumeJob(updatedMonitor);
+ } else {
+ await this.jobQueue.pauseJob(updatedMonitor);
+ }
+ return updatedMonitor;
+ }
+
+ async update(tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial) {
+ const allowedFields: (keyof IMonitor)[] = ["name", "interval", "isActive", "n", "notificationChannels"];
+ const safeUpdate: Partial = {};
+
+ for (const field of allowedFields) {
+ if (updateData[field] !== undefined) {
+ (safeUpdate as any)[field] = updateData[field];
+ }
+ }
+
+ const updatedMonitor = await Monitor.findByIdAndUpdate(
+ monitorId,
+ {
+ $set: {
+ ...safeUpdate,
+ updatedAt: new Date(),
+ updatedBy: tokenizedUser.sub,
+ },
+ },
+ { new: true, runValidators: true }
+ );
+
+ if (!updatedMonitor) {
+ throw new ApiError("Monitor not found", 404);
+ }
+ await this.jobQueue.updateJob(updatedMonitor);
+ return updatedMonitor;
+ }
+
+ async delete(monitorId: string) {
+ const monitor = await Monitor.findById(monitorId);
+ if (!monitor) {
+ throw new ApiError("Monitor not found", 404);
+ }
+ await monitor.deleteOne();
+ await this.jobQueue.deleteJob(monitor);
+ return true;
+ }
+}
+
+export default MonitorService;
diff --git a/server/src/service/v2/business/MonitorStatsService.ts b/server/src/service/v2/business/MonitorStatsService.ts
new file mode 100644
index 000000000..089b43ec4
--- /dev/null
+++ b/server/src/service/v2/business/MonitorStatsService.ts
@@ -0,0 +1,28 @@
+import { MonitorStats } from "../../../db/v2/models/index.js";
+import { Monitor } from "../../../db/v2/models/index.js";
+
+const SERVICE_NAME = "MonitorStatsServiceV2";
+export interface IMonitorStatsService {
+ cleanupOrphanedMonitorStats: () => Promise;
+}
+
+class MonitorStatsService implements IMonitorStatsService {
+ static SERVICE_NAME = SERVICE_NAME;
+ constructor() {}
+
+ async cleanupOrphanedMonitorStats() {
+ try {
+ const monitorIds = await Monitor.find().distinct("_id");
+ const result = await MonitorStats.deleteMany({
+ monitorId: { $nin: monitorIds },
+ });
+ console.log(`Deleted ${result.deletedCount} orphaned MonitorStats.`);
+ return true;
+ } catch (error) {
+ console.error("Error cleaning up orphaned MonitorStats:", error);
+ return false;
+ }
+ }
+}
+
+export default MonitorStatsService;
diff --git a/server/src/service/v2/business/NotificationChannelService.ts b/server/src/service/v2/business/NotificationChannelService.ts
new file mode 100644
index 000000000..112efa691
--- /dev/null
+++ b/server/src/service/v2/business/NotificationChannelService.ts
@@ -0,0 +1,104 @@
+import { ITokenizedUser, INotificationChannel, NotificationChannel, Monitor } from "../../../db/v2/models/index.js";
+import ApiError from "../../../utils/ApiError.js";
+
+const SERVICE_NAME = "NotificationChannelServiceV2";
+
+export interface INotificationChannelService {
+ create: (
+ tokenizedUser: ITokenizedUser,
+
+ notificationChannel: INotificationChannel
+ ) => Promise;
+ getAll: () => Promise;
+ get: (id: string) => Promise;
+ toggleActive: (tokenizedUser: ITokenizedUser, id: string) => Promise;
+ update: (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => Promise;
+ delete: (id: string) => Promise;
+}
+
+class NotificationChannelService implements INotificationChannelService {
+ static SERVICE_NAME = SERVICE_NAME;
+
+ constructor() {}
+
+ create = async (tokenizedUser: ITokenizedUser, notificationChannelData: INotificationChannel) => {
+ const notificationChannel = await NotificationChannel.create({
+ ...notificationChannelData,
+ createdBy: tokenizedUser.sub,
+ updatedBy: tokenizedUser.sub,
+ });
+ return notificationChannel;
+ };
+
+ get = async (id: string) => {
+ const channel = await NotificationChannel.findById(id);
+ if (!channel) {
+ throw new ApiError("Notification channel not found", 404);
+ }
+ return channel;
+ };
+
+ getAll = async () => {
+ return NotificationChannel.find();
+ };
+
+ toggleActive = async (tokenizedUser: ITokenizedUser, id: string) => {
+ const updatedChannel = await NotificationChannel.findOneAndUpdate(
+ { _id: id },
+ [
+ {
+ $set: {
+ isActive: { $not: "$isActive" },
+ updatedBy: tokenizedUser.sub,
+ updatedAt: new Date(),
+ },
+ },
+ ],
+ { new: true }
+ );
+ if (!updatedChannel) {
+ throw new ApiError("Notification channel not found", 404);
+ }
+ return updatedChannel;
+ };
+
+ update = async (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => {
+ const allowedFields: (keyof INotificationChannel)[] = ["name", "config", "isActive"];
+ const safeUpdate: Partial = {};
+ for (const field of allowedFields) {
+ if (updateData[field] !== undefined) {
+ (safeUpdate as any)[field] = updateData[field];
+ }
+ }
+
+ const updatedChannel = await NotificationChannel.findByIdAndUpdate(
+ id,
+ {
+ $set: {
+ ...safeUpdate,
+ updatedAt: new Date(),
+ updatedBy: tokenizedUser.sub,
+ },
+ },
+ { new: true, runValidators: true }
+ );
+
+ if (!updatedChannel) {
+ throw new ApiError("Failed to update notification channel", 500);
+ }
+
+ return updatedChannel;
+ };
+
+ delete = async (id: string) => {
+ const result = await NotificationChannel.deleteOne({ _id: id });
+ if (!result.deletedCount) {
+ throw new ApiError("Notification channel not found", 404);
+ }
+
+ await Monitor.updateMany({ notificationChannels: id }, { $pull: { notificationChannels: id } });
+ return result.deletedCount === 1;
+ };
+}
+
+export default NotificationChannelService;
diff --git a/server/src/service/v2/business/QueueService.ts b/server/src/service/v2/business/QueueService.ts
new file mode 100644
index 000000000..620544ba7
--- /dev/null
+++ b/server/src/service/v2/business/QueueService.ts
@@ -0,0 +1,26 @@
+import { IJobQueue } from "../infrastructure/JobQueue.js";
+
+const SERVICE_NAME = "QueueServiceV2";
+
+class QueueService {
+ static SERVICE_NAME = SERVICE_NAME;
+ private jobQueue: IJobQueue;
+
+ constructor(jobQueue: IJobQueue) {
+ this.jobQueue = jobQueue;
+ }
+
+ async getMetrics() {
+ return await this.jobQueue.getMetrics();
+ }
+
+ async getJobs() {
+ return await this.jobQueue.getJobs();
+ }
+
+ async flush() {
+ return await this.jobQueue.flush();
+ }
+}
+
+export default QueueService;
diff --git a/server/src/service/v2/business/UserService.ts b/server/src/service/v2/business/UserService.ts
new file mode 100644
index 000000000..09c50dcbc
--- /dev/null
+++ b/server/src/service/v2/business/UserService.ts
@@ -0,0 +1,15 @@
+import { IUser, User } from "../../../db/v2/models/index.js";
+
+const SERVICE_NAME = "UserServiceV2";
+export interface IUserService {
+ getAllUsers(): Promise;
+}
+
+class UserService implements IUserService {
+ static SERVICE_NAME = SERVICE_NAME;
+ async getAllUsers(): Promise {
+ return await User.find();
+ }
+}
+
+export default UserService;
diff --git a/server/src/service/v2/infrastructure/JobGenerator.ts b/server/src/service/v2/infrastructure/JobGenerator.ts
new file mode 100644
index 000000000..69b716920
--- /dev/null
+++ b/server/src/service/v2/infrastructure/JobGenerator.ts
@@ -0,0 +1,82 @@
+import { IMonitor } from "../../../db/v2/models/index.js";
+import { INetworkService } from "./NetworkService.js";
+import { ICheckService } from "../business/CheckService.js";
+import { IMonitorStatsService } from "../business/MonitorStatsService.js";
+import { IStatusService } from "./StatusService.js";
+import { INotificationService } from "./NotificationService.js";
+import { IMaintenanceService } from "../business/MaintenanceService.js";
+import ApiError from "../../../utils/ApiError.js";
+
+const SERVICE_NAME = "JobGeneratorV2";
+export interface IJobGenerator {
+ generateJob: () => (Monitor: IMonitor) => Promise;
+ generateCleanupJob: () => () => Promise;
+}
+
+class JobGenerator implements IJobGenerator {
+ static SERVICE_NAME = SERVICE_NAME;
+ private networkService: INetworkService;
+ private checkService: ICheckService;
+ private monitorStatsService: IMonitorStatsService;
+ private statusService: IStatusService;
+ private notificationService: INotificationService;
+ private maintenanceService: IMaintenanceService;
+
+ constructor(
+ networkService: INetworkService,
+ checkService: ICheckService,
+ monitorStatsService: IMonitorStatsService,
+ statusService: IStatusService,
+ notificationService: INotificationService,
+ maintenanceService: IMaintenanceService
+ ) {
+ this.networkService = networkService;
+ this.checkService = checkService;
+ this.monitorStatsService = monitorStatsService;
+ this.statusService = statusService;
+ this.notificationService = notificationService;
+ this.maintenanceService = maintenanceService;
+ }
+
+ generateJob = () => {
+ return async (monitor: IMonitor) => {
+ try {
+ const monitorId = monitor._id.toString();
+ if (!monitorId) {
+ throw new ApiError("No monitorID for creating job", 400);
+ }
+
+ // Check for active maintenance window, if found, skip the check
+ const isInMaintenance = await this.maintenanceService.isInMaintenance(monitorId);
+ if (isInMaintenance) {
+ return;
+ }
+
+ const status = await this.networkService.requestStatus(monitor);
+ const check = await this.checkService.buildCheck(status, monitor.type);
+ await check.save();
+ const [updatedMonitor, statusChanged] = await this.statusService.updateMonitorStatus(monitor, status);
+
+ if (statusChanged) {
+ await this.notificationService.handleNotifications(updatedMonitor);
+ }
+ await this.statusService.updateMonitorStats(updatedMonitor, status, statusChanged);
+ } catch (error) {
+ throw error;
+ }
+ };
+ };
+
+ generateCleanupJob = () => {
+ return async () => {
+ try {
+ await this.checkService.cleanupOrphanedChecks();
+ await this.monitorStatsService.cleanupOrphanedMonitorStats();
+ } catch (error) {
+ throw error;
+ }
+ };
+ };
+}
+
+export default JobGenerator;
diff --git a/server/src/service/v2/infrastructure/JobQueue.ts b/server/src/service/v2/infrastructure/JobQueue.ts
new file mode 100644
index 000000000..85479d8e5
--- /dev/null
+++ b/server/src/service/v2/infrastructure/JobQueue.ts
@@ -0,0 +1,231 @@
+import { IJob } from "super-simple-scheduler/dist/job/job.js";
+import { Monitor, IMonitor } from "../../../db/v2/models/index.js";
+import Scheduler from "super-simple-scheduler";
+import { IJobGenerator } from "./JobGenerator.js";
+
+const SERVICE_NAME = "JobQueueV2";
+export interface IJobMetrics {
+ jobs: number;
+ activeJobs: number;
+ failingJobs: number;
+ jobsWithFailures: Array<{
+ monitorId: string | number;
+ monitorUrl: string | null;
+ monitorType: string | null;
+ failedAt: number | null;
+ failCount: number | null;
+ failReason: string | null;
+ }>;
+ totalRuns: number;
+ totalFailures: number;
+}
+
+export interface IJobData extends IJob {
+ lastRunTook: number | null;
+}
+
+export interface IJobQueue {
+ init: () => Promise;
+ addJob: (monitor: IMonitor) => Promise;
+ pauseJob: (monitor: IMonitor) => Promise;
+ resumeJob: (monitor: IMonitor) => Promise;
+ updateJob: (monitor: IMonitor) => Promise;
+ deleteJob: (monitor: IMonitor) => Promise;
+ getMetrics: () => Promise;
+ getJobs: () => Promise;
+ flush: () => Promise;
+ shutdown: () => Promise;
+}
+
+export default class JobQueue implements IJobQueue {
+ static SERVICE_NAME = SERVICE_NAME;
+
+ private scheduler: Scheduler;
+ private static instance: JobQueue | null = null;
+ private jobGenerator: any;
+ constructor() {
+ this.scheduler = new Scheduler({
+ logLevel: "debug",
+ });
+ }
+
+ static async create(jobGenerator: IJobGenerator) {
+ if (!JobQueue.instance) {
+ const instance = new JobQueue();
+ instance.jobGenerator = jobGenerator;
+ await instance.init();
+ JobQueue.instance = instance;
+ }
+ return JobQueue.instance;
+ }
+
+ static getInstance(): JobQueue | null {
+ return JobQueue.instance;
+ }
+
+ init = async () => {
+ try {
+ this.scheduler.start();
+ // Add template and jobs
+ this.scheduler.addTemplate("monitor-job", this.jobGenerator.generateJob());
+
+ // Add a cleanup job
+ this.scheduler.addTemplate("cleanup-job", this.jobGenerator.generateCleanupJob());
+ await this.scheduler.addJob({
+ id: "cleanup-orphaned-checks",
+ template: "cleanup-job",
+ repeat: 24 * 60 * 60 * 1000, // 24 hours
+ active: true,
+ });
+
+ const monitors = await Monitor.find();
+ for (const monitor of monitors) {
+ this.addJob(monitor);
+ }
+
+ return true;
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+ };
+
+ addJob = async (monitor: IMonitor) => {
+ try {
+ return await this.scheduler?.addJob({
+ id: monitor._id.toString(),
+ template: "monitor-job",
+ repeat: monitor.interval,
+ active: monitor.isActive,
+ data: monitor,
+ });
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+ };
+
+ pauseJob = async (monitor: IMonitor) => {
+ try {
+ return await this.scheduler?.pauseJob(monitor._id.toString());
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+ };
+
+ resumeJob = async (monitor: IMonitor) => {
+ try {
+ return await this.scheduler.resumeJob(monitor._id.toString());
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+ };
+
+ updateJob = async (monitor: IMonitor) => {
+ try {
+ return await this.scheduler.updateJob(monitor._id.toString(), {
+ repeat: monitor.interval,
+ data: monitor,
+ });
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+ };
+
+ deleteJob = async (monitor: IMonitor) => {
+ try {
+ this.scheduler?.removeJob(monitor._id.toString());
+ return true;
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+ };
+
+ getMetrics = async (): Promise => {
+ try {
+ const jobs = await this.scheduler.getJobs();
+ const metrics: IJobMetrics = jobs.reduce(
+ (acc, job) => {
+ if (!job.data) return acc;
+
+ acc.totalRuns += job.runCount || 0;
+ acc.totalFailures += job.failCount || 0;
+ acc.jobs++;
+
+ // Check if job is currently failing (has recent failures)
+ const hasFailures = job.failCount && job.failCount > 0;
+ const isCurrentlyFailing = hasFailures && job.lastFailedAt && (!job.lastRunAt || job.lastFailedAt > job.lastRunAt);
+
+ if (isCurrentlyFailing) {
+ acc.failingJobs++;
+ }
+
+ if (job.lockedAt) {
+ acc.activeJobs++;
+ }
+
+ if (hasFailures) {
+ acc.jobsWithFailures.push({
+ monitorId: job.id,
+ monitorUrl: job.data?.url || null,
+ monitorType: job.data?.type || null,
+ failedAt: job.lastFailedAt || null,
+ failCount: job.failCount || null,
+ failReason: job.lastFailReason || null,
+ });
+ }
+ return acc;
+ },
+ {
+ jobs: 0,
+ activeJobs: 0,
+ failingJobs: 0,
+ jobsWithFailures: [],
+ totalRuns: 0,
+ totalFailures: 0,
+ }
+ );
+ return metrics;
+ } catch (error) {
+ console.error(error);
+ return null;
+ }
+ };
+
+ getJobs = async (): Promise => {
+ try {
+ const jobs = await this.scheduler.getJobs();
+ return jobs.map((job) => {
+ return {
+ ...job,
+ lastRunTook: job.lockedAt || !job.lastFinishedAt || !job.lastRunAt ? null : job.lastFinishedAt - job.lastRunAt,
+ };
+ });
+ } catch (error) {
+ console.error(error);
+ return null;
+ }
+ };
+
+ flush = async () => {
+ try {
+ return await this.scheduler.flushJobs();
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+ };
+
+ shutdown = async () => {
+ try {
+ return await this.scheduler.stop();
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+ };
+}
diff --git a/server/src/service/v2/infrastructure/NetworkService.ts b/server/src/service/v2/infrastructure/NetworkService.ts
new file mode 100644
index 000000000..ab8427d84
--- /dev/null
+++ b/server/src/service/v2/infrastructure/NetworkService.ts
@@ -0,0 +1,197 @@
+import { Got, HTTPError } from "got";
+import got from "got";
+import ping from "ping";
+import { IMonitor } from "../../../db/v2/models/index.js";
+import { GotTimings } from "../../../db/v2/models/checks/Check.js";
+import type { Response } from "got";
+import type { ISystemInfo, ICaptureInfo, ILighthouseResult } from "../../../db/v2/models/index.js";
+import { MonitorType, MonitorStatus } from "../../../db/v2/models/monitors/Monitor.js";
+import ApiError from "../../../utils/ApiError.js";
+import { config } from "../../../config/index.js";
+
+const SERVICE_NAME = "NetworkServiceV2";
+export interface INetworkService {
+ requestHttp: (monitor: IMonitor) => Promise;
+ requestInfrastructure: (monitor: IMonitor) => Promise;
+ requestStatus: (monitor: IMonitor) => Promise;
+ requestPagespeed: (monitor: IMonitor) => Promise;
+ requestPing: (monitor: IMonitor) => Promise;
+}
+
+export interface ICapturePayload {
+ data: ISystemInfo;
+ capture: ICaptureInfo;
+}
+
+export interface ILighthousePayload {
+ lighthouseResult: ILighthouseResult;
+}
+
+export interface StatusResponse {
+ monitorId: string;
+ type: MonitorType;
+ code?: number;
+ status: MonitorStatus;
+ message: string;
+ responseTime: number;
+ timings?: GotTimings;
+ payload?: TPayload;
+}
+
+class NetworkService implements INetworkService {
+ static SERVICE_NAME = SERVICE_NAME;
+ private got: Got;
+ private NETWORK_ERROR: number;
+ constructor() {
+ this.got = got;
+ this.NETWORK_ERROR = 5000;
+ }
+
+ private buildStatusResponse = (monitor: IMonitor, response: Response | null, error: any | null): StatusResponse => {
+ if (error) {
+ const statusResponse: StatusResponse = {
+ monitorId: monitor._id.toString(),
+ type: monitor.type,
+ status: "down" as MonitorStatus,
+ code: this.NETWORK_ERROR,
+ message: error.message || "Network error",
+ responseTime: 0,
+ timings: { phases: {} } as GotTimings,
+ };
+ if (error instanceof HTTPError) {
+ statusResponse.code = error?.response?.statusCode || this.NETWORK_ERROR;
+ statusResponse.message = error.message || "HTTP error";
+ statusResponse.responseTime = error.timings?.phases?.total || 0;
+ statusResponse.timings = error.timings;
+ }
+ return statusResponse;
+ }
+
+ const statusResponse: StatusResponse = {
+ monitorId: monitor._id.toString(),
+ type: monitor.type,
+ code: response?.statusCode || this.NETWORK_ERROR,
+ status: response?.ok === true ? "up" : "down",
+ message: response?.statusMessage || "",
+ responseTime: response?.timings?.phases?.total || 0,
+ timings: response?.timings || ({ phases: {} } as GotTimings),
+ };
+
+ return statusResponse;
+ };
+
+ requestHttp = async (monitor: IMonitor) => {
+ try {
+ const url = monitor.url;
+ if (!url) {
+ throw new Error("No URL provided");
+ }
+
+ try {
+ const response: Response = await this.got(url);
+ return this.buildStatusResponse(monitor, response, null);
+ } catch (error) {
+ return this.buildStatusResponse(monitor, null, error);
+ }
+ } catch (error) {
+ throw error;
+ }
+ };
+
+ requestInfrastructure = async (monitor: IMonitor) => {
+ const url = monitor.url;
+ if (!url) {
+ throw new Error("No URL provided");
+ }
+ const secret = monitor.secret;
+ if (!secret) {
+ throw new Error("No secret provided for infrastructure monitor");
+ }
+
+ let statusResponse: StatusResponse;
+ try {
+ const response: Response | null = await this.got(url, {
+ headers: { Authorization: `Bearer ${secret}` },
+ responseType: "json",
+ });
+
+ statusResponse = this.buildStatusResponse(monitor, response, null);
+ if (!response?.body) {
+ throw new ApiError("No payload received from infrastructure monitor", 500);
+ }
+ statusResponse.payload = response?.body;
+ return statusResponse;
+ } catch (error) {
+ statusResponse = this.buildStatusResponse(monitor, null, error);
+ }
+ return statusResponse;
+ };
+
+ requestPagespeed = async (monitor: IMonitor) => {
+ const apiKey = config.PAGESPEED_API_KEY;
+ if (!apiKey) {
+ throw new Error("No API key provided for pagespeed monitor");
+ }
+ const url = monitor.url;
+ if (!url) {
+ throw new Error("No URL provided");
+ }
+
+ let statusResponse: StatusResponse;
+
+ try {
+ const response: Response = await this.got(url);
+ statusResponse = this.buildStatusResponse(monitor, response, null) as StatusResponse;
+ } catch (error) {
+ statusResponse = this.buildStatusResponse(monitor, null, error);
+ }
+
+ const pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance&key=${apiKey}`;
+ const pagespeedResponse = await this.got(pagespeedUrl, {
+ responseType: "json",
+ });
+ const payload = pagespeedResponse.body;
+ if (payload) {
+ statusResponse.payload = payload;
+ return statusResponse;
+ } else {
+ throw new ApiError("No payload received from pagespeed monitor", 500);
+ }
+ };
+
+ requestPing = async (monitor: IMonitor) => {
+ const response = await ping.promise.probe(monitor.url);
+ const status = response?.alive === true ? "up" : "down";
+
+ const rawTime = typeof response?.time === "string" ? parseFloat(response.time) : Number(response?.time);
+ const responseTime = Number.isFinite(rawTime) ? rawTime : 0;
+
+ return {
+ monitorId: monitor._id.toString(),
+ type: monitor.type,
+ status: status as MonitorStatus,
+ message: "Ping successful",
+ responseTime,
+ timings: { phases: {} } as GotTimings,
+ };
+ };
+
+ requestStatus = async (monitor: IMonitor) => {
+ switch (monitor?.type) {
+ case "http":
+ return await this.requestHttp(monitor); // uses GOT
+ case "https":
+ return await this.requestHttp(monitor); // uses GOT
+ case "infrastructure":
+ return await this.requestInfrastructure(monitor); // uses GOT
+ case "pagespeed":
+ return await this.requestPagespeed(monitor); // uses GOT
+ case "ping":
+ return await this.requestPing(monitor); // uses PING
+ default:
+ throw new Error("Not implemented");
+ }
+ };
+}
+
+export default NetworkService;
diff --git a/server/src/service/v2/infrastructure/NotificationService.ts b/server/src/service/v2/infrastructure/NotificationService.ts
new file mode 100644
index 000000000..2cc5e75d8
--- /dev/null
+++ b/server/src/service/v2/infrastructure/NotificationService.ts
@@ -0,0 +1,61 @@
+import UserService from "../business/UserService.js";
+import { IMonitor, NotificationChannel } from "../../../db/v2/models/index.js";
+import { EmailService, SlackService, DiscordService, WebhookService } from "./NotificationServices/index.js";
+
+const SERVICE_NAME = "NotificationServiceV2";
+export interface INotificationService {
+ handleNotifications: (monitor: IMonitor) => Promise;
+}
+
+class NotificationService implements INotificationService {
+ static SERVICE_NAME = SERVICE_NAME;
+ private emailService: EmailService;
+ private slackService: SlackService;
+ private discordService: DiscordService;
+ private webhookService: WebhookService;
+ private userService: UserService;
+
+ constructor(userService: UserService) {
+ this.userService = userService;
+ this.emailService = new EmailService(userService);
+ this.slackService = new SlackService();
+ this.discordService = new DiscordService();
+ this.webhookService = new WebhookService();
+ }
+
+ handleNotifications = async (monitor: IMonitor) => {
+ const notificationIds = monitor.notificationChannels || [];
+
+ if (notificationIds.length === 0) {
+ return;
+ }
+
+ const notificationChannels = await NotificationChannel.find({
+ _id: { $in: notificationIds },
+ });
+
+ for (const channel of notificationChannels) {
+ // Implement sending logic based on channel.type and channel.config
+ let service;
+ switch (channel.type) {
+ case "email":
+ await this.emailService.sendMessage(this.emailService.buildAlert(monitor), channel);
+ break;
+ case "slack":
+ await this.slackService.sendMessage(this.slackService.buildAlert(monitor), channel);
+ break;
+ case "discord":
+ await this.discordService.sendMessage(this.discordService.buildAlert(monitor), channel);
+ break;
+ case "webhook":
+ await this.webhookService.sendMessage(this.webhookService.buildAlert(monitor), channel);
+ break;
+ default:
+ console.warn(`Unknown notification channel type: ${channel.type}`);
+ }
+ }
+ return;
+ };
+}
+
+export default NotificationService;
diff --git a/server/src/service/v2/infrastructure/NotificationServices/Discord.ts b/server/src/service/v2/infrastructure/NotificationServices/Discord.ts
new file mode 100644
index 000000000..c3a5d375a
--- /dev/null
+++ b/server/src/service/v2/infrastructure/NotificationServices/Discord.ts
@@ -0,0 +1,76 @@
+import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js";
+import { IAlert, IMessageService } from "./IMessageService.js";
+import got from "got";
+import ApiError from "../../../../utils/ApiError.js";
+
+const SERVICE_NAME = "DiscordServiceV2";
+class DiscordService implements IMessageService {
+ static SERVICE_NAME = SERVICE_NAME;
+ constructor() {}
+
+ private toDiscordEmbeds = (alert: IAlert) => {
+ return {
+ color: alert.status === "up" ? 65280 : 16711680,
+ title: `Monitor name: ${alert.name}`,
+ description: `Status: **${alert.status}**`,
+ fields: [
+ {
+ name: "Url",
+ value: alert.url,
+ },
+ {
+ name: "Checked at",
+ value: alert.checkTime ? alert.checkTime.toISOString() : "N/A",
+ },
+ { name: "Alert time", value: alert.alertTime.toISOString() },
+ ...(alert.details
+ ? Object.entries(alert.details).map(([key, value]) => ({
+ name: key,
+ value,
+ }))
+ : []),
+ ],
+ };
+ };
+
+ buildAlert = (monitor: IMonitor) => {
+ const name = monitor?.name || "Unnamed monitor";
+ const monitorStatus = monitor?.status || "unknown status";
+ const url = monitor?.url || "no URL";
+ const checkTime = monitor?.lastCheckedAt || null;
+ const alertTime = new Date();
+ return {
+ name,
+ url,
+ status: monitorStatus,
+ checkTime,
+ alertTime,
+ };
+ };
+
+ sendMessage = async (alert: IAlert, channel: INotificationChannel) => {
+ const notificationUrl = channel?.config?.url;
+ if (!notificationUrl) {
+ throw new ApiError("Webhook URL not configured", 400);
+ }
+
+ try {
+ const payload = {
+ content: "Status Alert",
+ embeds: [this.toDiscordEmbeds(alert)],
+ };
+ await got.post(notificationUrl, { json: payload });
+ } catch (error) {
+ console.warn("Failed to send Discord message", error);
+ return false;
+ }
+
+ return true;
+ };
+
+ testMessage = async () => {
+ return true;
+ };
+}
+
+export default DiscordService;
diff --git a/server/src/service/v2/infrastructure/NotificationServices/Email.ts b/server/src/service/v2/infrastructure/NotificationServices/Email.ts
new file mode 100644
index 000000000..4bed9a156
--- /dev/null
+++ b/server/src/service/v2/infrastructure/NotificationServices/Email.ts
@@ -0,0 +1,68 @@
+import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js";
+import { IMessageService, IAlert } from "./IMessageService.js";
+import nodemailer, { Transporter } from "nodemailer";
+import { config } from "../../../../config/index.js";
+import UserService from "../../business/UserService.js";
+import ApiError from "../../../../utils/ApiError.js";
+
+const SERVICE_NAME = "EmailServiceV2";
+class EmailService implements IMessageService {
+ static SERVICE_NAME = SERVICE_NAME;
+ private transporter: Transporter;
+ private userService: UserService;
+
+ constructor(userService: UserService) {
+ this.userService = userService;
+ this.transporter = nodemailer.createTransport({
+ host: config.SMTP_HOST,
+ port: config.SMTP_PORT,
+ secure: config.SMTP_PORT === 465,
+ auth: {
+ user: config.SMTP_USER,
+ pass: config.SMTP_PASS,
+ },
+ });
+ }
+
+ buildAlert = (monitor: IMonitor) => {
+ const name = monitor?.name || "Unnamed monitor";
+ const monitorStatus = monitor?.status || "unknown status";
+ const url = monitor?.url || "no URL";
+ const checkTime = monitor?.lastCheckedAt || null;
+ const alertTime = new Date();
+ return {
+ name,
+ url,
+ status: monitorStatus,
+ checkTime,
+ alertTime,
+ };
+ };
+
+ sendMessage = async (alert: string | IAlert, channel: INotificationChannel) => {
+ try {
+ const users = await this.userService.getAllUsers();
+ const emails = users.map((u) => u.email).join(",");
+
+ if (!emails || emails.length === 0) {
+ throw new ApiError("No user emails found", 500);
+ }
+
+ await this.transporter.sendMail({
+ from: `"Checkmate" <${config.SMTP_USER}>`,
+ to: emails,
+ subject: "Monitor Alert",
+ text: JSON.stringify(alert, null, 2),
+ });
+ return true;
+ } catch (error) {
+ return false;
+ }
+ };
+
+ testMessage = async () => {
+ return true;
+ };
+}
+
+export default EmailService;
diff --git a/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts b/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts
new file mode 100644
index 000000000..f00a20549
--- /dev/null
+++ b/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts
@@ -0,0 +1,15 @@
+import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js";
+
+export interface IAlert {
+ name: string;
+ url: string;
+ status: string;
+ details?: Record;
+ checkTime: Date | null;
+ alertTime: Date;
+}
+export interface IMessageService {
+ buildAlert: (monitor: IMonitor) => IAlert;
+ sendMessage: (alert: IAlert, channel: INotificationChannel) => Promise;
+ testMessage: (message: string, channel: INotificationChannel) => Promise;
+}
diff --git a/server/src/service/v2/infrastructure/NotificationServices/Slack.ts b/server/src/service/v2/infrastructure/NotificationServices/Slack.ts
new file mode 100644
index 000000000..0be56ca56
--- /dev/null
+++ b/server/src/service/v2/infrastructure/NotificationServices/Slack.ts
@@ -0,0 +1,118 @@
+import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js";
+import { IAlert, IMessageService } from "./IMessageService.js";
+import got from "got";
+
+const SERVICE_NAME = "SlackServiceV2";
+class SlackService implements IMessageService {
+ static SERVICE_NAME = SERVICE_NAME;
+ constructor() {}
+
+ private toSlackBlocks = (alert: IAlert) => {
+ return [
+ {
+ type: "header",
+ text: {
+ type: "plain_text",
+ text: "Status Alert",
+ },
+ },
+ {
+ type: "section",
+ text: {
+ type: "mrkdwn",
+ text: `*Monitor name:* ${alert.name}`,
+ },
+ },
+
+ {
+ type: "section",
+ text: {
+ type: "mrkdwn",
+ text: `*Status:* ${alert.status}`,
+ },
+ },
+
+ {
+ type: "section",
+ text: {
+ type: "mrkdwn",
+ text: `*URL:* ${alert.url}`,
+ },
+ },
+ {
+ type: "section",
+ text: {
+ type: "mrkdwn",
+ text: `*Checked at:* ${alert?.checkTime?.toISOString() || "N/A"}`,
+ },
+ },
+
+ {
+ type: "divider",
+ },
+
+ ...(alert.details
+ ? Object.entries(alert.details).map(([key, value]) => ({
+ type: "section",
+ fields: [
+ {
+ type: "mrkdwn",
+ text: `*${key}:* ${value}`,
+ },
+ ],
+ }))
+ : []),
+
+ {
+ type: "context",
+ elements: [
+ {
+ type: "mrkdwn",
+ text: `*Alert generated at:* ${alert?.alertTime?.toISOString() || "N/A"}`,
+ },
+ ],
+ },
+ ];
+ };
+
+ buildAlert = (monitor: IMonitor) => {
+ const name = monitor?.name || "Unnamed monitor";
+ const monitorStatus = monitor?.status || "unknown status";
+ const url = monitor?.url || "no URL";
+ const checkTime = monitor?.lastCheckedAt || null;
+ const alertTime = new Date();
+ return {
+ name,
+ url,
+ status: monitorStatus,
+ checkTime,
+ alertTime,
+ };
+ };
+
+ sendMessage = async (alert: IAlert, channel: INotificationChannel) => {
+ const notificationUrl = channel?.config?.url;
+ if (!notificationUrl) {
+ throw new Error("Webhook URL not configured");
+ }
+
+ try {
+ const payload = {
+ text: "Status Alert",
+ blocks: this.toSlackBlocks(alert),
+ };
+ await got.post(notificationUrl, { json: payload });
+ } catch (error) {
+ console.warn("Error sending Slack message:", error);
+ return false;
+ }
+
+ return true;
+ };
+
+ testMessage = async () => {
+ return true;
+ };
+}
+
+export default SlackService;
diff --git a/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts b/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts
new file mode 100644
index 000000000..5dd89b16d
--- /dev/null
+++ b/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts
@@ -0,0 +1,47 @@
+import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js";
+import { IAlert, IMessageService } from "./IMessageService.js";
+import ApiError from "../../../../utils/ApiError.js";
+import got from "got";
+
+const SERVICE_NAME = "WebhookServiceV2";
+class WebhookService implements IMessageService {
+ static SERVICE_NAME = SERVICE_NAME;
+
+ constructor() {}
+
+ buildAlert = (monitor: IMonitor) => {
+ const name = monitor?.name || "Unnamed monitor";
+ const monitorStatus = monitor?.status || "unknown status";
+ const url = monitor?.url || "no URL";
+ const checkTime = monitor?.lastCheckedAt || null;
+ const alertTime = new Date();
+ return {
+ name,
+ url,
+ status: monitorStatus,
+ checkTime,
+ alertTime,
+ };
+ };
+
+ sendMessage = async (alert: IAlert, channel: INotificationChannel) => {
+ const notificationUrl = channel?.config?.url;
+ if (!notificationUrl) {
+ throw new ApiError("Webhook URL not configured", 400);
+ }
+ try {
+ await got.post(notificationUrl, { json: { ...alert } });
+ } catch (error) {
+ console.warn("Failed to send webhook notification:", error);
+ return false;
+ }
+
+ return true;
+ };
+
+ testMessage = async () => {
+ return true;
+ };
+}
+
+export default WebhookService;
diff --git a/server/src/service/v2/infrastructure/NotificationServices/index.ts b/server/src/service/v2/infrastructure/NotificationServices/index.ts
new file mode 100644
index 000000000..2b949f716
--- /dev/null
+++ b/server/src/service/v2/infrastructure/NotificationServices/index.ts
@@ -0,0 +1,5 @@
+export { default as DiscordService } from "./Discord.js";
+export { default as EmailService } from "./Email.js";
+export { default as SlackService } from "./Slack.js";
+export { default as WebhookService } from "./Webhook.js";
+export * from "./IMessageService.js";
diff --git a/server/src/service/v2/infrastructure/StatusService.ts b/server/src/service/v2/infrastructure/StatusService.ts
new file mode 100644
index 000000000..f2881b160
--- /dev/null
+++ b/server/src/service/v2/infrastructure/StatusService.ts
@@ -0,0 +1,102 @@
+import { IMonitor, IMonitorStats, MonitorStats } from "../../../db/v2/models/index.js";
+import { StatusResponse } from "./NetworkService.js";
+import ApiError from "../../../utils/ApiError.js";
+
+const SERVICE_NAME = "StatusServiceV2";
+const MAX_LATEST_CHECKS = 25;
+export interface IStatusService {
+ updateMonitorStatus: (monitor: IMonitor, status: StatusResponse) => Promise;
+
+ calculateAvgResponseTime: (stats: IMonitorStats, statusResponse: StatusResponse) => number;
+
+ updateMonitorStats: (monitor: IMonitor, status: StatusResponse, statusChanged: boolean) => Promise;
+}
+
+export type StatusChangeResult = [updatedMonitor: IMonitor, statusChanged: boolean];
+
+class StatusService implements IStatusService {
+ static SERVICE_NAME = SERVICE_NAME;
+ updateMonitorStatus = async (monitor: IMonitor, statusResponse: StatusResponse): Promise => {
+ const newStatus = statusResponse.status;
+ monitor.lastCheckedAt = new Date();
+
+ // Store latest checks for display
+ monitor.latestChecks = monitor.latestChecks || [];
+ monitor.latestChecks.push({
+ status: newStatus,
+ responseTime: statusResponse.responseTime,
+ checkedAt: monitor.lastCheckedAt,
+ });
+ while (monitor.latestChecks.length > MAX_LATEST_CHECKS) {
+ monitor.latestChecks.shift();
+ }
+
+ // Update monitor status
+ if (monitor.status === "initializing") {
+ monitor.status = newStatus;
+ return [await monitor.save(), true];
+ } else {
+ const { n } = monitor;
+ const latestChecks = monitor.latestChecks.slice(-n);
+ // Return early if not enough statuses to evaluate
+ if (latestChecks.length < n) {
+ return [await monitor.save(), false];
+ }
+
+ // If all different than current status, update status
+ const allDifferent = latestChecks.every((check) => check.status !== monitor.status);
+ if (allDifferent && monitor.status !== newStatus) {
+ monitor.status = newStatus;
+ }
+ return [await monitor.save(), allDifferent];
+ }
+ };
+
+ calculateAvgResponseTime = (stats: IMonitorStats, statusResponse: StatusResponse): number => {
+ let avgResponseTime = stats.avgResponseTime;
+ // Set initial
+ if (avgResponseTime === 0) {
+ avgResponseTime = statusResponse.responseTime;
+ } else {
+ avgResponseTime = (avgResponseTime * (stats.totalChecks - 1) + statusResponse.responseTime) / stats.totalChecks;
+ }
+ return avgResponseTime;
+ };
+
+ updateMonitorStats = async (monitor: IMonitor, statusResponse: StatusResponse, statusChanged: boolean) => {
+ const stats = await MonitorStats.findOne({ monitorId: monitor._id });
+ if (!stats) {
+ throw new ApiError("MonitorStats not found", 500);
+ }
+
+ // Update check counts
+ stats.totalChecks += 1;
+ stats.totalUpChecks += statusResponse.status === "up" ? 1 : 0;
+ stats.totalDownChecks += statusResponse.status === "down" ? 1 : 0;
+
+ // Update streak
+ if (!statusChanged) {
+ stats.currentStreak += 1;
+ } else {
+ stats.currentStreak = 1;
+ stats.currentStreakStatus = statusResponse.status;
+ stats.currentStreakStartedAt = Date.now();
+ }
+
+ // Update time stamps
+ stats.lastCheckTimestamp = Date.now();
+ stats.timeOfLastFailure = statusResponse.status === "down" ? Date.now() : stats.timeOfLastFailure;
+
+ // Update stats that need updated check counts
+ stats.avgResponseTime = this.calculateAvgResponseTime(stats, statusResponse);
+ stats.uptimePercentage = stats.totalUpChecks / stats.totalChecks;
+
+ // Other
+ stats.lastResponseTime = statusResponse.responseTime;
+ stats.maxResponseTime = Math.max(stats.maxResponseTime, statusResponse.responseTime);
+
+ return await stats.save();
+ };
+}
+
+export default StatusService;
diff --git a/server/src/types/express.d.ts b/server/src/types/express.d.ts
new file mode 100644
index 000000000..7c78a053c
--- /dev/null
+++ b/server/src/types/express.d.ts
@@ -0,0 +1,10 @@
+import { ITokenizedUser } from "../db/models/index.ts";
+
+declare global {
+ namespace Express {
+ interface Request {
+ user?: ITokenizedUser;
+ resource?: any;
+ }
+ }
+}
diff --git a/server/src/types/index.ts b/server/src/types/index.ts
new file mode 100644
index 000000000..6e708b934
--- /dev/null
+++ b/server/src/types/index.ts
@@ -0,0 +1 @@
+export type { MonitorWithChecksResponse } from "./monitor-response-with-checks.js";
diff --git a/server/src/types/monitor-response-with-checks.ts b/server/src/types/monitor-response-with-checks.ts
new file mode 100644
index 000000000..660ce0035
--- /dev/null
+++ b/server/src/types/monitor-response-with-checks.ts
@@ -0,0 +1,11 @@
+import { IMonitor, IMonitorStats } from "../db/v2/models/index.js";
+
+export interface MonitorWithChecksResponse {
+ monitor: IMonitor;
+ checks: Array<{
+ _id: string;
+ count: number;
+ avgResponseTime: number;
+ }>;
+ stats: IMonitorStats;
+}
diff --git a/server/src/utils/ApiError.ts b/server/src/utils/ApiError.ts
new file mode 100644
index 000000000..313d01c2a
--- /dev/null
+++ b/server/src/utils/ApiError.ts
@@ -0,0 +1,15 @@
+class ApiError extends Error {
+ public status: number;
+
+ constructor(message: string, status: number = 500) {
+ super(message);
+ this.status = status;
+ this.name = this.constructor.name;
+
+ if (Error.captureStackTrace) {
+ Error.captureStackTrace(this, this.constructor);
+ }
+ }
+}
+
+export default ApiError;
diff --git a/server/src/utils/JWTUtils.ts b/server/src/utils/JWTUtils.ts
new file mode 100644
index 000000000..4eebb78a0
--- /dev/null
+++ b/server/src/utils/JWTUtils.ts
@@ -0,0 +1,22 @@
+import jwt from "jsonwebtoken";
+import { AuthResult } from "../service/v2/business/AuthService.js";
+
+const encode = (data: AuthResult): string => {
+ const secret = process.env.JWT_SECRET;
+ if (!secret) {
+ throw new Error("JWT_SECRET is not defined");
+ }
+ const token = jwt.sign(data, secret, { expiresIn: "99d" });
+ return token;
+};
+
+const decode = (token: string): AuthResult => {
+ const secret = process.env.JWT_SECRET;
+ if (!secret) {
+ throw new Error("JWT_SECRET is not defined");
+ }
+ const decoded = jwt.verify(token, secret) as AuthResult;
+ return decoded;
+};
+
+export { encode, decode };
diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js
index fa8dccde2..1cd5cd5d6 100755
--- a/server/src/validation/joi.js
+++ b/server/src/validation/joi.js
@@ -670,6 +670,10 @@ const editSuperadminUserByIdBodyValidation = joi.object({
.required(),
});
+const editUserPasswordByIdBodyValidation = joi.object({
+ password: joi.string().min(8).required().pattern(passwordPattern),
+});
+
export {
roleValidatior,
loginValidation,
@@ -739,4 +743,5 @@ export {
editUserByIdParamValidation,
editUserByIdBodyValidation,
editSuperadminUserByIdBodyValidation,
+ editUserPasswordByIdBodyValidation,
};
diff --git a/server/tests/controllers/authController.test.js b/server/tests/controllers/authController.test.js
deleted file mode 100755
index 341c8eda6..000000000
--- a/server/tests/controllers/authController.test.js
+++ /dev/null
@@ -1,962 +0,0 @@
-import {
- issueToken,
- registerUser,
- loginUser,
- editUser,
- checkSuperadminExists,
- requestRecovery,
- validateRecovery,
- resetPassword,
- deleteUser,
- getAllUsers,
-} from "../../controllers/authController.js";
-import jwt from "jsonwebtoken";
-import { errorMessages, successMessages } from "../../utils/messages.js";
-import sinon from "sinon";
-import { tokenType } from "../../utils/utils.js";
-import logger from "../../utils/logger.js";
-
-describe("Auth Controller - issueToken", function () {
- let stub;
-
- afterEach(function () {
- sinon.restore(); // Restore stubs after each test
- });
-
- it("should reject with an error if jwt.sign fails", function () {
- const error = new Error("jwt.sign error");
- stub = sinon.stub(jwt, "sign").throws(error);
- const payload = { id: "123" };
- const appSettings = { jwtSecret: "my_secret" };
- expect(() => issueToken(payload, tokenType.ACCESS_TOKEN, appSettings)).to.throw(error);
- });
-
- it("should return a token if jwt.sign is successful and appSettings.jwtTTL is not defined", function () {
- const payload = { id: "123" };
- const appSettings = { jwtSecret: "my_secret" };
- const expectedToken = "mockToken";
-
- stub = sinon.stub(jwt, "sign").returns(expectedToken);
- const token = issueToken(payload, tokenType.ACCESS_TOKEN, appSettings);
- expect(token).to.equal(expectedToken);
- });
-
- it("should return a token if jwt.sign is successful and appSettings.jwtTTL is defined", function () {
- const payload = { id: "123" };
- const appSettings = { jwtSecret: "my_secret", jwtTTL: "1s" };
- const expectedToken = "mockToken";
-
- stub = sinon.stub(jwt, "sign").returns(expectedToken);
- const token = issueToken(payload, tokenType.ACCESS_TOKEN, appSettings);
- expect(token).to.equal(expectedToken);
- });
-
- it("should return a refresh token if jwt.sign is successful and appSettings.refreshTokenTTL is not defined", function () {
- const payload = {};
- const appSettings = { refreshTokenSecret: "my_refresh_secret" };
- const expectedToken = "mockRefreshToken";
-
- stub = sinon.stub(jwt, "sign").returns(expectedToken);
- const token = issueToken(payload, tokenType.REFRESH_TOKEN, appSettings);
- expect(token).to.equal(expectedToken);
- });
-
- it("should return a refresh token if jwt.sign is successful and appSettings.refreshTokenTTL is defined", function () {
- const payload = {};
- const appSettings = {
- refreshTokenSecret: "my_refresh_secret",
- refreshTokenTTL: "7d",
- };
- const expectedToken = "mockRefreshToken";
-
- stub = sinon.stub(jwt, "sign").returns(expectedToken);
- const token = issueToken(payload, tokenType.REFRESH_TOKEN, appSettings);
- expect(token).to.equal(expectedToken);
- });
-});
-
-describe("Auth Controller - registerUser", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- body: {
- firstName: "firstname",
- lastName: "lastname",
- email: "test@test.com",
- password: "Uptime1!",
- role: ["admin"],
- teamId: "123",
- inviteToken: "invite",
- },
- db: {
- checkSuperadmin: sinon.stub(),
- getInviteTokenAndDelete: sinon.stub(),
- updateAppSettings: sinon.stub(),
- insertUser: sinon.stub(),
- },
- settingsService: {
- getSettings: sinon.stub().resolves({
- jwtSecret: "my_secret",
- refreshTokenSecret: "my_refresh_secret",
- }),
- },
- emailService: {
- buildAndSendEmail: sinon.stub(),
- },
- file: {},
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- sinon.stub(logger, "error");
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if body validation fails", async function () {
- req.body = {};
- await registerUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if checkSuperadmin fails", async function () {
- req.db.checkSuperadmin.throws(new Error("checkSuperadmin error"));
- await registerUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("checkSuperadmin error");
- });
-
- it("should reject with an error if getInviteTokenAndDelete fails", async function () {
- req.db.checkSuperadmin.returns(true);
- req.db.getInviteTokenAndDelete.throws(new Error("getInviteTokenAndDelete error"));
- await registerUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("getInviteTokenAndDelete error");
- });
-
- it("should reject with an error if updateAppSettings fails", async function () {
- req.db.checkSuperadmin.returns(false);
- req.db.updateAppSettings.throws(new Error("updateAppSettings error"));
- await registerUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("updateAppSettings error");
- });
-
- it("should reject with an error if insertUser fails", async function () {
- req.db.checkSuperadmin.resolves(false);
- req.db.updateAppSettings.resolves();
- req.db.insertUser.rejects(new Error("insertUser error"));
- await registerUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("insertUser error");
- });
-
- it("should reject with an error if settingsService.getSettings fails", async function () {
- req.db.checkSuperadmin.resolves(false);
- req.db.updateAppSettings.resolves();
- req.db.insertUser.resolves({ _id: "123" });
- req.settingsService.getSettings.rejects(new Error("settingsService.getSettings error"));
- await registerUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("settingsService.getSettings error");
- });
-
- it("should log an error if emailService.buildAndSendEmail fails", async function () {
- req.db.checkSuperadmin.resolves(false);
- req.db.updateAppSettings.resolves();
- req.db.insertUser.returns({ _id: "123" });
- req.settingsService.getSettings.returns({
- jwtSecret: "my_secret",
- refreshTokenSecret: "my_secret",
- });
- req.emailService.buildAndSendEmail.rejects(new Error("emailService error"));
- await registerUser(req, res, next);
- expect(logger.error.calledOnce).to.be.true;
- expect(logger.error.firstCall.args[0].message).to.equal("emailService error");
- });
-
- it("should return a success message and data if all operations are successful", async function () {
- const user = { _id: "123" };
- req.db.checkSuperadmin.resolves(false);
- req.db.updateAppSettings.resolves();
- req.db.insertUser.returns(user);
- req.settingsService.getSettings.returns({
- jwtSecret: "my_secret",
- refreshTokenSecret: "my_secret",
- });
- req.emailService.buildAndSendEmail.resolves("message-id");
- await registerUser(req, res, next);
- expect(res.status.calledWith(200)).to.be.true;
- expect(
- res.json.calledWith({
- success: true,
- msg: successMessages.AUTH_CREATE_USER,
- data: { user, token: sinon.match.string, refreshToken: sinon.match.string },
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-
- it("should return a success message and data if all operations are successful and superAdmin true", async function () {
- const user = { _id: "123" };
- req.db.checkSuperadmin.resolves(true);
- req.db.updateAppSettings.resolves();
- req.db.insertUser.returns(user);
- req.settingsService.getSettings.returns({
- jwtSecret: "my_secret",
- refreshTokenSecret: "my_secret",
- });
- req.emailService.buildAndSendEmail.resolves("message-id");
- await registerUser(req, res, next);
- expect(res.status.calledWith(200)).to.be.true;
- expect(
- res.json.calledWith({
- success: true,
- msg: successMessages.AUTH_CREATE_USER,
- data: { user, token: sinon.match.string, refreshToken: sinon.match.string },
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-});
-
-describe("Auth Controller - loginUser", function () {
- let req, res, next, user;
-
- beforeEach(function () {
- req = {
- body: { email: "test@example.com", password: "Password123!" },
- db: {
- getUserByEmail: sinon.stub(),
- },
- settingsService: {
- getSettings: sinon.stub().resolves({
- jwtSecret: "my_secret",
- refreshTokenSecret: "my_refresh_token",
- }),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- user = {
- _doc: {
- email: "test@example.com",
- },
- comparePassword: sinon.stub(),
- };
- });
-
- it("should reject with an error if validation fails", async function () {
- req.body = {};
- await loginUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if getUserByEmail fails", async function () {
- req.db.getUserByEmail.rejects(new Error("getUserByEmail error"));
- await loginUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("getUserByEmail error");
- });
-
- it("should login user successfully", async function () {
- req.db.getUserByEmail.resolves(user);
- user.comparePassword.resolves(true);
- await loginUser(req, res, next);
- expect(res.status.calledWith(200)).to.be.true;
- expect(
- res.json.calledWith({
- success: true,
- msg: successMessages.AUTH_LOGIN_USER,
- data: {
- user: {
- email: "test@example.com",
- avatarImage: undefined,
- },
- token: sinon.match.string,
- refreshToken: sinon.match.string,
- },
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-
- it("should reject a user with an incorrect password", async function () {
- req.body = {
- email: "test@test.com",
- password: "Password123!",
- };
- req.db.getUserByEmail.resolves(user);
- user.comparePassword.resolves(false);
- await loginUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal(errorMessages.AUTH_INCORRECT_PASSWORD);
- });
-});
-
-describe("Auth Controller - refreshAuthToken", function () {
- let req, res, next, issueTokenStub;
-
- beforeEach(function () {
- req = {
- headers: {
- "x-refresh-token": "valid_refresh_token",
- authorization: "Bearer old_auth_token",
- },
- settingsService: {
- getSettings: sinon.stub().resolves({
- jwtSecret: "my_secret",
- refreshTokenSecret: "my_refresh_secret",
- }),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- sinon.stub(jwt, "verify");
-
- issueTokenStub = sinon.stub().returns("new_auth_token");
- sinon.replace({ issueToken }, "issueToken", issueTokenStub);
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject if no refresh token is provided", async function () {
- delete req.headers["x-refresh-token"];
- await refreshAuthToken(req, res, next);
-
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal(errorMessages.NO_REFRESH_TOKEN);
- expect(next.firstCall.args[0].status).to.equal(401);
- });
-
- it("should reject if the refresh token is invalid", async function () {
- jwt.verify.yields(new Error("invalid token"));
- await refreshAuthToken(req, res, next);
-
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal(errorMessages.INVALID_REFRESH_TOKEN);
- expect(next.firstCall.args[0].status).to.equal(401);
- });
-
- it("should reject if the refresh token is expired", async function () {
- const error = new Error("Token expired");
- error.name = "TokenExpiredError";
- jwt.verify.yields(error);
- await refreshAuthToken(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal(errorMessages.EXPIRED_REFRESH_TOKEN);
- expect(next.firstCall.args[0].status).to.equal(401);
- });
-
- it("should reject if settingsService.getSettings fails", async function () {
- req.settingsService.getSettings.rejects(new Error("settingsService.getSettings error"));
- await refreshAuthToken(req, res, next);
-
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("settingsService.getSettings error");
- });
-
- it("should generate a new auth token if the refresh token is valid", async function () {
- const decodedPayload = { expiresIn: "60" };
- jwt.verify.callsFake(() => {
- return decodedPayload;
- });
- await refreshAuthToken(req, res, next);
-
- expect(res.status.calledWith(200)).to.be.true;
- expect(
- res.json.calledWith({
- success: true,
- msg: successMessages.AUTH_TOKEN_REFRESHED,
- data: {
- user: decodedPayload,
- token: sinon.match.string,
- refreshToken: "valid_refresh_token",
- },
- })
- ).to.be.true;
- });
-});
-
-describe("Auth Controller - editUser", function () {
- let req, res, next, stub, user;
-
- beforeEach(function () {
- req = {
- params: { userId: "123" },
- body: { password: "Password1!", newPassword: "Password2!" },
- headers: { authorization: "Bearer token" },
- user: { _id: "123" },
- settingsService: {
- getSettings: sinon.stub().returns({ jwtSecret: "my_secret" }),
- },
- db: {
- getUserByEmail: sinon.stub(),
- updateUser: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- stub = sinon.stub(jwt, "verify").returns({ email: "test@example.com" });
- });
-
- afterEach(function () {
- sinon.restore();
- stub.restore();
- });
-
- it("should reject with an error if param validation fails", async function () {
- req.params = {};
- await editUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if body validation fails", async function () {
- req.body = { invalid: 1 };
- await editUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if param.userId !== req.user._id", async function () {
- req.params = { userId: "456" };
- await editUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(401);
- });
-
- it("should reject with an error if !req.body.password and getUserByEmail fails", async function () {
- req.db.getUserByEmail.rejects(new Error("getUserByEmail error"));
- await editUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("getUserByEmail error");
- });
-
- it("should reject with an error if user.comparePassword fails", async function () {
- req.db.getUserByEmail.returns({
- comparePassword: sinon.stub().rejects(new Error("Bad Password Match")),
- });
- await editUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("Bad Password Match");
- });
-
- it("should reject with an error if user.comparePassword returns false", async function () {
- req.db.getUserByEmail.returns({
- comparePassword: sinon.stub().returns(false),
- });
- await editUser(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(401);
- expect(next.firstCall.args[0].message).to.equal(errorMessages.AUTH_INCORRECT_PASSWORD);
- });
-
- it("should edit a user if it receives a proper request", async function () {
- const user = {
- comparePassword: sinon.stub().resolves(true),
- };
- req.db.getUserByEmail.resolves(user);
- req.db.updateUser.resolves({ email: "test@example.com" });
-
- await editUser(req, res, next);
-
- expect(req.db.getUserByEmail.calledOnce).to.be.true;
- expect(req.db.updateUser.calledOnce).to.be.true;
- expect(res.status.calledWith(200)).to.be.true;
- expect(
- res.json.calledWith({
- success: true,
- msg: successMessages.AUTH_UPDATE_USER,
- data: { email: "test@example.com" },
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-
- it("should edit a user if it receives a proper request and both password fields are undefined", async function () {
- req.body.password = undefined;
- req.body.newPassword = undefined;
- req.db.getUserByEmail.resolves(user);
- req.db.updateUser.resolves({ email: "test@example.com" });
-
- await editUser(req, res, next);
- expect(res.status.calledWith(200)).to.be.true;
- expect(
- res.json.calledWith({
- success: true,
- msg: successMessages.AUTH_UPDATE_USER,
- data: { email: "test@example.com" },
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-
- it("should reject an edit request if password format is incorrect", async function () {
- req.body = { password: "bad_password", newPassword: "bad_password" };
- const user = {
- comparePassword: sinon.stub().resolves(true),
- };
- req.db.getUserByEmail.resolves(user);
-
- await editUser(req, res, next);
-
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-});
-
-describe("Auth Controller - checkSuperadminExists", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- db: {
- checkSuperadmin: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- it("should reject with an error if checkSuperadmin fails", async function () {
- req.db.checkSuperadmin.rejects(new Error("checkSuperadmin error"));
- await checkSuperadminExists(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("checkSuperadmin error");
- });
-
- it("should return true if a superadmin exists", async function () {
- req.db.checkSuperadmin.resolves(true);
- await checkSuperadminExists(req, res, next);
- expect(res.status.calledWith(200)).to.be.true;
- expect(
- res.json.calledWith({
- success: true,
- msg: successMessages.AUTH_SUPERADMIN_EXISTS,
- data: true,
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-
- it("should return false if a superadmin does not exist", async function () {
- req.db.checkSuperadmin.resolves(false);
- await checkSuperadminExists(req, res, next);
- expect(res.status.calledWith(200)).to.be.true;
- expect(
- res.json.calledWith({
- success: true,
- msg: successMessages.AUTH_SUPERADMIN_EXISTS,
- data: false,
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-});
-
-describe("Auth Controller - requestRecovery", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- body: { email: "test@test.com" },
- db: {
- getUserByEmail: sinon.stub(),
- requestRecoveryToken: sinon.stub(),
- },
- settingsService: {
- getSettings: sinon.stub().returns({ clientHost: "http://localhost" }),
- },
- emailService: {
- buildAndSendEmail: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- it("should reject with an error if validation fails", async function () {
- req.body = {};
- await requestRecovery(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if getUserByEmail fails", async function () {
- req.db.getUserByEmail.rejects(new Error("getUserByEmail error"));
- await requestRecovery(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("getUserByEmail error");
- });
-
- it("should throw an error if the user is not found", async function () {
- req.db.getUserByEmail.resolves(null);
- await requestRecovery(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- // expect(next.firstCall.args[0].message).to.equal(
- // errorMessages.FRIENDLY_ERROR
- // );
- });
-
- it("should throw an error if the email is not provided", async function () {
- req.body = {};
- await requestRecovery(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should return a success message if the email is provided", async function () {
- const user = { firstName: "John" };
- const recoveryToken = { token: "recovery-token" };
- const msgId = "message-id";
- req.db.getUserByEmail.resolves(user);
- req.db.requestRecoveryToken.resolves(recoveryToken);
- req.emailService.buildAndSendEmail.resolves(msgId);
- await requestRecovery(req, res, next);
- expect(req.db.getUserByEmail.calledOnceWith("test@test.com")).to.be.true;
- expect(req.db.requestRecoveryToken.calledOnceWith(req, res)).to.be.true;
- expect(
- req.emailService.buildAndSendEmail.calledOnceWith(
- "passwordResetTemplate",
- {
- name: "John",
- email: "test@test.com",
- url: "http://localhost/set-new-password/recovery-token",
- },
- "test@test.com",
- "Checkmate Password Reset"
- )
- ).to.be.true;
- expect(res.status.calledOnceWith(200)).to.be.true;
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN,
- data: msgId,
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-});
-
-describe("Auth Controller - validateRecovery", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- body: { recoveryToken: "recovery-token" },
- db: {
- validateRecoveryToken: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- it("should reject with an error if validation fails", async function () {
- req.body = {};
- await validateRecovery(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if validateRecoveryToken fails", async function () {
- req.db.validateRecoveryToken.rejects(new Error("validateRecoveryToken error"));
- await validateRecovery(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("validateRecoveryToken error");
- });
-
- it("should return a success message if the token is valid", async function () {
- req.db.validateRecoveryToken.resolves();
- await validateRecovery(req, res, next);
- expect(res.status.calledOnceWith(200)).to.be.true;
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.AUTH_VERIFY_RECOVERY_TOKEN,
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-});
-
-describe("Auth Controller - resetPassword", function () {
- let req, res, next, newPasswordValidation, handleValidationError, handleError;
-
- beforeEach(function () {
- req = {
- body: {
- recoveryToken: "recovery-token",
- password: "Password1!",
- },
- db: {
- resetPassword: sinon.stub(),
- },
- settingsService: {
- getSettings: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- newPasswordValidation = {
- validateAsync: sinon.stub(),
- };
- handleValidationError = sinon.stub();
- handleError = sinon.stub();
- });
-
- it("should reject with an error if validation fails", async function () {
- req.body = { password: "bad_password" };
- await resetPassword(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if resetPassword fails", async function () {
- const error = new Error("resetPassword error");
- newPasswordValidation.validateAsync.resolves();
- req.db.resetPassword.rejects(error);
- await resetPassword(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("resetPassword error");
- });
-
- it("should reset password successfully", async function () {
- const user = { _doc: {} };
- const appSettings = { jwtSecret: "my_secret" };
- const token = "token";
-
- newPasswordValidation.validateAsync.resolves();
- req.db.resetPassword.resolves(user);
- req.settingsService.getSettings.resolves(appSettings);
-
- await resetPassword(req, res, next);
-
- expect(req.db.resetPassword.calledOnceWith(req, res)).to.be.true;
- expect(req.settingsService.getSettings.calledOnce).to.be.true;
- expect(res.status.calledOnceWith(200)).to.be.true;
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.AUTH_RESET_PASSWORD,
- data: { user: sinon.match.object, token: sinon.match.string },
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-});
-
-describe("Auth Controller - deleteUser", function () {
- let req, res, next, handleError;
-
- beforeEach(function () {
- req = {
- headers: {
- authorization: "Bearer token",
- },
- db: {
- getUserByEmail: sinon.stub(),
- getMonitorsByTeamId: sinon.stub(),
- deleteJob: sinon.stub(),
- deleteChecks: sinon.stub(),
- deletePageSpeedChecksByMonitorId: sinon.stub(),
- deleteNotificationsByMonitorId: sinon.stub(),
- deleteTeam: sinon.stub(),
- deleteAllOtherUsers: sinon.stub(),
- deleteMonitorsByUserId: sinon.stub(),
- deleteUser: sinon.stub(),
- },
- jobQueue: {
- deleteJob: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
-
- sinon.stub(jwt, "decode");
-
- handleError = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should throw an error if user is not found", async function () {
- jwt.decode.returns({ email: "test@example.com" });
- req.db.getUserByEmail.throws(new Error(errorMessages.DB_USER_NOT_FOUND));
-
- await deleteUser(req, res, next);
-
- expect(req.db.getUserByEmail.calledOnceWith("test@example.com")).to.be.true;
- expect(next.calledOnce).to.be.true;
- expect(next.firstCall.args[0].message).to.equal(errorMessages.DB_USER_NOT_FOUND);
- expect(res.status.notCalled).to.be.true;
- expect(res.json.notCalled).to.be.true;
- });
-
- it("should delete user and associated data if user is superadmin", async function () {
- const user = {
- _id: "user_id",
- email: "test@example.com",
- role: ["superadmin"],
- teamId: "team_id",
- };
- const monitors = [{ _id: "monitor_id" }];
-
- jwt.decode.returns({ email: "test@example.com" });
- req.db.getUserByEmail.resolves(user);
- req.db.getMonitorsByTeamId.resolves({ monitors });
-
- await deleteUser(req, res, next);
-
- expect(req.db.getUserByEmail.calledOnceWith("test@example.com")).to.be.true;
- expect(
- req.db.getMonitorsByTeamId.calledOnceWith({
- params: { teamId: "team_id" },
- })
- ).to.be.true;
- expect(req.jobQueue.deleteJob.calledOnceWith(monitors[0])).to.be.true;
- expect(req.db.deleteChecks.calledOnceWith("monitor_id")).to.be.true;
- expect(req.db.deletePageSpeedChecksByMonitorId.calledOnceWith("monitor_id")).to.be.true;
- expect(req.db.deleteNotificationsByMonitorId.calledOnceWith("monitor_id")).to.be.true;
- expect(req.db.deleteTeam.calledOnceWith("team_id")).to.be.true;
- expect(req.db.deleteAllOtherUsers.calledOnce).to.be.true;
- expect(req.db.deleteMonitorsByUserId.calledOnceWith("user_id")).to.be.true;
- expect(req.db.deleteUser.calledOnceWith("user_id")).to.be.true;
- expect(res.status.calledOnceWith(200)).to.be.true;
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.AUTH_DELETE_USER,
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-
- it("should delete user if user is not superadmin", async function () {
- const user = {
- _id: "user_id",
- email: "test@example.com",
- role: ["user"],
- teamId: "team_id",
- };
-
- jwt.decode.returns({ email: "test@example.com" });
- req.db.getUserByEmail.resolves(user);
-
- await deleteUser(req, res, next);
-
- expect(req.db.getUserByEmail.calledOnceWith("test@example.com")).to.be.true;
- expect(
- req.db.getMonitorsByTeamId.calledOnceWith({
- params: { teamId: "team_id" },
- })
- ).to.be.true;
- expect(req.db.deleteUser.calledOnceWith("user_id")).to.be.true;
- expect(res.status.calledOnceWith(200)).to.be.true;
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.AUTH_DELETE_USER,
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-
- it("should handle errors", async function () {
- const error = new Error("Something went wrong");
- const SERVICE_NAME = "AuthController";
- jwt.decode.returns({ email: "test@example.com" });
- req.db.getUserByEmail.rejects(error);
- await deleteUser(req, res, next);
- expect(next.calledOnce).to.be.true;
- expect(next.firstCall.args[0].message).to.equal("Something went wrong");
- expect(res.status.notCalled).to.be.true;
- expect(res.json.notCalled).to.be.true;
- });
-});
-
-describe("Auth Controller - getAllUsers", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- db: {
- getAllUsers: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore(); // Restore the original methods after each test
- });
-
- it("should return 200 and all users", async function () {
- const allUsers = [{ id: 1, name: "John Doe" }];
- req.db.getAllUsers.resolves(allUsers);
-
- await getAllUsers(req, res, next);
-
- expect(req.db.getAllUsers.calledOnce).to.be.true;
- expect(res.status.calledOnceWith(200)).to.be.true;
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: "Got all users",
- data: allUsers,
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-
- it("should call next with error when an exception occurs", async function () {
- const error = new Error("Something went wrong");
- req.db.getAllUsers.rejects(error);
- await getAllUsers(req, res, next);
- expect(req.db.getAllUsers.calledOnce).to.be.true;
- expect(next.calledOnce).to.be.true;
- expect(res.status.notCalled).to.be.true;
- expect(res.json.notCalled).to.be.true;
- });
-});
diff --git a/server/tests/controllers/checkController.test.js b/server/tests/controllers/checkController.test.js
deleted file mode 100755
index c5fb57d50..000000000
--- a/server/tests/controllers/checkController.test.js
+++ /dev/null
@@ -1,372 +0,0 @@
-import { createCheck, getChecks, getTeamChecks, deleteChecks, deleteChecksByTeamId, updateChecksTTL } from "../../controllers/checkController.js";
-import jwt from "jsonwebtoken";
-import { errorMessages, successMessages } from "../../utils/messages.js";
-import sinon from "sinon";
-describe("Check Controller - createCheck", function () {
- let req, res, next, handleError;
-
- beforeEach(function () {
- req = {
- params: {},
- body: {},
- db: {
- createCheck: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- handleError = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore(); // Restore the original methods after each test
- });
-
- it("should reject with a validation if params are invalid", async function () {
- await createCheck(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with a validation error if body is invalid", async function () {
- req.params = {
- monitorId: "monitorId",
- };
- await createCheck(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should call next with error if data retrieval fails", async function () {
- req.params = {
- monitorId: "monitorId",
- };
- req.body = {
- monitorId: "monitorId",
- status: true,
- responseTime: 100,
- statusCode: 200,
- message: "message",
- };
- req.db.createCheck.rejects(new Error("error"));
- await createCheck(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- });
-
- it("should return a success message if check is created", async function () {
- req.params = {
- monitorId: "monitorId",
- };
- req.db.createCheck.resolves({ id: "123" });
- req.body = {
- monitorId: "monitorId",
- status: true,
- responseTime: 100,
- statusCode: 200,
- message: "message",
- };
- await createCheck(req, res, next);
- expect(res.status.calledWith(200)).to.be.true;
- expect(
- res.json.calledWith({
- success: true,
- msg: successMessages.CHECK_CREATE,
- data: { id: "123" },
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-});
-
-describe("Check Controller - getChecks", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- params: {},
- query: {},
- db: {
- getChecks: sinon.stub(),
- getChecksCount: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with a validation error if params are invalid", async function () {
- await getChecks(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should return a success message if checks are found", async function () {
- req.params = {
- monitorId: "monitorId",
- };
- req.db.getChecks.resolves([{ id: "123" }]);
- req.db.getChecksCount.resolves(1);
- await getChecks(req, res, next);
- expect(res.status.calledWith(200)).to.be.true;
- expect(
- res.json.calledWith({
- success: true,
- msg: successMessages.CHECK_GET,
- data: { checksCount: 1, checks: [{ id: "123" }] },
- })
- ).to.be.true;
- expect(next.notCalled).to.be.true;
- });
-
- it("should call next with error if data retrieval fails", async function () {
- req.params = {
- monitorId: "monitorId",
- };
- req.db.getChecks.rejects(new Error("error"));
- await getChecks(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- });
-});
-
-describe("Check Controller - getTeamChecks", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- params: {},
- query: {},
- db: {
- getTeamChecks: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with a validation error if params are invalid", async function () {
- await getTeamChecks(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should return 200 and check data on successful validation and data retrieval", async function () {
- req.params = { teamId: "1" };
- const checkData = [{ id: 1, name: "Check 1" }];
- req.db.getTeamChecks.resolves(checkData);
-
- await getTeamChecks(req, res, next);
- expect(req.db.getTeamChecks.calledOnceWith(req)).to.be.true;
- expect(res.status.calledOnceWith(200)).to.be.true;
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.CHECK_GET,
- data: checkData,
- })
- ).to.be.true;
- });
-
- it("should call next with error if data retrieval fails", async function () {
- req.params = { teamId: "1" };
- req.db.getTeamChecks.rejects(new Error("Retrieval Error"));
- await getTeamChecks(req, res, next);
- expect(req.db.getTeamChecks.calledOnceWith(req)).to.be.true;
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(res.status.notCalled).to.be.true;
- expect(res.json.notCalled).to.be.true;
- });
-});
-
-describe("Check Controller - deleteChecks", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- params: {},
- db: {
- deleteChecks: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if param validation fails", async function () {
- await deleteChecks(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should call next with error if data retrieval fails", async function () {
- req.params = { monitorId: "1" };
- req.db.deleteChecks.rejects(new Error("Deletion Error"));
- await deleteChecks(req, res, next);
- expect(req.db.deleteChecks.calledOnceWith(req.params.monitorId)).to.be.true;
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(res.status.notCalled).to.be.true;
- expect(res.json.notCalled).to.be.true;
- });
-
- it("should delete checks successfully", async function () {
- req.params = { monitorId: "123" };
- req.db.deleteChecks.resolves(1);
- await deleteChecks(req, res, next);
- expect(req.db.deleteChecks.calledOnceWith(req.params.monitorId)).to.be.true;
- expect(res.status.calledOnceWith(200)).to.be.true;
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.CHECK_DELETE,
- data: { deletedCount: 1 },
- })
- ).to.be.true;
- });
-});
-
-describe("Check Controller - deleteChecksByTeamId", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- params: {},
- db: {
- deleteChecksByTeamId: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if param validation fails", async function () {
- await deleteChecksByTeamId(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should call next with error if data retrieval fails", async function () {
- req.params = { teamId: "1" };
- req.db.deleteChecksByTeamId.rejects(new Error("Deletion Error"));
- await deleteChecksByTeamId(req, res, next);
- expect(req.db.deleteChecksByTeamId.calledOnceWith(req.params.teamId)).to.be.true;
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(res.status.notCalled).to.be.true;
- expect(res.json.notCalled).to.be.true;
- });
-
- it("should delete checks successfully", async function () {
- req.params = { teamId: "123" };
- req.db.deleteChecksByTeamId.resolves(1);
- await deleteChecksByTeamId(req, res, next);
- expect(req.db.deleteChecksByTeamId.calledOnceWith(req.params.teamId)).to.be.true;
- expect(res.status.calledOnceWith(200)).to.be.true;
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.CHECK_DELETE,
- data: { deletedCount: 1 },
- })
- ).to.be.true;
- });
-});
-
-describe("Check Controller - updateCheckTTL", function () {
- let stub, req, res, next;
-
- beforeEach(function () {
- stub = sinon.stub(jwt, "verify").callsFake(() => {
- return { teamId: "123" };
- });
-
- req = {
- body: {},
- headers: { authorization: "Bearer token" },
- settingsService: {
- getSettings: sinon.stub().returns({ jwtSecret: "my_secret" }),
- },
- db: {
- updateChecksTTL: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- stub.restore();
- });
-
- it("should reject if body validation fails", async function () {
- await updateChecksTTL(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should throw a JwtError if verification fails", async function () {
- stub.restore();
- req.body = {
- ttl: 1,
- };
- await updateChecksTTL(req, res, next);
- expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError);
- });
-
- it("should call next with error if data retrieval fails", async function () {
- req.body = {
- ttl: 1,
- };
- req.db.updateChecksTTL.rejects(new Error("Update Error"));
- await updateChecksTTL(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- });
-
- it("should update TTL successfully", async function () {
- req.body = {
- ttl: 1,
- };
- req.db.updateChecksTTL.resolves();
- await updateChecksTTL(req, res, next);
- expect(req.db.updateChecksTTL.calledOnceWith("123", 1 * 86400)).to.be.true;
- expect(res.status.calledOnceWith(200)).to.be.true;
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.CHECK_UPDATE_TTL,
- })
- ).to.be.true;
- });
-});
diff --git a/server/tests/controllers/controllerUtils.test.js b/server/tests/controllers/controllerUtils.test.js
deleted file mode 100755
index a55318857..000000000
--- a/server/tests/controllers/controllerUtils.test.js
+++ /dev/null
@@ -1,157 +0,0 @@
-import sinon from "sinon";
-
-import { handleValidationError, handleError, fetchMonitorCertificate } from "../../controllers/controllerUtils.js";
-import { expect } from "chai";
-import sslChecker from "ssl-checker";
-import { afterEach } from "node:test";
-import exp from "constants";
-
-describe("controllerUtils - handleValidationError", function () {
- it("should set status to 422", function () {
- const error = {};
- const serviceName = "TestService";
- const result = handleValidationError(error, serviceName);
- expect(result.status).to.equal(422);
- });
-
- it("should set service to the provided serviceName", function () {
- const error = {};
- const serviceName = "TestService";
- const result = handleValidationError(error, serviceName);
- expect(result.service).to.equal(serviceName);
- });
-
- it("should set message to error.details[0].message if present", function () {
- const error = {
- details: [{ message: "Detail message" }],
- };
- const serviceName = "TestService";
- const result = handleValidationError(error, serviceName);
- expect(result.message).to.equal("Detail message");
- });
-
- it("should set message to error.message if error.details is not present", function () {
- const error = {
- message: "Error message",
- };
- const serviceName = "TestService";
- const result = handleValidationError(error, serviceName);
- expect(result.message).to.equal("Error message");
- });
-
- it('should set message to "Validation Error" if neither error.details nor error.message is present', function () {
- const error = {};
- const serviceName = "TestService";
- const result = handleValidationError(error, serviceName);
- expect(result.message).to.equal("Validation Error");
- });
-});
-
-describe("controllerUtils - handleError", function () {
- it("should set stats to the provided status if error.code is undefined", function () {
- const error = {};
- const serviceName = "TestService";
- const method = "testMethod";
- const status = 400;
- const result = handleError(error, serviceName, method, status);
- expect(result.status).to.equal(status);
- });
-
- it("should not overwrite error.code if it is already defined", function () {
- const error = { status: 404 };
- const serviceName = "TestService";
- const method = "testMethod";
- const status = 400;
- const result = handleError(error, serviceName, method, status);
- expect(result.status).to.equal(404);
- });
-
- it("should set service to the provided serviceName if error.service is undefined", function () {
- const error = {};
- const serviceName = "TestService";
- const method = "testMethod";
- const result = handleError(error, serviceName, method);
- expect(result.service).to.equal(serviceName);
- });
-
- it("should not overwrite error.service if it is already defined", function () {
- const error = { service: "ExistingService" };
- const serviceName = "TestService";
- const method = "testMethod";
- const result = handleError(error, serviceName, method);
- expect(result.service).to.equal("ExistingService");
- });
-
- it("should set method to the provided method if error.method is undefined", function () {
- const error = {};
- const serviceName = "TestService";
- const method = "testMethod";
- const result = handleError(error, serviceName, method);
- expect(result.method).to.equal(method);
- });
-
- it("should not overwrite error.method if it is already defined", function () {
- const error = { method: "existingMethod" };
- const serviceName = "TestService";
- const method = "testMethod";
- const result = handleError(error, serviceName, method);
- expect(result.method).to.equal("existingMethod");
- });
-
- it("should set code to 500 if error.code is undefined and no code is provided", function () {
- const error = {};
- const serviceName = "TestService";
- const method = "testMethod";
- const result = handleError(error, serviceName, method);
- expect(result.status).to.equal(500);
- });
-});
-
-describe("controllerUtils - fetchMonitorCertificate", function () {
- let sslChecker, monitor;
-
- beforeEach(function () {
- monitor = {
- url: "https://www.google.com",
- };
- sslChecker = sinon.stub();
- });
-
- afterEach(() => {
- sinon.restore();
- });
-
- it("should reject with an error if a URL does not parse", async function () {
- monitor.url = "invalidurl";
- try {
- await fetchMonitorCertificate(sslChecker, monitor);
- } catch (error) {
- expect(error).to.be.an("error");
- expect(error.message).to.equal("Invalid URL");
- }
- });
-
- it("should reject with an error if sslChecker throws an error", async function () {
- sslChecker.rejects(new Error("Test error"));
- try {
- await fetchMonitorCertificate(sslChecker, monitor);
- } catch (error) {
- expect(error).to.be.an("error");
- expect(error.message).to.equal("Test error");
- }
- });
-
- it("should return a certificate if sslChecker resolves", async function () {
- sslChecker.resolves({ validTo: "2022-01-01" });
- const result = await fetchMonitorCertificate(sslChecker, monitor);
- expect(result).to.deep.equal({ validTo: "2022-01-01" });
- });
-
- it("should throw an error if a ssl-checker returns null", async function () {
- sslChecker.returns(null);
- await fetchMonitorCertificate(sslChecker, monitor).catch((error) => {
- expect(error).to.be.an("error");
- expect(error.message).to.equal("Certificate not found");
- });
- });
-});
diff --git a/server/tests/controllers/inviteController.test.js b/server/tests/controllers/inviteController.test.js
deleted file mode 100755
index 635bdf653..000000000
--- a/server/tests/controllers/inviteController.test.js
+++ /dev/null
@@ -1,202 +0,0 @@
-import { issueInvitation, inviteVerifyController } from "../../controllers/inviteController.js";
-import jwt from "jsonwebtoken";
-import sinon from "sinon";
-import joi from "joi";
-describe("inviteController - issueInvitation", function () {
- let req, res, next, stub;
-
- beforeEach(function () {
- req = {
- headers: { authorization: "Bearer token" },
- body: {
- email: "test@test.com",
- role: ["admin"],
- teamId: "123",
- },
- db: { requestInviteToken: sinon.stub() },
- settingsService: { getSettings: sinon.stub() },
- emailService: { buildAndSendEmail: sinon.stub() },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if role validation fails", async function () {
- stub = sinon.stub(jwt, "decode").callsFake(() => {
- return { role: ["bad_role"], firstname: "first_name", teamId: "1" };
- });
- await issueInvitation(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0]).to.be.instanceOf(joi.ValidationError);
- expect(next.firstCall.args[0].status).to.equal(422);
- stub.restore();
- });
-
- it("should reject with an error if body validation fails", async function () {
- stub = sinon.stub(jwt, "decode").callsFake(() => {
- return { role: ["admin"], firstname: "first_name", teamId: "1" };
- });
- req.body = {};
- await issueInvitation(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- stub.restore();
- });
-
- it("should reject with an error if DB operations fail", async function () {
- stub = sinon.stub(jwt, "decode").callsFake(() => {
- return { role: ["admin"], firstname: "first_name", teamId: "1" };
- });
- req.db.requestInviteToken.throws(new Error("DB error"));
- await issueInvitation(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- stub.restore();
- });
-
- it("should send an invite successfully", async function () {
- const token = "token";
- const decodedToken = {
- role: "admin",
- firstname: "John",
- teamId: "team123",
- };
- const inviteToken = { token: "inviteToken" };
- const clientHost = "http://localhost";
-
- stub = sinon.stub(jwt, "decode").callsFake(() => {
- return decodedToken;
- });
- req.db.requestInviteToken.resolves(inviteToken);
- req.settingsService.getSettings.returns({ clientHost });
- req.emailService.buildAndSendEmail.resolves();
- await issueInvitation(req, res, next);
- expect(res.status.calledWith(200)).to.be.true;
- expect(
- res.json.calledWith({
- success: true,
- msg: "Invite sent",
- data: inviteToken,
- })
- ).to.be.true;
- stub.restore();
- });
-
- it("should send an email successfully", async function () {
- const token = "token";
- const decodedToken = {
- role: "admin",
- firstname: "John",
- teamId: "team123",
- };
- const inviteToken = { token: "inviteToken" };
- const clientHost = "http://localhost";
-
- stub = sinon.stub(jwt, "decode").callsFake(() => {
- return decodedToken;
- });
- req.db.requestInviteToken.resolves(inviteToken);
- req.settingsService.getSettings.returns({ clientHost });
- req.emailService.buildAndSendEmail.resolves();
-
- await issueInvitation(req, res, next);
- expect(req.emailService.buildAndSendEmail.calledOnce).to.be.true;
- expect(
- req.emailService.buildAndSendEmail.calledWith(
- "employeeActivationTemplate",
- {
- name: "John",
- link: "http://localhost/register/inviteToken",
- },
- "test@test.com",
- "Welcome to Uptime Monitor"
- )
- ).to.be.true;
- stub.restore();
- });
-
- it("should continue executing if sending an email fails", async function () {
- const token = "token";
- req.emailService.buildAndSendEmail.rejects(new Error("Email error"));
- const decodedToken = {
- role: "admin",
- firstname: "John",
- teamId: "team123",
- };
- const inviteToken = { token: "inviteToken" };
- const clientHost = "http://localhost";
-
- stub = sinon.stub(jwt, "decode").callsFake(() => {
- return decodedToken;
- });
- req.db.requestInviteToken.resolves(inviteToken);
- req.settingsService.getSettings.returns({ clientHost });
- await issueInvitation(req, res, next);
- expect(res.status.calledWith(200)).to.be.true;
- expect(
- res.json.calledWith({
- success: true,
- msg: "Invite sent",
- data: inviteToken,
- })
- ).to.be.true;
- stub.restore();
- });
-});
-
-describe("inviteController - inviteVerifyController", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- body: { token: "token" },
- db: {
- getInviteToken: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if body validation fails", async function () {
- req.body = {};
- await inviteVerifyController(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if DB operations fail", async function () {
- req.db.getInviteToken.throws(new Error("DB error"));
- await inviteVerifyController(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should return 200 and invite data when validation and invite retrieval are successful", async function () {
- req.db.getInviteToken.resolves({ invite: "data" });
- await inviteVerifyController(req, res, next);
- expect(res.status.calledWith(200)).to.be.true;
- expect(
- res.json.calledWith({
- status: "success",
- msg: "Invite verified",
- data: { invite: "data" },
- })
- ).to.be.true;
- expect(next.called).to.be.false;
- });
-});
diff --git a/server/tests/controllers/maintenanceWindowController.test.js b/server/tests/controllers/maintenanceWindowController.test.js
deleted file mode 100755
index 0ea07b389..000000000
--- a/server/tests/controllers/maintenanceWindowController.test.js
+++ /dev/null
@@ -1,422 +0,0 @@
-import {
- createMaintenanceWindows,
- getMaintenanceWindowById,
- getMaintenanceWindowsByTeamId,
- getMaintenanceWindowsByMonitorId,
- deleteMaintenanceWindow,
- editMaintenanceWindow,
-} from "../../controllers/maintenanceWindowController.js";
-
-import jwt from "jsonwebtoken";
-import { successMessages } from "../../utils/messages.js";
-import sinon from "sinon";
-
-describe("maintenanceWindowController - createMaintenanceWindows", function () {
- let req, res, next, stub;
-
- beforeEach(function () {
- req = {
- body: {
- monitors: ["66ff52e7c5911c61698ac724"],
- name: "window",
- active: true,
- start: "2024-10-11T05:27:13.747Z",
- end: "2024-10-11T05:27:14.747Z",
- repeat: "123",
- },
- headers: {
- authorization: "Bearer token",
- },
- settingsService: {
- getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }),
- },
- db: {
- createMaintenanceWindow: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if body validation fails", async function () {
- stub = sinon.stub(jwt, "verify").callsFake(() => {
- return { teamId: "123" };
- });
- req.body = {};
- await createMaintenanceWindows(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- stub.restore();
- });
-
- it("should reject with an error if jwt.verify fails", async function () {
- stub = sinon.stub(jwt, "verify").throws(new jwt.JsonWebTokenError());
- await createMaintenanceWindows(req, res, next);
- expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError);
- stub.restore();
- });
-
- it("should reject with an error DB operations fail", async function () {
- stub = sinon.stub(jwt, "verify").callsFake(() => {
- return { teamId: "123" };
- });
- req.db.createMaintenanceWindow.throws(new Error("DB error"));
- await createMaintenanceWindows(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- stub.restore();
- });
-
- it("should return success message if all operations are successful", async function () {
- stub = sinon.stub(jwt, "verify").callsFake(() => {
- return { teamId: "123" };
- });
- await createMaintenanceWindows(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(201);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MAINTENANCE_WINDOW_CREATE,
- })
- ).to.be.true;
- stub.restore();
- });
-
- it("should return success message if all operations are successful with active set to undefined", async function () {
- req.body.active = undefined;
- stub = sinon.stub(jwt, "verify").callsFake(() => {
- return { teamId: "123" };
- });
- await createMaintenanceWindows(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(201);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MAINTENANCE_WINDOW_CREATE,
- })
- ).to.be.true;
- stub.restore();
- });
-});
-
-describe("maintenanceWindowController - getMaintenanceWindowById", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- body: {},
- params: {
- id: "123",
- },
- headers: {
- authorization: "Bearer token",
- },
- settingsService: {
- getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }),
- },
- db: {
- getMaintenanceWindowById: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- it("should reject if param validation fails", async function () {
- req.params = {};
- await getMaintenanceWindowById(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject if DB operations fail", async function () {
- req.db.getMaintenanceWindowById.throws(new Error("DB error"));
- await getMaintenanceWindowById(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should return success message with data if all operations are successful", async function () {
- req.db.getMaintenanceWindowById.returns({ id: "123" });
- await getMaintenanceWindowById(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MAINTENANCE_WINDOW_GET_BY_ID,
- data: { id: "123" },
- })
- ).to.be.true;
- });
-});
-
-describe("maintenanceWindowController - getMaintenanceWindowsByTeamId", function () {
- let req, res, next, stub;
-
- beforeEach(function () {
- req = {
- body: {},
- params: {},
- query: {},
- headers: {
- authorization: "Bearer token",
- },
- settingsService: {
- getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }),
- },
- db: {
- getMaintenanceWindowsByTeamId: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- it("should reject if query validation fails", async function () {
- req.query = {
- invalid: 1,
- };
- await getMaintenanceWindowsByTeamId(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject if jwt.verify fails", async function () {
- stub = sinon.stub(jwt, "verify").throws(new jwt.JsonWebTokenError());
- await getMaintenanceWindowsByTeamId(req, res, next);
- expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError);
- stub.restore();
- });
-
- it("should reject with an error if DB operations fail", async function () {
- stub = sinon.stub(jwt, "verify").callsFake(() => {
- return { teamId: "123" };
- });
- req.db.getMaintenanceWindowsByTeamId.throws(new Error("DB error"));
- await getMaintenanceWindowsByTeamId(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- stub.restore();
- });
-
- it("should return success message with data if all operations are successful", async function () {
- stub = sinon.stub(jwt, "verify").callsFake(() => {
- return { teamId: "123" };
- });
- req.db.getMaintenanceWindowsByTeamId.returns([{ id: "123" }]);
- await getMaintenanceWindowsByTeamId(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MAINTENANCE_WINDOW_GET_BY_TEAM,
- data: [{ id: jwt.verify().teamId }],
- })
- ).to.be.true;
- stub.restore();
- });
-});
-
-describe("maintenanceWindowController - getMaintenanceWindowsByMonitorId", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- body: {},
- params: {
- monitorId: "123",
- },
- query: {},
- headers: {
- authorization: "Bearer token",
- },
- settingsService: {
- getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }),
- },
- db: {
- getMaintenanceWindowsByMonitorId: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject if param validation fails", async function () {
- req.params = {};
- await getMaintenanceWindowsByMonitorId(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if DB operations fail", async function () {
- req.db.getMaintenanceWindowsByMonitorId.throws(new Error("DB error"));
- await getMaintenanceWindowsByMonitorId(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should return success message with data if all operations are successful", async function () {
- const data = [{ monitorId: "123" }];
- req.db.getMaintenanceWindowsByMonitorId.returns(data);
- await getMaintenanceWindowsByMonitorId(req, res, next);
- expect(req.db.getMaintenanceWindowsByMonitorId.calledOnceWith(req.params.monitorId));
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MAINTENANCE_WINDOW_GET_BY_MONITOR,
- data: data,
- })
- ).to.be.true;
- });
-});
-
-describe("maintenanceWindowController - deleteMaintenanceWindow", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- body: {},
- params: {
- id: "123",
- },
- query: {},
- headers: {
- authorization: "Bearer token",
- },
- settingsService: {
- getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }),
- },
- db: {
- deleteMaintenanceWindowById: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject if param validation fails", async function () {
- req.params = {};
- await deleteMaintenanceWindow(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if DB operations fail", async function () {
- req.db.deleteMaintenanceWindowById.throws(new Error("DB error"));
- await deleteMaintenanceWindow(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should return success message if all operations are successful", async function () {
- await deleteMaintenanceWindow(req, res, next);
- expect(req.db.deleteMaintenanceWindowById.calledOnceWith(req.params.id));
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MAINTENANCE_WINDOW_DELETE,
- })
- ).to.be.true;
- });
-});
-
-describe("maintenanceWindowController - editMaintenanceWindow", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- body: {
- active: true,
- name: "test",
- },
- params: {
- id: "123",
- },
- query: {},
- headers: {
- authorization: "Bearer token",
- },
- settingsService: {
- getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }),
- },
- db: {
- editMaintenanceWindowById: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject if param validation fails", async function () {
- req.params = {};
- await editMaintenanceWindow(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject if body validation fails", async function () {
- req.body = { invalid: 1 };
- await editMaintenanceWindow(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if DB operations fail", async function () {
- req.db.editMaintenanceWindowById.throws(new Error("DB error"));
- await editMaintenanceWindow(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should return success message with data if all operations are successful", async function () {
- const data = { id: "123" };
- req.db.editMaintenanceWindowById.returns(data);
-
- await editMaintenanceWindow(req, res, next);
- expect(req.db.editMaintenanceWindowById.calledOnceWith(req.params.id, req.body));
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MAINTENANCE_WINDOW_EDIT,
- data: data,
- })
- ).to.be.true;
- });
-});
diff --git a/server/tests/controllers/monitorController.test.js b/server/tests/controllers/monitorController.test.js
deleted file mode 100755
index 963862579..000000000
--- a/server/tests/controllers/monitorController.test.js
+++ /dev/null
@@ -1,1116 +0,0 @@
-import {
- getAllMonitors,
- getAllMonitorsWithUptimeStats,
- getMonitorStatsById,
- getMonitorCertificate,
- getMonitorById,
- getMonitorsAndSummaryByTeamId,
- getMonitorsByTeamId,
- createMonitor,
- checkEndpointResolution,
- deleteMonitor,
- deleteAllMonitors,
- editMonitor,
- pauseMonitor,
- addDemoMonitors,
-} from "../../controllers/monitorController.js";
-import jwt from "jsonwebtoken";
-import sinon from "sinon";
-import { successMessages } from "../../utils/messages.js";
-import logger from "../../utils/logger.js";
-import axios from "axios";
-const SERVICE_NAME = "monitorController";
-
-describe("Monitor Controller - getAllMonitors", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- params: {},
- query: {},
- body: {},
- db: {
- getAllMonitors: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if DB operations fail", async function () {
- req.db.getAllMonitors.throws(new Error("DB error"));
- await getAllMonitors(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should return success message and data if all operations succeed", async function () {
- const data = [{ monitor: "data" }];
- req.db.getAllMonitors.returns(data);
- await getAllMonitors(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MONITOR_GET_ALL,
- data: data,
- })
- ).to.be.true;
- });
-});
-describe("Monitor Controller - getAllMonitorsWithUptimeStats", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- params: {},
- query: {},
- body: {},
- db: {
- getAllMonitorsWithUptimeStats: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if DB operations fail", async function () {
- req.db.getAllMonitorsWithUptimeStats.throws(new Error("DB error"));
- await getAllMonitorsWithUptimeStats(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should return success message and data if all operations succeed", async function () {
- const data = [{ monitor: "data" }];
- req.db.getAllMonitorsWithUptimeStats.returns(data);
- await getAllMonitorsWithUptimeStats(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MONITOR_GET_ALL,
- data: data,
- })
- ).to.be.true;
- });
-});
-
-describe("Monitor Controller - getMonitorStatsById", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- params: {
- monitorId: "123",
- },
- query: {},
- body: {},
- db: {
- getMonitorStatsById: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if param validation fails", async function () {
- req.params = {};
- await getMonitorStatsById(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if query validation fails", async function () {
- req.query = { invalid: 1 };
- await getMonitorStatsById(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if DB operations fail", async function () {
- req.db.getMonitorStatsById.throws(new Error("DB error"));
- await getMonitorStatsById(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should return success message and data if all operations succeed", async function () {
- const data = [{ monitorStats: "data" }];
- req.db.getMonitorStatsById.returns(data);
- await getMonitorStatsById(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MONITOR_STATS_BY_ID,
- data: data,
- })
- ).to.be.true;
- });
-});
-
-describe("Monitor Controller - getMonitorCertificate", function () {
- let req, res, next, fetchMonitorCertificate;
-
- beforeEach(function () {
- req = {
- params: {
- monitorId: "123",
- },
- query: {},
- body: {},
- db: {
- getMonitorById: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- fetchMonitorCertificate = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if param validation fails", async function () {
- req.params = {};
- await getMonitorCertificate(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if getMonitorById operation fails", async function () {
- req.db.getMonitorById.throws(new Error("DB error"));
- await getMonitorCertificate(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should return success message and data if all operations succeed with a valid cert", async function () {
- req.db.getMonitorById.returns({ url: "https://www.google.com" });
- const data = { certificate: "cert", validTo: "2024/08/08" };
- fetchMonitorCertificate.returns(data);
- await getMonitorCertificate(req, res, next, fetchMonitorCertificate);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MONITOR_CERTIFICATE,
- data: { certificateDate: new Date(data.validTo) },
- })
- ).to.be.true;
- });
-
- it("should return an error if fetchMonitorCertificate fails", async function () {
- req.db.getMonitorById.returns({ url: "https://www.google.com" });
- fetchMonitorCertificate.throws(new Error("Certificate error"));
- await getMonitorCertificate(req, res, next, fetchMonitorCertificate);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("Certificate error");
- });
-});
-
-describe("Monitor Controller - getMonitorById", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- params: {
- monitorId: "123",
- },
- query: {},
- body: {},
- db: {
- getMonitorById: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if param validation fails", async function () {
- req.params = {};
- await getMonitorById(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if query param validation fails", async function () {
- req.query = { invalid: 1 };
- await getMonitorById(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if DB operations fail", async function () {
- req.db.getMonitorById.throws(new Error("DB error"));
- await getMonitorById(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should return 404 if a monitor is not found", async function () {
- const error = new Error("Monitor not found");
- error.status = 404;
- req.db.getMonitorById.throws(error);
- await getMonitorById(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(404);
- });
-
- it("should return success message and data if all operations succeed", async function () {
- const data = { monitor: "data" };
- req.db.getMonitorById.returns(data);
- await getMonitorById(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MONITOR_GET_BY_ID,
- data: data,
- })
- ).to.be.true;
- });
-});
-
-describe("Monitor Controller - getMonitorsAndSummaryByTeamId", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- params: {
- teamId: "123",
- },
- query: {},
- body: {},
- db: {
- getMonitorsAndSummaryByTeamId: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if param validation fails", async function () {
- req.params = {};
- await getMonitorsAndSummaryByTeamId(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if query validation fails", async function () {
- req.query = { invalid: 1 };
- await getMonitorsAndSummaryByTeamId(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if DB operations fail", async function () {
- req.db.getMonitorsAndSummaryByTeamId.throws(new Error("DB error"));
- await getMonitorsAndSummaryByTeamId(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should return success message and data if all operations succeed", async function () {
- const data = { monitors: "data", summary: "data" };
- req.db.getMonitorsAndSummaryByTeamId.returns(data);
- await getMonitorsAndSummaryByTeamId(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MONITOR_GET_BY_USER_ID(req.params.teamId),
- data: data,
- })
- ).to.be.true;
- });
-});
-
-describe("Monitor Controller - getMonitorsByTeamId", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- params: {
- teamId: "123",
- },
- query: {},
- body: {},
- db: {
- getMonitorsByTeamId: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if param validation fails", async function () {
- req.params = {};
- await getMonitorsByTeamId(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if query validation fails", async function () {
- req.query = { invalid: 1 };
- await getMonitorsByTeamId(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if DB operations fail", async function () {
- req.db.getMonitorsByTeamId.throws(new Error("DB error"));
- await getMonitorsByTeamId(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should return success message and data if all operations succeed", async function () {
- const data = { monitors: "data" };
- req.db.getMonitorsByTeamId.returns(data);
- await getMonitorsByTeamId(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MONITOR_GET_BY_USER_ID(req.params.teamId),
- data: data,
- })
- ).to.be.true;
- });
-});
-
-describe("Monitor Controller - createMonitor", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- params: {},
- query: {},
- body: {
- userId: "123",
- teamId: "123",
- name: "test_monitor",
- description: "test_monitor_desc",
- type: "http",
- url: "https://example.com",
- notifications: [{ email: "example@example.com" }],
- },
- db: {
- createMonitor: sinon.stub(),
- createNotification: sinon.stub(),
- },
- jobQueue: {
- addJob: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if body validation fails", async function () {
- req.body = {};
- await createMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if DB createMonitor operation fail", async function () {
- req.db.createMonitor.throws(new Error("DB error"));
- await createMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should reject with an error if DB createNotification operation fail", async function () {
- req.db.createNotification.throws(new Error("DB error"));
- req.db.createMonitor.returns({ _id: "123" });
- await createMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should reject with an error if monitor.save operation fail", async function () {
- req.db.createMonitor.returns({
- _id: "123",
- save: sinon.stub().throws(new Error("Monitor save error")),
- });
- await createMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("Monitor save error");
- });
-
- it("should throw an error if addJob operation fails", async function () {
- req.db.createMonitor.returns({ _id: "123", save: sinon.stub() });
- req.jobQueue.addJob.throws(new Error("Job error"));
- await createMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("Job error");
- });
-
- it("should return success message and data if all operations succeed", async function () {
- const monitor = { _id: "123", save: sinon.stub() };
- req.db.createMonitor.returns(monitor);
- await createMonitor(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(201);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MONITOR_CREATE,
- data: monitor,
- })
- ).to.be.true;
- });
-});
-
-describe("Monitor Controller - checkEndpointResolution", function () {
- let req, res, next, axiosGetStub;
-
- beforeEach(function () {
- req = { query: { monitorURL: "https://example.com" } };
- res = { status: sinon.stub().returnsThis(), json: sinon.stub() };
- next = sinon.stub();
- axiosGetStub = sinon.stub(axios, "get");
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should resolve the URL successfully", async function () {
- axiosGetStub.resolves({ status: 200, statusText: "OK" });
- await checkEndpointResolution(req, res, next);
- expect(res.status.calledWith(200)).to.be.true;
- expect(
- res.json.calledWith({
- success: true,
- code: 200,
- statusText: "OK",
- msg: "URL resolved successfully",
- })
- ).to.be.true;
- expect(next.called).to.be.false;
- });
-
- it("should return an error if endpoint resolution fails", async function () {
- const axiosError = new Error("resolution failed");
- axiosError.code = "ENOTFOUND";
- axiosGetStub.rejects(axiosError);
- await checkEndpointResolution(req, res, next);
- expect(next.calledOnce).to.be.true;
- const errorPassedToNext = next.getCall(0).args[0];
- expect(errorPassedToNext).to.be.an.instanceOf(Error);
- expect(errorPassedToNext.message).to.include("resolution failed");
- expect(errorPassedToNext.code).to.equal("ENOTFOUND");
- expect(errorPassedToNext.status).to.equal(500);
- });
-
- it("should reject with an error if query validation fails", async function () {
- req.query.monitorURL = "invalid-url";
- await checkEndpointResolution(req, res, next);
- expect(next.calledOnce).to.be.true;
- const error = next.getCall(0).args[0];
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- expect(error.message).to.equal('"monitorURL" must be a valid uri');
- });
-});
-
-describe("Monitor Controller - deleteMonitor", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- params: {
- monitorId: "123",
- },
- query: {},
- body: {},
- db: {
- deleteMonitor: sinon.stub(),
- deleteChecks: sinon.stub(),
- deletePageSpeedChecksByMonitorId: sinon.stub(),
- deleteNotificationsByMonitorId: sinon.stub(),
- },
- jobQueue: {
- deleteJob: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- sinon.stub(logger, "error");
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if param validation fails", async function () {
- req.params = {};
- await deleteMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if DB deleteMonitor operation fail", async function () {
- req.db.deleteMonitor.throws(new Error("DB error"));
- await deleteMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should log an error if deleteJob throws an error", async function () {
- const error = new Error("Job error");
- const monitor = { name: "test_monitor", _id: "123" };
- req.db.deleteMonitor.returns(monitor);
- req.jobQueue.deleteJob.rejects(error);
- await deleteMonitor(req, res, next);
- expect(logger.error.calledOnce).to.be.true;
- expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`);
- });
-
- it("should log an error if deleteChecks throws an error", async function () {
- const error = new Error("Checks error");
- const monitor = { name: "test_monitor", _id: "123" };
- req.db.deleteMonitor.returns(monitor);
- req.db.deleteChecks.rejects(error);
- await deleteMonitor(req, res, next);
- expect(logger.error.calledOnce).to.be.true;
- expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`);
- });
-
- it("should log an error if deletePageSpeedChecksByMonitorId throws an error", async function () {
- const error = new Error("PageSpeed error");
- const monitor = { name: "test_monitor", _id: "123" };
- req.db.deleteMonitor.returns(monitor);
- req.db.deletePageSpeedChecksByMonitorId.rejects(error);
- await deleteMonitor(req, res, next);
- expect(logger.error.calledOnce).to.be.true;
- expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`);
- });
-
- it("should log an error if deleteNotificationsByMonitorId throws an error", async function () {
- const error = new Error("Notifications error");
- const monitor = { name: "test_monitor", _id: "123" };
- req.db.deleteMonitor.returns(monitor);
- req.db.deleteNotificationsByMonitorId.rejects(error);
- await deleteMonitor(req, res, next);
- expect(logger.error.calledOnce).to.be.true;
- expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`);
- });
-
- it("should return success message if all operations succeed", async function () {
- const monitor = { name: "test_monitor", _id: "123" };
- req.db.deleteMonitor.returns(monitor);
- await deleteMonitor(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MONITOR_DELETE,
- })
- ).to.be.true;
- });
-});
-
-describe("Monitor Controller - deleteAllMonitors", function () {
- let req, res, next, stub;
-
- beforeEach(function () {
- stub = sinon.stub(jwt, "verify").callsFake(() => {
- return { teamId: "123" };
- });
- req = {
- headers: {
- authorization: "Bearer token",
- },
- params: {
- monitorId: "123",
- },
- query: {},
- body: {},
- db: {
- deleteAllMonitors: sinon.stub(),
- deleteChecks: sinon.stub(),
- deletePageSpeedChecksByMonitorId: sinon.stub(),
- deleteNotificationsByMonitorId: sinon.stub(),
- },
- jobQueue: {
- deleteJob: sinon.stub(),
- },
- settingsService: {
- getSettings: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- sinon.stub(logger, "error");
- });
-
- afterEach(function () {
- sinon.restore();
- stub.restore();
- });
-
- it("should reject with an error if getTokenFromHeaders throws an error", async function () {
- req.headers = {};
- await deleteAllMonitors(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("No auth headers");
- expect(next.firstCall.args[0].status).to.equal(500);
- });
-
- it("should reject with an error if token validation fails", async function () {
- stub.restore();
- req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
- await deleteAllMonitors(req, res, next);
- expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError);
- });
-
- it("should reject with an error if DB deleteAllMonitors operation fail", async function () {
- req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
- req.db.deleteAllMonitors.throws(new Error("DB error"));
- await deleteAllMonitors(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should log an error if deleteChecks throws an error", async function () {
- const monitors = [{ name: "test_monitor", _id: "123" }];
- req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
- req.db.deleteAllMonitors.returns({ monitors, deletedCount: 1 });
- const error = new Error("Check error");
- req.db.deleteChecks.rejects(error);
- await deleteAllMonitors(req, res, next);
- expect(logger.error.calledOnce).to.be.true;
- expect(logger.error.firstCall.args[0].message).to.equal(
- `Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}`
- );
- });
-
- it("should log an error if deletePageSpeedChecksByMonitorId throws an error", async function () {
- const monitors = [{ name: "test_monitor", _id: "123" }];
- req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
- req.db.deleteAllMonitors.returns({ monitors, deletedCount: 1 });
- const error = new Error("Pagespeed Check error");
- req.db.deletePageSpeedChecksByMonitorId.rejects(error);
- await deleteAllMonitors(req, res, next);
- expect(logger.error.calledOnce).to.be.true;
- expect(logger.error.firstCall.args[0].message).to.equal(
- `Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}`
- );
- });
-
- it("should log an error if deleteNotificationsByMonitorId throws an error", async function () {
- const monitors = [{ name: "test_monitor", _id: "123" }];
- req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
- req.db.deleteAllMonitors.returns({ monitors, deletedCount: 1 });
- const error = new Error("Notifications Check error");
- req.db.deleteNotificationsByMonitorId.rejects(error);
- await deleteAllMonitors(req, res, next);
- expect(logger.error.calledOnce).to.be.true;
- expect(logger.error.firstCall.args[0].message).to.equal(
- `Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}`
- );
- });
-
- it("should return success message if all operations succeed", async function () {
- req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
- req.db.deleteAllMonitors.returns({
- monitors: [{ name: "test_monitor", _id: "123" }],
- deletedCount: 1,
- });
- await deleteAllMonitors(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: "Deleted 1 monitors",
- })
- ).to.be.true;
- });
-});
-
-describe("Monitor Controller - editMonitor", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- headers: {},
- params: {
- monitorId: "123",
- },
- query: {},
- body: {
- notifications: [{ email: "example@example.com" }],
- },
- db: {
- getMonitorById: sinon.stub(),
- editMonitor: sinon.stub(),
- deleteNotificationsByMonitorId: sinon.stub(),
- createNotification: sinon.stub(),
- },
- jobQueue: {
- deleteJob: sinon.stub(),
- addJob: sinon.stub(),
- },
- settingsService: {
- getSettings: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if param validation fails", async function () {
- req.params = {};
- await editMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if body validation fails", async function () {
- req.body = { invalid: 1 };
- await editMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if getMonitorById operation fails", async function () {
- req.db.getMonitorById.throws(new Error("DB error"));
- await editMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should reject with an error if editMonitor operation fails", async function () {
- req.db.getMonitorById.returns({ teamId: "123" });
- req.db.editMonitor.throws(new Error("DB error"));
- await editMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should reject with an error if deleteNotificationsByMonitorId operation fails", async function () {
- req.db.getMonitorById.returns({ teamId: "123" });
- req.db.editMonitor.returns({ _id: "123" });
- req.db.deleteNotificationsByMonitorId.throws(new Error("DB error"));
- await editMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should reject with an error if createNotification operation fails", async function () {
- req.db.getMonitorById.returns({ teamId: "123" });
- req.db.editMonitor.returns({ _id: "123" });
- req.db.createNotification.throws(new Error("DB error"));
- await editMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should reject with an error if deleteJob operation fails", async function () {
- req.db.getMonitorById.returns({ teamId: "123" });
- req.db.editMonitor.returns({ _id: "123" });
- req.jobQueue.deleteJob.throws(new Error("Job error"));
- await editMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("Job error");
- });
-
- it("should reject with an error if addJob operation fails", async function () {
- req.db.getMonitorById.returns({ teamId: "123" });
- req.db.editMonitor.returns({ _id: "123" });
- req.jobQueue.addJob.throws(new Error("Add Job error"));
- await editMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("Add Job error");
- });
-
- it("should return success message with data if all operations succeed", async function () {
- const monitor = { _id: "123" };
- req.db.getMonitorById.returns({ teamId: "123" });
- req.db.editMonitor.returns(monitor);
- await editMonitor(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MONITOR_EDIT,
- data: monitor,
- })
- ).to.be.true;
- });
-});
-
-describe("Monitor Controller - pauseMonitor", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- headers: {},
- params: {
- monitorId: "123",
- },
- query: {},
- body: {},
- db: {
- getMonitorById: sinon.stub(),
- },
- jobQueue: {
- deleteJob: sinon.stub(),
- addJob: sinon.stub(),
- },
- settingsService: {
- getSettings: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should reject with an error if param validation fails", async function () {
- req.params = {};
- await pauseMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if getMonitorById operation fails", async function () {
- req.db.getMonitorById.throws(new Error("DB error"));
- await pauseMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should reject with an error if deleteJob operation fails", async function () {
- const monitor = { _id: req.params.monitorId, isActive: true };
- req.db.getMonitorById.returns(monitor);
- req.jobQueue.deleteJob.throws(new Error("Delete Job error"));
- await pauseMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("Delete Job error");
- });
-
- it("should reject with an error if addJob operation fails", async function () {
- const monitor = { _id: req.params.monitorId, isActive: false };
- req.db.getMonitorById.returns(monitor);
- req.jobQueue.addJob.throws(new Error("Add Job error"));
- await pauseMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("Add Job error");
- });
-
- it("should reject with an error if monitor.save operation fails", async function () {
- const monitor = {
- _id: req.params.monitorId,
- active: false,
- save: sinon.stub().throws(new Error("Save error")),
- };
- req.db.getMonitorById.returns(monitor);
- await pauseMonitor(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("Save error");
- });
-
- it("should return success pause message with data if all operations succeed with inactive monitor", async function () {
- const monitor = {
- _id: req.params.monitorId,
- isActive: false,
- save: sinon.stub().resolves(),
- };
- req.db.getMonitorById.returns(monitor);
- await pauseMonitor(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MONITOR_PAUSE,
- data: monitor,
- })
- ).to.be.true;
- });
-
- it("should return success resume message with data if all operations succeed with active monitor", async function () {
- const monitor = {
- _id: req.params.monitorId,
- isActive: true,
- save: sinon.stub().resolves(),
- };
- req.db.getMonitorById.returns(monitor);
- await pauseMonitor(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MONITOR_RESUME,
- data: monitor,
- })
- ).to.be.true;
- });
-});
-
-describe("Monitor Controller - addDemoMonitors", function () {
- let req, res, next, stub;
-
- beforeEach(function () {
- stub = sinon.stub(jwt, "verify").callsFake(() => {
- return { _id: "123", teamId: "123" };
- });
- req = {
- headers: {
- authorization: "Bearer token",
- },
- params: {},
- query: {},
- body: {},
- db: {
- addDemoMonitors: sinon.stub(),
- },
- settingsService: {
- getSettings: sinon.stub(),
- },
- jobQueue: {
- addJob: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- stub.restore();
- });
-
- it("should reject with an error if getTokenFromHeaders fails", async function () {
- req.headers = {};
- await addDemoMonitors(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("No auth headers");
- expect(next.firstCall.args[0].status).to.equal(500);
- });
-
- it("should reject with an error if getting settings fails", async function () {
- req.settingsService.getSettings.throws(new Error("Settings error"));
- await addDemoMonitors(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("Settings error");
- });
-
- it("should reject with an error if JWT validation fails", async function () {
- stub.restore();
- req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
- await addDemoMonitors(req, res, next);
- expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError);
- });
-
- it("should reject with an error if addDemoMonitors operation fails", async function () {
- req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
- req.db.addDemoMonitors.throws(new Error("DB error"));
- await addDemoMonitors(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("DB error");
- });
-
- it("should reject with an error if addJob operation fails", async function () {
- req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
- req.db.addDemoMonitors.returns([{ _id: "123" }]);
- req.jobQueue.addJob.throws(new Error("Add Job error"));
- await addDemoMonitors(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("Add Job error");
- });
-
- it("should return success message with data if all operations succeed", async function () {
- const monitors = [{ _id: "123" }];
- req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
- req.db.addDemoMonitors.returns(monitors);
- await addDemoMonitors(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(
- res.json.calledOnceWith({
- success: true,
- msg: successMessages.MONITOR_DEMO_ADDED,
- data: monitors.length,
- })
- ).to.be.true;
- });
-});
diff --git a/server/tests/controllers/queueController.test.js b/server/tests/controllers/queueController.test.js
deleted file mode 100755
index 6201bdb5a..000000000
--- a/server/tests/controllers/queueController.test.js
+++ /dev/null
@@ -1,176 +0,0 @@
-import { afterEach } from "node:test";
-import { getMetrics, getJobs, addJob, obliterateQueue } from "../../controllers/queueController.js";
-import { successMessages } from "../../utils/messages.js";
-import sinon from "sinon";
-
-describe("Queue Controller - getMetrics", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- headers: {},
- params: {},
- body: {},
- db: {},
- jobQueue: {
- getMetrics: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(() => {
- sinon.restore();
- });
-
- it("should throw an error if getMetrics throws an error", async function () {
- req.jobQueue.getMetrics.throws(new Error("getMetrics error"));
- await getMetrics(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("getMetrics error");
- });
-
- it("should return a success message and data if getMetrics is successful", async function () {
- const data = { data: "metrics" };
- req.jobQueue.getMetrics.returns(data);
- await getMetrics(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(res.json.firstCall.args[0]).to.deep.equal({
- success: true,
- msg: successMessages.QUEUE_GET_METRICS,
- data,
- });
- });
-});
-
-describe("Queue Controller - getJobs", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- headers: {},
- params: {},
- body: {},
- db: {},
- jobQueue: {
- getJobStats: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(() => {
- sinon.restore();
- });
-
- it("should reject with an error if getJobs throws an error", async function () {
- req.jobQueue.getJobStats.throws(new Error("getJobs error"));
- await getJobs(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("getJobs error");
- });
-
- it("should return a success message and data if getJobs is successful", async function () {
- const data = { data: "jobs" };
- req.jobQueue.getJobStats.returns(data);
- await getJobs(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(res.json.firstCall.args[0]).to.deep.equal({
- success: true,
- msg: successMessages.QUEUE_GET_METRICS,
- data,
- });
- });
-});
-
-describe("Queue Controller - addJob", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- headers: {},
- params: {},
- body: {},
- db: {},
- jobQueue: {
- addJob: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(() => {
- sinon.restore();
- });
-
- it("should reject with an error if addJob throws an error", async function () {
- req.jobQueue.addJob.throws(new Error("addJob error"));
- await addJob(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("addJob error");
- });
-
- it("should return a success message if addJob is successful", async function () {
- req.jobQueue.addJob.resolves();
- await addJob(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(res.json.firstCall.args[0]).to.deep.equal({
- success: true,
- msg: successMessages.QUEUE_ADD_JOB,
- });
- });
-});
-
-describe("Queue Controller - obliterateQueue", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- headers: {},
- params: {},
- body: {},
- db: {},
- jobQueue: {
- obliterate: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(() => {
- sinon.restore();
- });
-
- it("should reject with an error if obliterateQueue throws an error", async function () {
- req.jobQueue.obliterate.throws(new Error("obliterateQueue error"));
- await obliterateQueue(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("obliterateQueue error");
- });
-
- it("should return a success message if obliterateQueue is successful", async function () {
- req.jobQueue.obliterate.resolves();
- await obliterateQueue(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(res.json.firstCall.args[0]).to.deep.equal({
- success: true,
- msg: successMessages.QUEUE_OBLITERATE,
- });
- });
-});
diff --git a/server/tests/controllers/settingsController.test.js b/server/tests/controllers/settingsController.test.js
deleted file mode 100755
index 721c41c02..000000000
--- a/server/tests/controllers/settingsController.test.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import { afterEach } from "node:test";
-import { getAppSettings, updateAppSettings } from "../../controllers/settingsController.js";
-
-import { successMessages } from "../../utils/messages.js";
-import sinon from "sinon";
-
-describe("Settings Controller - getAppSettings", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- headers: {},
- params: {},
- body: {},
- db: {},
- settingsService: {
- getSettings: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(() => {
- sinon.restore();
- });
-
- it("should throw an error if getSettings throws an error", async function () {
- req.settingsService.getSettings.throws(new Error("getSettings error"));
- await getAppSettings(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("getSettings error");
- });
-
- it("should return a success message and data if getSettings is successful", async function () {
- const data = { data: "settings" };
- req.settingsService.getSettings.returns(data);
- await getAppSettings(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(res.json.firstCall.args[0]).to.deep.equal({
- success: true,
- msg: successMessages.GET_APP_SETTINGS,
- data,
- });
- });
-});
-
-describe("Settings Controller - updateAppSettings", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- headers: {},
- params: {},
- body: {},
- db: {
- updateAppSettings: sinon.stub(),
- },
- settingsService: {
- reloadSettings: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(() => {
- sinon.restore();
- });
-
- it("should reject with an error if body validation fails", async function () {
- req.body = { invalid: 1 };
- await updateAppSettings(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].status).to.equal(422);
- });
-
- it("should reject with an error if updateAppSettings throws an error", async function () {
- req.db.updateAppSettings.throws(new Error("updateAppSettings error"));
- await updateAppSettings(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("updateAppSettings error");
- });
-
- it("should reject with an error if reloadSettings throws an error", async function () {
- req.settingsService.reloadSettings.throws(new Error("reloadSettings error"));
- await updateAppSettings(req, res, next);
- expect(next.firstCall.args[0]).to.be.an("error");
- expect(next.firstCall.args[0].message).to.equal("reloadSettings error");
- });
-
- it("should return a success message and data if updateAppSettings is successful", async function () {
- const data = { data: "settings" };
- req.settingsService.reloadSettings.returns(data);
- await updateAppSettings(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(res.json.firstCall.args[0]).to.deep.equal({
- success: true,
- msg: successMessages.UPDATE_APP_SETTINGS,
- data,
- });
- });
-});
diff --git a/server/tests/controllers/statusPageController.test.js b/server/tests/controllers/statusPageController.test.js
deleted file mode 100755
index cada662e0..000000000
--- a/server/tests/controllers/statusPageController.test.js
+++ /dev/null
@@ -1,127 +0,0 @@
-import sinon from "sinon";
-import { createStatusPage, getStatusPageByUrl } from "../../controllers/statusPageController.js";
-
-describe("statusPageController", function () {
- let req, res, next;
-
- beforeEach(function () {
- req = {
- params: {},
- body: {},
- db: {
- createStatusPage: sinon.stub(),
- getStatusPageByUrl: sinon.stub(),
- },
- };
- res = {
- status: sinon.stub().returnsThis(),
- json: sinon.stub(),
- };
- next = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("createStatusPage", function () {
- beforeEach(function () {
- req.body = {
- companyName: "Test Company",
- url: "123456",
- timezone: "America/Toronto",
- color: "#000000",
- theme: "light",
- monitors: ["67309ca673788e808884c8ac", "67309ca673788e808884c8ac"],
- };
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should handle a validation error", async function () {
- req.body = {
- // Invalid data that will trigger validation error
- companyName: "",
- url: "",
- timezone: "",
- color: "invalid",
- theme: "invalid",
- monitors: ["invalid-id"],
- };
- try {
- await createStatusPage(req, res, next);
- } catch (error) {
- expect(error).to.be.an.instanceOf(Error);
- expect(error.message).to.equal("Validation error");
- }
- });
-
- it("should handle a db error", async function () {
- const err = new Error("DB error");
- req.db.createStatusPage.throws(err);
-
- try {
- await createStatusPage(req, res, next);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
-
- it("should insert a properly formatted status page", async function () {
- const result = await createStatusPage(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(res.json.firstCall.args[0].success).to.be.true;
- });
- });
-
- describe("getStatusPageByUrl", function () {
- beforeEach(function () {
- req.params = {
- url: "123456",
- };
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should handle a validation error", async function () {
- req.params = {
- url: "",
- };
-
- try {
- await getStatusPageByUrl(req, res, next);
- } catch (error) {
- expect(error).to.be.an.instanceOf(Error);
- expect(error.message).to.equal("Validation error");
- }
- });
-
- it("should handle a DB error", async function () {
- const err = new Error("DB error");
- req.db.getStatusPageByUrl.throws(err);
-
- try {
- await getStatusPageByUrl(req, res, next);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
-
- it("should return a status page", async function () {
- const statusPage = {
- _id: "123456",
- companyName: "Test Company",
- url: "123456",
- };
- req.db.getStatusPageByUrl.resolves(statusPage);
- const result = await getStatusPageByUrl(req, res, next);
- expect(res.status.firstCall.args[0]).to.equal(200);
- expect(res.json.firstCall.args[0].success).to.be.true;
- expect(res.json.firstCall.args[0].data).to.deep.equal(statusPage);
- });
- });
-});
diff --git a/server/tests/db/checkModule.test.js b/server/tests/db/checkModule.test.js
deleted file mode 100755
index 6eef12445..000000000
--- a/server/tests/db/checkModule.test.js
+++ /dev/null
@@ -1,589 +0,0 @@
-import sinon from "sinon";
-import {
- createCheck,
- getChecksCount,
- getChecks,
- getTeamChecks,
- deleteChecks,
- deleteChecksByTeamId,
- updateChecksTTL,
-} from "../../db/mongo/modules/checkModule.js";
-import Check from "../../db/models/Check.js";
-import Monitor from "../../db/models/Monitor.js";
-import User from "../../db/models/User.js";
-import logger from "../../utils/logger.js";
-
-describe("checkModule", function () {
- describe("createCheck", function () {
- let checkCountDocumentsStub, checkSaveStub, monitorFindByIdStub, monitorSaveStub;
- const mockMonitor = {
- _id: "123",
- uptimePercentage: 0.5,
- status: true,
- save: () => this,
- };
- const mockCheck = { active: true };
-
- beforeEach(function () {
- checkSaveStub = sinon.stub(Check.prototype, "save");
- checkCountDocumentsStub = sinon.stub(Check, "countDocuments");
- monitorFindByIdStub = sinon.stub(Monitor, "findById");
- monitorSaveStub = sinon.stub(Monitor.prototype, "save");
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should return undefined early if no monitor is found", async function () {
- monitorFindByIdStub.returns(null);
- const check = await createCheck({ monitorId: "123" });
- expect(check).to.be.undefined;
- });
-
- it("should return a check", async function () {
- monitorFindByIdStub.returns(mockMonitor);
- checkSaveStub.returns(mockCheck);
- monitorSaveStub.returns(mockMonitor);
- const check = await createCheck({ monitorId: "123", status: true });
- expect(check).to.deep.equal(mockCheck);
- });
-
- it("should return a check if status is down", async function () {
- mockMonitor.status = false;
- monitorFindByIdStub.returns(mockMonitor);
- checkSaveStub.returns(mockCheck);
- monitorSaveStub.returns(mockMonitor);
- const check = await createCheck({ monitorId: "123", status: false });
- expect(check).to.deep.equal(mockCheck);
- });
-
- it("should return a check if uptimePercentage is undefined", async function () {
- mockMonitor.uptimePercentage = undefined;
- monitorFindByIdStub.returns(mockMonitor);
- checkSaveStub.returns(mockCheck);
- monitorSaveStub.returns(mockMonitor);
- const check = await createCheck({ monitorId: "123", status: true });
- expect(check).to.deep.equal(mockCheck);
- });
-
- it("should return a check if uptimePercentage is undefined and status is down", async function () {
- mockMonitor.uptimePercentage = undefined;
- monitorFindByIdStub.returns(mockMonitor);
- checkSaveStub.returns(mockCheck);
- monitorSaveStub.returns(mockMonitor);
- const check = await createCheck({ monitorId: "123", status: false });
- expect(check).to.deep.equal(mockCheck);
- });
-
- it("should monitor save error", async function () {
- const err = new Error("Save Error");
- monitorSaveStub.throws(err);
- try {
- await createCheck({ monitorId: "123" });
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
-
- it("should handle errors", async function () {
- const err = new Error("DB Error");
- checkCountDocumentsStub.throws(err);
- try {
- await createCheck({ monitorId: "123" });
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("getChecksCount", function () {
- let checkCountDocumentStub;
-
- beforeEach(function () {
- checkCountDocumentStub = sinon.stub(Check, "countDocuments");
- });
-
- afterEach(function () {
- checkCountDocumentStub.restore();
- });
-
- it("should return count with basic monitorId query", async function () {
- const req = {
- params: { monitorId: "test123" },
- query: {},
- };
- checkCountDocumentStub.resolves(5);
-
- const result = await getChecksCount(req);
-
- expect(result).to.equal(5);
- expect(checkCountDocumentStub.calledOnce).to.be.true;
- expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({
- monitorId: "test123",
- });
- });
-
- it("should include dateRange in query when provided", async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { dateRange: "day" },
- };
- checkCountDocumentStub.resolves(3);
-
- const result = await getChecksCount(req);
-
- expect(result).to.equal(3);
- expect(checkCountDocumentStub.firstCall.args[0]).to.have.property("createdAt");
- expect(checkCountDocumentStub.firstCall.args[0].createdAt).to.have.property("$gte");
- });
-
- it('should handle "all" filter correctly', async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { filter: "all" },
- };
- checkCountDocumentStub.resolves(2);
-
- const result = await getChecksCount(req);
-
- expect(result).to.equal(2);
- expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({
- monitorId: "test123",
- status: false,
- });
- });
-
- it('should handle "down" filter correctly', async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { filter: "down" },
- };
- checkCountDocumentStub.resolves(2);
-
- const result = await getChecksCount(req);
-
- expect(result).to.equal(2);
- expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({
- monitorId: "test123",
- status: false,
- });
- });
-
- it('should handle "resolve" filter correctly', async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { filter: "resolve" },
- };
- checkCountDocumentStub.resolves(1);
-
- const result = await getChecksCount(req);
-
- expect(result).to.equal(1);
- expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({
- monitorId: "test123",
- status: false,
- statusCode: 5000,
- });
- });
-
- it("should handle unknown filter correctly", async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { filter: "unknown" },
- };
- checkCountDocumentStub.resolves(1);
-
- const result = await getChecksCount(req);
-
- expect(result).to.equal(1);
- expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({
- monitorId: "test123",
- status: false,
- });
- });
-
- it("should combine dateRange and filter in query", async function () {
- const req = {
- params: { monitorId: "test123" },
- query: {
- dateRange: "week",
- filter: "down",
- },
- };
- checkCountDocumentStub.resolves(4);
-
- const result = await getChecksCount(req);
-
- expect(result).to.equal(4);
- expect(checkCountDocumentStub.firstCall.args[0]).to.have.all.keys("monitorId", "createdAt", "status");
- });
- });
-
- describe("getChecks", function () {
- let checkFindStub, monitorFindStub;
-
- beforeEach(function () {
- checkFindStub = sinon.stub(Check, "find").returns({
- skip: sinon.stub().returns({
- limit: sinon.stub().returns({
- sort: sinon.stub().returns([{ id: 1 }, { id: 2 }]),
- }),
- }),
- });
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should return checks with basic monitorId query", async function () {
- const req = {
- params: { monitorId: "test123" },
- query: {},
- };
-
- const result = await getChecks(req);
-
- expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
- });
-
- it("should return checks with limit query", async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { limit: 10 },
- };
-
- const result = await getChecks(req);
-
- expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
- });
-
- it("should handle pagination correctly", async function () {
- const req = {
- params: { monitorId: "test123" },
- query: {
- page: 2,
- rowsPerPage: 10,
- },
- };
-
- const result = await getChecks(req);
-
- expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
- });
-
- it("should handle dateRange filter", async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { dateRange: "week" },
- };
- const result = await getChecks(req);
-
- expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
- });
-
- it('should handle "all" filter', async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { filter: "all" },
- };
-
- await getChecks(req);
- const result = await getChecks(req);
- expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
- });
-
- it('should handle "down" filter', async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { filter: "down" },
- };
-
- await getChecks(req);
- const result = await getChecks(req);
- expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
- });
-
- it('should handle "resolve" filter', async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { filter: "resolve" },
- };
-
- await getChecks(req);
- const result = await getChecks(req);
- expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
- });
-
- it('should handle "unknown" filter', async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { filter: "unknown" },
- };
-
- await getChecks(req);
- const result = await getChecks(req);
- expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
- });
-
- it("should handle ascending sort order", async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { sortOrder: "asc" },
- };
-
- await getChecks(req);
- const result = await getChecks(req);
- expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
- });
-
- it("should handle error case", async function () {
- const req = {
- params: { monitorId: "test123" },
- query: {},
- };
-
- checkFindStub.throws(new Error("Database error"));
-
- try {
- await getChecks(req);
- } catch (error) {
- expect(error.message).to.equal("Database error");
- expect(error.service).to.equal("checkModule");
- expect(error.method).to.equal("getChecks");
- }
- });
- });
-
- describe("getTeamChecks", function () {
- let checkFindStub, checkCountDocumentsStub, monitorFindStub;
- const mockMonitors = [{ _id: "123" }];
-
- beforeEach(function () {
- monitorFindStub = sinon.stub(Monitor, "find").returns({
- select: sinon.stub().returns(mockMonitors),
- });
- checkCountDocumentsStub = sinon.stub(Check, "countDocuments").returns(2);
- checkFindStub = sinon.stub(Check, "find").returns({
- skip: sinon.stub().returns({
- limit: sinon.stub().returns({
- sort: sinon.stub().returns({
- select: sinon.stub().returns([{ id: 1 }, { id: 2 }]),
- }),
- }),
- }),
- });
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should return checks with basic monitorId query", async function () {
- const req = {
- params: { teamId: "test123" },
- query: {},
- };
-
- const result = await getTeamChecks(req);
- expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
- });
-
- it("should handle pagination correctly", async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { limit: 1, page: 2, rowsPerPage: 10 },
- };
-
- const result = await getTeamChecks(req);
- expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
- });
-
- it("should handle dateRange filter", async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { dateRange: "week" },
- };
- const result = await getTeamChecks(req);
- expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
- });
-
- it('should handle "all" filter', async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { filter: "all" },
- };
-
- await getChecks(req);
- const result = await getTeamChecks(req);
- expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
- });
-
- it('should handle "down" filter', async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { filter: "down" },
- };
-
- await getChecks(req);
- const result = await getTeamChecks(req);
- expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
- });
-
- it('should handle "resolve" filter', async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { filter: "resolve" },
- };
-
- await getChecks(req);
- const result = await getTeamChecks(req);
- expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
- });
-
- it('should handle "unknown" filter', async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { filter: "unknown" },
- };
-
- await getChecks(req);
- const result = await getTeamChecks(req);
- expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
- });
-
- it("should handle ascending sort order", async function () {
- const req = {
- params: { monitorId: "test123" },
- query: { sortOrder: "asc" },
- };
-
- await getChecks(req);
- const result = await getTeamChecks(req);
- expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
- });
-
- it("should handle error case", async function () {
- const req = {
- params: { monitorId: "test123" },
- query: {},
- };
-
- checkFindStub.throws(new Error("Database error"));
-
- try {
- await getTeamChecks(req);
- } catch (error) {
- expect(error.message).to.equal("Database error");
- expect(error.service).to.equal("checkModule");
- expect(error.method).to.equal("getTeamChecks");
- }
- });
- });
-
- describe("deleteChecks", function () {
- let checkDeleteManyStub;
-
- beforeEach(function () {
- checkDeleteManyStub = sinon.stub(Check, "deleteMany").resolves({ deletedCount: 1 });
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should return a value if a check is deleted", async function () {
- const result = await deleteChecks("123");
- expect(result).to.equal(1);
- });
-
- it("should handle an error", async function () {
- checkDeleteManyStub.throws(new Error("Database error"));
- try {
- await deleteChecks("123");
- } catch (error) {
- expect(error.message).to.equal("Database error");
- expect(error.method).to.equal("deleteChecks");
- }
- });
- });
-
- describe("deleteChecksByTeamId", function () {
- let mockMonitors = [{ _id: 123, save: () => this }];
- let monitorFindStub, monitorSaveStub, checkDeleteManyStub;
-
- beforeEach(function () {
- monitorSaveStub = sinon.stub(Monitor.prototype, "save");
- monitorFindStub = sinon.stub(Monitor, "find").returns(mockMonitors);
- checkDeleteManyStub = sinon.stub(Check, "deleteMany").resolves({ deletedCount: 1 });
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should return a deleted count", async function () {
- const result = await deleteChecksByTeamId("123");
- expect(result).to.equal(1);
- });
-
- it("should handle errors", async function () {
- const err = new Error("DB Error");
- monitorFindStub.throws(err);
- try {
- const result = await deleteChecksByTeamId("123");
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("updateChecksTTL", function () {
- let userUpdateManyStub;
- let loggerStub;
-
- beforeEach(function () {
- loggerStub = sinon.stub(logger, "error");
- userUpdateManyStub = sinon.stub(User, "updateMany");
- Check.collection = { dropIndex: sinon.stub(), createIndex: sinon.stub() };
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should return undefined", async function () {
- const result = await updateChecksTTL("123", 10);
- expect(result).to.be.undefined;
- });
-
- it("should log an error if dropIndex throws an error", async function () {
- const err = new Error("Drop Index Error");
- Check.collection.dropIndex.throws(err);
- await updateChecksTTL("123", 10);
- expect(loggerStub.calledOnce).to.be.true;
- expect(loggerStub.firstCall.args[0].message).to.equal(err.message);
- });
-
- it("should throw an error if createIndex throws an error", async function () {
- const err = new Error("Create Index Error");
- Check.collection.createIndex.throws(err);
- try {
- await updateChecksTTL("123", 10);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
-
- it("should throw an error if User.updateMany throws an error", async function () {
- const err = new Error("Update Many Error");
- userUpdateManyStub.throws(err);
- try {
- await updateChecksTTL("123", 10);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-});
diff --git a/server/tests/db/hardwareCheckModule.test.js b/server/tests/db/hardwareCheckModule.test.js
deleted file mode 100755
index 20ce197fb..000000000
--- a/server/tests/db/hardwareCheckModule.test.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import sinon from "sinon";
-import HardwareCheck from "../../db/models/HardwareCheck.js";
-import { createHardwareCheck } from "../../db/mongo/modules/hardwareCheckModule.js";
-import Monitor from "../../db/models/Monitor.js";
-import logger from "../../utils/logger.js";
-
-const mockHardwareCheck = {
- data: {
- cpu: {
- physical_core: 4,
- logical_core: 8,
- frequency: 4800,
- current_frequency: 1411,
- temperature: [45, 50, 46, 47, 45, 50, 46, 47],
- free_percent: 0.8552990910595134,
- usage_percent: 0.14470090894048657,
- },
- memory: {
- total_bytes: 16467628032,
- available_bytes: 7895044096,
- used_bytes: 6599561216,
- usage_percent: 0.4008,
- },
- disk: [
- {
- read_speed_bytes: null,
- write_speed_bytes: null,
- total_bytes: 931258499072,
- free_bytes: 737097256960,
- usage_percent: 0.1661,
- },
- ],
- host: {
- os: "linux",
- platform: "ubuntu",
- kernel_version: "6.8.0-48-generic",
- },
- },
- errors: [
- {
- metric: ["cpu.temperature"],
- err: "unable to read CPU temperature",
- },
- ],
-};
-
-const mockMonitor = {
- _id: "123",
- uptimePercentage: 1,
- status: true,
- save: () => this,
-};
-
-describe("HardwareCheckModule", function () {
- let hardwareCheckSaveStub, hardwareCheckCountDocumentsStub, monitorFindByIdStub, loggerStub;
-
- beforeEach(function () {
- loggerStub = sinon.stub(logger, "error");
- hardwareCheckSaveStub = sinon.stub(HardwareCheck.prototype, "save");
- monitorFindByIdStub = sinon.stub(Monitor, "findById");
- hardwareCheckCountDocumentsStub = sinon.stub(HardwareCheck, "countDocuments");
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("createHardwareCheck", function () {
- it("should return a hardware check", async function () {
- hardwareCheckSaveStub.resolves(mockHardwareCheck);
- monitorFindByIdStub.resolves(mockMonitor);
- hardwareCheckCountDocumentsStub.resolves(1);
- const hardwareCheck = await createHardwareCheck({ status: true });
- expect(hardwareCheck).to.exist;
- expect(hardwareCheck).to.deep.equal(mockHardwareCheck);
- });
-
- it("should return a hardware check for a check with status false", async function () {
- hardwareCheckSaveStub.resolves(mockHardwareCheck);
- monitorFindByIdStub.resolves(mockMonitor);
- hardwareCheckCountDocumentsStub.resolves(1);
- const hardwareCheck = await createHardwareCheck({ status: false });
- expect(hardwareCheck).to.exist;
- expect(hardwareCheck).to.deep.equal(mockHardwareCheck);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- monitorFindByIdStub.resolves(mockMonitor);
- hardwareCheckSaveStub.rejects(err);
- try {
- await createHardwareCheck({});
- } catch (error) {
- expect(error).to.exist;
- expect(error).to.deep.equal(err);
- }
- });
-
- it("should log an error if a monitor is not found", async function () {
- monitorFindByIdStub.resolves(null);
- const res = await createHardwareCheck({});
- expect(loggerStub.calledOnce).to.be.true;
- expect(res).to.be.null;
- });
-
- it("should handle a monitor with undefined uptimePercentage", async function () {
- monitorFindByIdStub.resolves({ ...mockMonitor, uptimePercentage: undefined });
- hardwareCheckSaveStub.resolves(mockHardwareCheck);
- hardwareCheckCountDocumentsStub.resolves(1);
- const res = await createHardwareCheck({});
- expect(res).to.exist;
- });
-
- it("should handle a monitor with undefined uptimePercentage and true status", async function () {
- monitorFindByIdStub.resolves({
- ...mockMonitor,
- uptimePercentage: undefined,
- });
- hardwareCheckSaveStub.resolves(mockHardwareCheck);
- hardwareCheckCountDocumentsStub.resolves(1);
- const res = await createHardwareCheck({ status: true });
- expect(res).to.exist;
- });
-
- it("should handle a monitor with undefined uptimePercentage and false status", async function () {
- monitorFindByIdStub.resolves({
- ...mockMonitor,
- uptimePercentage: undefined,
- });
- hardwareCheckSaveStub.resolves(mockHardwareCheck);
- hardwareCheckCountDocumentsStub.resolves(1);
- const res = await createHardwareCheck({ status: false });
- expect(res).to.exist;
- });
- });
-});
diff --git a/server/tests/db/inviteModule.test.js b/server/tests/db/inviteModule.test.js
deleted file mode 100755
index e8913c983..000000000
--- a/server/tests/db/inviteModule.test.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import sinon from "sinon";
-import InviteToken from "../../db/models/InviteToken.js";
-import { requestInviteToken, getInviteToken, getInviteTokenAndDelete } from "../../db/mongo/modules/inviteModule.js";
-import { errorMessages } from "../../utils/messages.js";
-
-describe("Invite Module", function () {
- const mockUserData = {
- email: "test@test.com",
- teamId: "123",
- role: ["admin"],
- token: "123",
- };
- const mockInviteToken = { _id: 123, time: 123 };
- let inviteTokenDeleteManyStub, inviteTokenSaveStub, inviteTokenFindOneStub, inviteTokenFindOneAndDeleteStub;
-
- beforeEach(function () {
- inviteTokenDeleteManyStub = sinon.stub(InviteToken, "deleteMany");
- inviteTokenSaveStub = sinon.stub(InviteToken.prototype, "save");
- inviteTokenFindOneStub = sinon.stub(InviteToken, "findOne");
- inviteTokenFindOneAndDeleteStub = sinon.stub(InviteToken, "findOneAndDelete");
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("requestInviteToken", function () {
- it("should return a new invite token", async function () {
- inviteTokenDeleteManyStub.resolves();
- inviteTokenSaveStub.resolves();
- const inviteToken = await requestInviteToken(mockUserData);
- expect(inviteToken.email).to.equal(mockUserData.email);
- expect(inviteToken.role).to.deep.equal(mockUserData.role);
- expect(inviteToken.token).to.exist;
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- inviteTokenDeleteManyStub.rejects(err);
- try {
- await requestInviteToken(mockUserData);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("getInviteToken", function () {
- it("should return an invite token", async function () {
- inviteTokenFindOneStub.resolves(mockInviteToken);
- const inviteToken = await getInviteToken(mockUserData.token);
- expect(inviteToken).to.deep.equal(mockInviteToken);
- });
-
- it("should handle a token not found", async function () {
- inviteTokenFindOneStub.resolves(null);
- try {
- await getInviteToken(mockUserData.token);
- } catch (error) {
- expect(error.message).to.equal(errorMessages.AUTH_INVITE_NOT_FOUND);
- }
- });
-
- it("should handle DB errors", async function () {
- const err = new Error("test error");
- inviteTokenFindOneStub.rejects(err);
- try {
- await getInviteToken(mockUserData.token);
- } catch (error) {
- expect(error).to.deep.equal(err);
- expect(error.method).to.equal("getInviteToken");
- }
- });
- });
-
- describe("getInviteTokenAndDelete", function () {
- it("should return a deleted invite", async function () {
- inviteTokenFindOneAndDeleteStub.resolves(mockInviteToken);
- const deletedInvite = await getInviteTokenAndDelete(mockUserData.token);
- expect(deletedInvite).to.deep.equal(mockInviteToken);
- });
-
- it("should handle a token not found", async function () {
- inviteTokenFindOneAndDeleteStub.resolves(null);
- try {
- await getInviteTokenAndDelete(mockUserData.token);
- } catch (error) {
- expect(error.message).to.equal(errorMessages.AUTH_INVITE_NOT_FOUND);
- }
- });
-
- it("should handle DB errors", async function () {
- const err = new Error("test error");
- inviteTokenFindOneAndDeleteStub.rejects(err);
- try {
- await getInviteTokenAndDelete(mockUserData.token);
- } catch (error) {
- expect(error).to.deep.equal(err);
- expect(error.method).to.equal("getInviteTokenAndDelete");
- }
- });
- });
-});
diff --git a/server/tests/db/maintenanceWindowModule.test.js b/server/tests/db/maintenanceWindowModule.test.js
deleted file mode 100755
index 576c97f84..000000000
--- a/server/tests/db/maintenanceWindowModule.test.js
+++ /dev/null
@@ -1,255 +0,0 @@
-import sinon from "sinon";
-import MaintenanceWindow from "../../db/models/MaintenanceWindow.js";
-import {
- createMaintenanceWindow,
- getMaintenanceWindowById,
- getMaintenanceWindowsByTeamId,
- getMaintenanceWindowsByMonitorId,
- deleteMaintenanceWindowById,
- deleteMaintenanceWindowByMonitorId,
- deleteMaintenanceWindowByUserId,
- editMaintenanceWindowById,
-} from "../../db/mongo/modules/maintenanceWindowModule.js";
-
-describe("MaintenanceWindow Module", function () {
- const mockMaintenanceWindow = {
- monitorId: "123",
- active: true,
- oneTime: true,
- start: 1,
- end: 20000,
- };
-
- let mockMaintenanceWindows = [mockMaintenanceWindow];
- let maintenanceWindowSaveStub,
- maintenanceWindowFindByIdStub,
- maintenanceWindowCountDocumentsStub,
- maintenanceWindowFindStub,
- maintenanceWindowFindByIdAndDeleteStub,
- maintenanceWindowDeleteManyStub,
- maintenanceWindowFindByIdAndUpdateStub;
-
- beforeEach(function () {
- maintenanceWindowSaveStub = sinon.stub(MaintenanceWindow.prototype, "save");
- maintenanceWindowFindByIdStub = sinon.stub(MaintenanceWindow, "findById");
- maintenanceWindowCountDocumentsStub = sinon.stub(MaintenanceWindow, "countDocuments");
- maintenanceWindowFindStub = sinon.stub(MaintenanceWindow, "find").returns({
- skip: sinon.stub().returns({
- limit: sinon.stub().returns({
- sort: sinon.stub().returns(mockMaintenanceWindows),
- }),
- }),
- });
- maintenanceWindowFindByIdAndDeleteStub = sinon.stub(MaintenanceWindow, "findByIdAndDelete");
- maintenanceWindowDeleteManyStub = sinon.stub(MaintenanceWindow, "deleteMany");
- maintenanceWindowFindByIdAndUpdateStub = sinon.stub(MaintenanceWindow, "findByIdAndUpdate");
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("createMaintenanceWindow", function () {
- it("should save a new maintenance window", async function () {
- maintenanceWindowSaveStub.resolves(mockMaintenanceWindow);
- const result = await createMaintenanceWindow(mockMaintenanceWindow);
- expect(result).to.deep.equal(mockMaintenanceWindow);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- maintenanceWindowSaveStub.rejects(err);
- try {
- await createMaintenanceWindow(mockMaintenanceWindow);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("getMaintenanceWindowById", function () {
- it("should return a maintenance window", async function () {
- maintenanceWindowFindByIdStub.resolves(mockMaintenanceWindow);
- const result = await getMaintenanceWindowById(mockMaintenanceWindow.id);
- expect(result).to.deep.equal(mockMaintenanceWindow);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- maintenanceWindowFindByIdStub.rejects(err);
- try {
- await getMaintenanceWindowById(mockMaintenanceWindow.id);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("getMaintenanceWindowsByTeamId", function () {
- let query;
-
- beforeEach(function () {
- query = {
- active: true,
- page: 1,
- rowsPerPage: 10,
- field: "name",
- order: "asc",
- };
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should return a list of maintenance windows and count", async function () {
- maintenanceWindowCountDocumentsStub.resolves(1);
- const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query);
- expect(result).to.deep.equal({
- maintenanceWindows: mockMaintenanceWindows,
- maintenanceWindowCount: 1,
- });
- });
-
- it("should return a list of maintenance windows and count with empty query", async function () {
- query = undefined;
- maintenanceWindowCountDocumentsStub.resolves(1);
- const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query);
- expect(result).to.deep.equal({
- maintenanceWindows: mockMaintenanceWindows,
- maintenanceWindowCount: 1,
- });
- });
-
- it("should return a list of maintenance windows and count with no pagination provided", async function () {
- query.page = undefined;
- query.rowsPerPage = undefined;
- maintenanceWindowCountDocumentsStub.resolves(1);
- const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query);
- expect(result).to.deep.equal({
- maintenanceWindows: mockMaintenanceWindows,
- maintenanceWindowCount: 1,
- });
- });
-
- it("should return a list of maintenance windows and count with field and desc order", async function () {
- query.order = "desc";
- maintenanceWindowCountDocumentsStub.resolves(1);
- const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query);
- expect(result).to.deep.equal({
- maintenanceWindows: mockMaintenanceWindows,
- maintenanceWindowCount: 1,
- });
- });
-
- it("should return a list of maintenance windows and count no field", async function () {
- query.field = undefined;
- maintenanceWindowCountDocumentsStub.resolves(1);
- const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query);
- expect(result).to.deep.equal({
- maintenanceWindows: mockMaintenanceWindows,
- maintenanceWindowCount: 1,
- });
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- maintenanceWindowCountDocumentsStub.rejects(err);
- try {
- await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("getMaintenanceWindowsByMonitorId", function () {
- it("should return a list of maintenance windows", async function () {
- maintenanceWindowFindStub.resolves(mockMaintenanceWindows);
- const result = await getMaintenanceWindowsByMonitorId(mockMaintenanceWindow.monitorId);
- expect(result).to.deep.equal(mockMaintenanceWindows);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- maintenanceWindowFindStub.rejects(err);
- try {
- await getMaintenanceWindowsByMonitorId(mockMaintenanceWindow.monitorId);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("deleteMaintenanceWindowById", function () {
- it("should delete a maintenance window", async function () {
- maintenanceWindowFindByIdAndDeleteStub.resolves(mockMaintenanceWindow);
- const result = await deleteMaintenanceWindowById(mockMaintenanceWindow.id);
- expect(result).to.deep.equal(mockMaintenanceWindow);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- maintenanceWindowFindByIdAndDeleteStub.rejects(err);
- try {
- await deleteMaintenanceWindowById(mockMaintenanceWindow.id);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("deleteMaintenanceWindowByMonitorId", function () {
- it("should return the number of documents deleted", async function () {
- maintenanceWindowDeleteManyStub.resolves({ deletedCount: 1 });
- const result = await deleteMaintenanceWindowByMonitorId(mockMaintenanceWindow.monitorId);
- expect(result).to.deep.equal({ deletedCount: 1 });
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- maintenanceWindowDeleteManyStub.rejects(err);
- try {
- await deleteMaintenanceWindowByMonitorId(mockMaintenanceWindow.monitorId);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("deleteMaintenanceWindowByUserId", function () {
- it("should return the number of documents deleted", async function () {
- maintenanceWindowDeleteManyStub.resolves({ deletedCount: 1 });
- const result = await deleteMaintenanceWindowByUserId(mockMaintenanceWindow.userId);
- expect(result).to.deep.equal({ deletedCount: 1 });
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- maintenanceWindowDeleteManyStub.rejects(err);
- try {
- await deleteMaintenanceWindowByUserId(mockMaintenanceWindow.userId);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("editMaintenanceWindowById", function () {
- it("should return the updated maintenance window", async function () {
- maintenanceWindowFindByIdAndUpdateStub.resolves(mockMaintenanceWindow);
- const result = await editMaintenanceWindowById(mockMaintenanceWindow.id, mockMaintenanceWindow);
- expect(result).to.deep.equal(mockMaintenanceWindow);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- maintenanceWindowFindByIdAndUpdateStub.rejects(err);
- try {
- await editMaintenanceWindowById(mockMaintenanceWindow.id, mockMaintenanceWindow);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-});
diff --git a/server/tests/db/monitorModule.test.js b/server/tests/db/monitorModule.test.js
deleted file mode 100755
index e59c352cf..000000000
--- a/server/tests/db/monitorModule.test.js
+++ /dev/null
@@ -1,1865 +0,0 @@
-import sinon from "sinon";
-import Monitor from "../../db/models/Monitor.js";
-import Check from "../../db/models/Check.js";
-import PageSpeedCheck from "../../db/models/PageSpeedCheck.js";
-import HardwareCheck from "../../db/models/HardwareCheck.js";
-import Notification from "../../db/models/Notification.js";
-
-import { errorMessages } from "../../utils/messages.js";
-import {
- getAllMonitors,
- getAllMonitorsWithUptimeStats,
- getMonitorStatsById,
- getMonitorById,
- getMonitorsAndSummaryByTeamId,
- getMonitorsByTeamId,
- createMonitor,
- deleteMonitor,
- deleteAllMonitors,
- deleteMonitorsByUserId,
- editMonitor,
- addDemoMonitors,
- calculateUptimeDuration,
- getLastChecked,
- getLatestResponseTime,
- getAverageResponseTime,
- getUptimePercentage,
- getIncidents,
- getMonitorChecks,
- processChecksForDisplay,
- groupChecksByTime,
- calculateGroupStats,
-} from "../../db/mongo/modules/monitorModule.js";
-
-describe("monitorModule", function () {
- let monitorFindStub,
- monitorFindByIdStub,
- monitorFindByIdAndUpdateStub,
- monitorFindByIdAndDeleteStub,
- monitorDeleteManyStub,
- monitorCountStub,
- monitorInsertManyStub,
- checkFindStub,
- pageSpeedCheckFindStub,
- hardwareCheckFindStub;
-
- beforeEach(function () {
- monitorFindStub = sinon.stub(Monitor, "find");
- monitorFindByIdStub = sinon.stub(Monitor, "findById");
- monitorFindByIdAndUpdateStub = sinon.stub(Monitor, "findByIdAndUpdate");
- monitorFindByIdAndDeleteStub = sinon.stub(Monitor, "findByIdAndDelete");
- monitorDeleteManyStub = sinon.stub(Monitor, "deleteMany");
- monitorCountStub = sinon.stub(Monitor, "countDocuments");
-
- monitorInsertManyStub = sinon.stub(Monitor, "insertMany");
- checkFindStub = sinon.stub(Check, "find").returns({
- sort: sinon.stub(),
- });
- pageSpeedCheckFindStub = sinon.stub(PageSpeedCheck, "find").returns({
- sort: sinon.stub(),
- });
- hardwareCheckFindStub = sinon.stub(HardwareCheck, "find").returns({
- sort: sinon.stub(),
- });
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("getAllMonitors", function () {
- it("should return all monitors", async function () {
- const mockMonitors = [
- { _id: "1", name: "Monitor 1", url: "test1.com" },
- { _id: "2", name: "Monitor 2", url: "test2.com" },
- ];
- monitorFindStub.returns(mockMonitors);
- const result = await getAllMonitors();
-
- expect(result).to.deep.equal(mockMonitors);
- expect(monitorFindStub.calledOnce).to.be.true;
- expect(monitorFindStub.firstCall.args).to.deep.equal([]);
- });
-
- it("should handle empty results", async function () {
- monitorFindStub.returns([]);
- const result = await getAllMonitors();
- expect(result).to.be.an("array").that.is.empty;
- });
-
- it("should throw error when database fails", async function () {
- // Arrange
- const error = new Error("Database error");
- error.service = "MonitorModule";
- error.method = "getAllMonitors";
- monitorFindStub.rejects(error);
- // Act & Assert
- try {
- await getAllMonitors();
- expect.fail("Should have thrown an error");
- } catch (err) {
- expect(err).to.equal(error);
- expect(err.service).to.equal("monitorModule");
- expect(err.method).to.equal("getAllMonitors");
- }
- });
- });
-
- describe("getAllMonitorsWithUptimeStats", function () {
- it("should return monitors with uptime stats for different time periods", async function () {
- // Mock data
- const mockMonitors = [
- {
- _id: "monitor1",
- type: "http",
- toObject: () => ({
- _id: "monitor1",
- type: "http",
- name: "Test Monitor",
- }),
- },
- ];
-
- const mockChecks = [{ status: true }, { status: true }, { status: false }, { status: true }];
-
- monitorFindStub.resolves(mockMonitors);
- checkFindStub.resolves(mockChecks);
-
- const result = await getAllMonitorsWithUptimeStats();
-
- expect(result).to.be.an("array");
- expect(result).to.have.lengthOf(1);
-
- const monitor = result[0];
- expect(monitor).to.have.property("_id", "monitor1");
- expect(monitor).to.have.property("name", "Test Monitor");
-
- // Check uptime percentages exist for all time periods
- expect(monitor).to.have.property("1");
- expect(monitor).to.have.property("7");
- expect(monitor).to.have.property("30");
- expect(monitor).to.have.property("90");
-
- // Verify uptime percentage calculation (3 successful out of 4 = 75%)
- expect(monitor["1"]).to.equal(75);
- expect(monitor["7"]).to.equal(75);
- expect(monitor["30"]).to.equal(75);
- expect(monitor["90"]).to.equal(75);
- });
-
- it("should return monitors with stats for pagespeed type", async function () {
- // Mock data
- const mockMonitors = [
- {
- _id: "monitor1",
- type: "pagespeed",
- toObject: () => ({
- _id: "monitor1",
- type: "pagespeed",
- name: "Test Monitor",
- }),
- },
- ];
-
- const mockChecks = [{ status: true }, { status: true }, { status: false }, { status: true }];
-
- monitorFindStub.resolves(mockMonitors);
- pageSpeedCheckFindStub.resolves(mockChecks);
-
- const result = await getAllMonitorsWithUptimeStats();
-
- expect(result).to.be.an("array");
- expect(result).to.have.lengthOf(1);
-
- const monitor = result[0];
- expect(monitor).to.have.property("_id", "monitor1");
- expect(monitor).to.have.property("name", "Test Monitor");
-
- // Check uptime percentages exist for all time periods
- expect(monitor).to.have.property("1");
- expect(monitor).to.have.property("7");
- expect(monitor).to.have.property("30");
- expect(monitor).to.have.property("90");
-
- // Verify uptime percentage calculation (3 successful out of 4 = 75%)
- expect(monitor["1"]).to.equal(75);
- expect(monitor["7"]).to.equal(75);
- expect(monitor["30"]).to.equal(75);
- expect(monitor["90"]).to.equal(75);
- });
-
- it("should return monitors with stats for hardware type", async function () {
- // Mock data
- const mockMonitors = [
- {
- _id: "monitor1",
- type: "hardware",
- toObject: () => ({
- _id: "monitor1",
- type: "hardware",
- name: "Test Monitor",
- }),
- },
- ];
-
- const mockChecks = [{ status: true }, { status: true }, { status: false }, { status: true }];
-
- monitorFindStub.resolves(mockMonitors);
- hardwareCheckFindStub.resolves(mockChecks);
-
- const result = await getAllMonitorsWithUptimeStats();
-
- expect(result).to.be.an("array");
- expect(result).to.have.lengthOf(1);
-
- const monitor = result[0];
- expect(monitor).to.have.property("_id", "monitor1");
- expect(monitor).to.have.property("name", "Test Monitor");
-
- // Check uptime percentages exist for all time periods
- expect(monitor).to.have.property("1");
- expect(monitor).to.have.property("7");
- expect(monitor).to.have.property("30");
- expect(monitor).to.have.property("90");
-
- // Verify uptime percentage calculation (3 successful out of 4 = 75%)
- expect(monitor["1"]).to.equal(75);
- expect(monitor["7"]).to.equal(75);
- expect(monitor["30"]).to.equal(75);
- expect(monitor["90"]).to.equal(75);
- });
-
- it("should handle errors appropriately", async function () {
- // Setup stub to throw error
- monitorFindStub.rejects(new Error("Database error"));
-
- try {
- await getAllMonitorsWithUptimeStats();
- } catch (error) {
- expect(error).to.be.an("error");
- expect(error.message).to.equal("Database error");
- expect(error.service).to.equal("monitorModule");
- expect(error.method).to.equal("getAllMonitorsWithUptimeStats");
- }
- });
-
- it("should handle empty monitor list", async function () {
- monitorFindStub.resolves([]);
-
- const result = await getAllMonitorsWithUptimeStats();
-
- expect(result).to.be.an("array");
- expect(result).to.have.lengthOf(0);
- });
-
- it("should handle monitor with no checks", async function () {
- const mockMonitors = [
- {
- _id: "monitor1",
- type: "http",
- toObject: () => ({
- _id: "monitor1",
- type: "http",
- name: "Test Monitor",
- }),
- },
- ];
-
- monitorFindStub.resolves(mockMonitors);
- checkFindStub.resolves([]);
-
- const result = await getAllMonitorsWithUptimeStats();
-
- expect(result[0]).to.have.property("1", 0);
- expect(result[0]).to.have.property("7", 0);
- expect(result[0]).to.have.property("30", 0);
- expect(result[0]).to.have.property("90", 0);
- });
- });
-
- describe("calculateUptimeDuration", function () {
- let clock;
- const NOW = new Date("2024-01-01T12:00:00Z").getTime();
-
- beforeEach(function () {
- // Fix the current time
- clock = sinon.useFakeTimers(NOW);
- });
-
- afterEach(function () {
- clock.restore();
- });
-
- it("should return 0 when checks array is empty", function () {
- expect(calculateUptimeDuration([])).to.equal(0);
- });
-
- it("should return 0 when checks array is null", function () {
- expect(calculateUptimeDuration(null)).to.equal(0);
- });
-
- it("should calculate uptime from last down check to most recent check", function () {
- const checks = [
- { status: true, createdAt: "2024-01-01T11:00:00Z" }, // Most recent
- { status: true, createdAt: "2024-01-01T10:00:00Z" },
- { status: false, createdAt: "2024-01-01T09:00:00Z" }, // Last down
- { status: true, createdAt: "2024-01-01T08:00:00Z" },
- ];
-
- // Expected: 2 hours (from 09:00 to 11:00) = 7200000ms
- expect(calculateUptimeDuration(checks)).to.equal(7200000);
- });
-
- it("should calculate uptime from first check when no down checks exist", function () {
- const checks = [
- { status: true, createdAt: "2024-01-01T11:00:00Z" },
- { status: true, createdAt: "2024-01-01T10:00:00Z" },
- { status: true, createdAt: "2024-01-01T09:00:00Z" },
- ];
-
- // Expected: Current time (12:00) - First check (09:00) = 3 hours = 10800000ms
- expect(calculateUptimeDuration(checks)).to.equal(10800000);
- });
- });
-
- describe("getLastChecked", function () {
- let clock;
- const NOW = new Date("2024-01-01T12:00:00Z").getTime();
-
- beforeEach(function () {
- // Fix the current time
- clock = sinon.useFakeTimers(NOW);
- });
-
- afterEach(function () {
- clock.restore();
- });
-
- it("should return 0 when checks array is empty", function () {
- expect(getLastChecked([])).to.equal(0);
- });
-
- it("should return 0 when checks array is null", function () {
- expect(getLastChecked(null)).to.equal(0);
- });
-
- it("should return time difference between now and most recent check", function () {
- const checks = [
- { createdAt: "2024-01-01T11:30:00Z" }, // 30 minutes ago
- { createdAt: "2024-01-01T11:00:00Z" },
- { createdAt: "2024-01-01T10:30:00Z" },
- ];
-
- // Expected: 30 minutes = 1800000ms
- expect(getLastChecked(checks)).to.equal(1800000);
- });
-
- it("should handle checks from different days", function () {
- const checks = [
- { createdAt: "2023-12-31T12:00:00Z" }, // 24 hours ago
- { createdAt: "2023-12-30T12:00:00Z" },
- ];
-
- // Expected: 24 hours = 86400000ms
- expect(getLastChecked(checks)).to.equal(86400000);
- });
- });
-
- describe("getLatestResponseTime", function () {
- it("should return 0 when checks array is empty", function () {
- expect(getLatestResponseTime([])).to.equal(0);
- });
-
- it("should return 0 when checks array is null", function () {
- expect(getLatestResponseTime(null)).to.equal(0);
- });
-
- it("should return response time from most recent check", function () {
- const checks = [
- { responseTime: 150, createdAt: "2024-01-01T11:30:00Z" }, // Most recent
- { responseTime: 200, createdAt: "2024-01-01T11:00:00Z" },
- { responseTime: 250, createdAt: "2024-01-01T10:30:00Z" },
- ];
-
- expect(getLatestResponseTime(checks)).to.equal(150);
- });
-
- it("should handle missing responseTime in checks", function () {
- const checks = [{ createdAt: "2024-01-01T11:30:00Z" }, { responseTime: 200, createdAt: "2024-01-01T11:00:00Z" }];
-
- expect(getLatestResponseTime(checks)).to.equal(0);
- });
- });
-
- describe("getAverageResponseTime", function () {
- it("should return 0 when checks array is empty", function () {
- expect(getAverageResponseTime([])).to.equal(0);
- });
-
- it("should return 0 when checks array is null", function () {
- expect(getAverageResponseTime(null)).to.equal(0);
- });
-
- it("should calculate average response time from all checks", function () {
- const checks = [
- { responseTime: 100, createdAt: "2024-01-01T11:30:00Z" },
- { responseTime: 200, createdAt: "2024-01-01T11:00:00Z" },
- { responseTime: 300, createdAt: "2024-01-01T10:30:00Z" },
- ];
-
- // Average: (100 + 200 + 300) / 3 = 200
- expect(getAverageResponseTime(checks)).to.equal(200);
- });
-
- it("should handle missing responseTime in some checks", function () {
- const checks = [
- { responseTime: 100, createdAt: "2024-01-01T11:30:00Z" },
- { createdAt: "2024-01-01T11:00:00Z" },
- { responseTime: 300, createdAt: "2024-01-01T10:30:00Z" },
- ];
-
- // Average: (100 + 300) / 2 = 200
- expect(getAverageResponseTime(checks)).to.equal(200);
- });
-
- it("should return 0 when no checks have responseTime", function () {
- const checks = [{ createdAt: "2024-01-01T11:30:00Z" }, { createdAt: "2024-01-01T11:00:00Z" }];
-
- expect(getAverageResponseTime(checks)).to.equal(0);
- });
- });
-
- describe("getUptimePercentage", function () {
- it("should return 0 when checks array is empty", function () {
- expect(getUptimePercentage([])).to.equal(0);
- });
-
- it("should return 0 when checks array is null", function () {
- expect(getUptimePercentage(null)).to.equal(0);
- });
-
- it("should return 100 when all checks are up", function () {
- const checks = [{ status: true }, { status: true }, { status: true }];
- expect(getUptimePercentage(checks)).to.equal(100);
- });
-
- it("should return 0 when all checks are down", function () {
- const checks = [{ status: false }, { status: false }, { status: false }];
- expect(getUptimePercentage(checks)).to.equal(0);
- });
-
- it("should calculate correct percentage for mixed status checks", function () {
- const checks = [{ status: true }, { status: false }, { status: true }, { status: true }];
- // 3 up out of 4 total = 75%
- expect(getUptimePercentage(checks)).to.equal(75);
- });
-
- it("should handle undefined status values", function () {
- const checks = [{ status: true }, { status: undefined }, { status: true }];
- // 2 up out of 3 total ≈ 66.67%
- expect(getUptimePercentage(checks)).to.equal((2 / 3) * 100);
- });
- });
-
- describe("getIncidents", function () {
- it("should return 0 when checks array is empty", function () {
- expect(getIncidents([])).to.equal(0);
- });
-
- it("should return 0 when checks array is null", function () {
- expect(getIncidents(null)).to.equal(0);
- });
-
- it("should return 0 when all checks are up", function () {
- const checks = [{ status: true }, { status: true }, { status: true }];
- expect(getIncidents(checks)).to.equal(0);
- });
-
- it("should count all incidents when all checks are down", function () {
- const checks = [{ status: false }, { status: false }, { status: false }];
- expect(getIncidents(checks)).to.equal(3);
- });
-
- it("should count correct number of incidents for mixed status checks", function () {
- const checks = [{ status: true }, { status: false }, { status: true }, { status: false }, { status: true }];
- expect(getIncidents(checks)).to.equal(2);
- });
-
- it("should handle undefined status values", function () {
- const checks = [{ status: true }, { status: undefined }, { status: false }, { status: false }];
- // Only counts explicit false values
- expect(getIncidents(checks)).to.equal(2);
- });
- });
-
- describe("getMonitorChecks", function () {
- let mockModel;
-
- beforeEach(function () {
- // Create a mock model with chainable methods
- const mockChecks = [
- { monitorId: "123", createdAt: new Date("2024-01-01") },
- { monitorId: "123", createdAt: new Date("2024-01-02") },
- ];
-
- mockModel = {
- find: sinon.stub().returns({
- sort: sinon.stub().returns(mockChecks),
- }),
- };
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should return all checks and date-ranged checks", async function () {
- // Arrange
- const monitorId = "123";
- const dateRange = {
- start: new Date("2024-01-01"),
- end: new Date("2024-01-02"),
- };
- const sortOrder = -1;
-
- // Act
- const result = await getMonitorChecks(monitorId, mockModel, dateRange, sortOrder);
-
- // Assert
- expect(result).to.have.keys(["checksAll", "checksForDateRange"]);
-
- // Verify find was called with correct parameters
- expect(mockModel.find.firstCall.args[0]).to.deep.equal({ monitorId });
- expect(mockModel.find.secondCall.args[0]).to.deep.equal({
- monitorId,
- createdAt: { $gte: dateRange.start, $lte: dateRange.end },
- });
-
- // Verify sort was called with correct parameters
- const sortCalls = mockModel.find().sort.getCalls();
- sortCalls.forEach((call) => {
- expect(call.args[0]).to.deep.equal({ createdAt: sortOrder });
- });
- });
-
- it("should handle empty results", async function () {
- // Arrange
- const emptyModel = {
- find: sinon.stub().returns({
- sort: sinon.stub().returns([]),
- }),
- };
-
- // Act
- const result = await getMonitorChecks(
- "123",
- emptyModel,
- {
- start: new Date(),
- end: new Date(),
- },
- -1
- );
-
- // Assert
- expect(result.checksAll).to.be.an("array").that.is.empty;
- expect(result.checksForDateRange).to.be.an("array").that.is.empty;
- });
-
- it("should maintain sort order", async function () {
- // Arrange
- const sortedChecks = [
- { monitorId: "123", createdAt: new Date("2024-01-02") },
- { monitorId: "123", createdAt: new Date("2024-01-01") },
- ];
-
- const sortedModel = {
- find: sinon.stub().returns({
- sort: sinon.stub().returns(sortedChecks),
- }),
- };
-
- // Act
- const result = await getMonitorChecks(
- "123",
- sortedModel,
- {
- start: new Date("2024-01-01"),
- end: new Date("2024-01-02"),
- },
- -1
- );
-
- // Assert
- expect(result.checksAll[0].createdAt).to.be.greaterThan(result.checksAll[1].createdAt);
- expect(result.checksForDateRange[0].createdAt).to.be.greaterThan(result.checksForDateRange[1].createdAt);
- });
- });
-
- describe("processChecksForDisplay", function () {
- let normalizeStub;
-
- beforeEach(function () {
- normalizeStub = sinon.stub();
- });
-
- it("should return original checks when numToDisplay is not provided", function () {
- const checks = [1, 2, 3, 4, 5];
- const result = processChecksForDisplay(normalizeStub, checks);
- expect(result).to.deep.equal(checks);
- });
-
- it("should return original checks when numToDisplay is greater than checks length", function () {
- const checks = [1, 2, 3];
- const result = processChecksForDisplay(normalizeStub, checks, 5);
- expect(result).to.deep.equal(checks);
- });
-
- it("should filter checks based on numToDisplay", function () {
- const checks = [1, 2, 3, 4, 5, 6];
- const result = processChecksForDisplay(normalizeStub, checks, 3);
- // Should return [1, 3, 5] as n = ceil(6/3) = 2
- expect(result).to.deep.equal([1, 3, 5]);
- });
-
- it("should handle empty checks array", function () {
- const checks = [];
- const result = processChecksForDisplay(normalizeStub, checks, 3);
- expect(result).to.be.an("array").that.is.empty;
- });
-
- it("should call normalizeData when normalize is true", function () {
- const checks = [1, 2, 3];
- normalizeStub.returns([10, 20, 30]);
-
- const result = processChecksForDisplay(normalizeStub, checks, null, true);
-
- expect(normalizeStub.args[0]).to.deep.equal([checks, 1, 100]);
- expect(result).to.deep.equal([10, 20, 30]);
- });
-
- it("should handle both filtering and normalization", function () {
- const checks = [1, 2, 3, 4, 5, 6];
- normalizeStub.returns([10, 30, 50]);
-
- const result = processChecksForDisplay(normalizeStub, checks, 3, true);
-
- expect(normalizeStub.args[0][0]).to.deep.equal([1, 3, 5]);
- expect(result).to.deep.equal([10, 30, 50]);
- });
- });
-
- describe("groupChecksByTime", function () {
- const mockChecks = [
- { createdAt: "2024-01-15T10:30:45Z" },
- { createdAt: "2024-01-15T10:45:15Z" },
- { createdAt: "2024-01-15T11:15:00Z" },
- { createdAt: "2024-01-16T10:30:00Z" },
- ];
-
- it("should group checks by hour when dateRange is 'day'", function () {
- const result = groupChecksByTime(mockChecks, "day");
-
- // Get timestamps for 10:00 and 11:00 on Jan 15
- const time1 = new Date("2024-01-15T10:00:00Z").getTime();
- const time2 = new Date("2024-01-15T11:00:00Z").getTime();
- const time3 = new Date("2024-01-16T10:00:00Z").getTime();
-
- expect(Object.keys(result)).to.have.lengthOf(3);
-
- expect(result[time1].checks).to.have.lengthOf(2);
- expect(result[time2].checks).to.have.lengthOf(1);
- expect(result[time3].checks).to.have.lengthOf(1);
- });
-
- it("should group checks by day when dateRange is not 'day'", function () {
- const result = groupChecksByTime(mockChecks, "week");
-
- expect(Object.keys(result)).to.have.lengthOf(2);
- expect(result["2024-01-15"].checks).to.have.lengthOf(3);
- expect(result["2024-01-16"].checks).to.have.lengthOf(1);
- });
-
- it("should handle empty checks array", function () {
- const result = groupChecksByTime([], "day");
- expect(result).to.deep.equal({});
- });
-
- it("should handle single check", function () {
- const singleCheck = [{ createdAt: "2024-01-15T10:30:45Z" }];
- const result = groupChecksByTime(singleCheck, "day");
-
- const expectedTime = new Date("2024-01-15T10:00:00Z").getTime();
- expect(Object.keys(result)).to.have.lengthOf(1);
- expect(result[expectedTime].checks).to.have.lengthOf(1);
- });
-
- it("should skip invalid dates and process valid ones", function () {
- const checksWithInvalidDate = [
- { createdAt: "invalid-date" },
- { createdAt: "2024-01-15T10:30:45Z" },
- { createdAt: null },
- { createdAt: undefined },
- { createdAt: "" },
- ];
-
- const result = groupChecksByTime(checksWithInvalidDate, "day");
-
- const expectedTime = new Date("2024-01-15T10:00:00Z").getTime();
- expect(Object.keys(result)).to.have.lengthOf(1);
- expect(result[expectedTime].checks).to.have.lengthOf(1);
- expect(result[expectedTime].checks[0].createdAt).to.equal("2024-01-15T10:30:45Z");
- });
-
- it("should handle checks in same time group", function () {
- const checksInSameHour = [{ createdAt: "2024-01-15T10:15:00Z" }, { createdAt: "2024-01-15T10:45:00Z" }];
-
- const result = groupChecksByTime(checksInSameHour, "day");
-
- const expectedTime = new Date("2024-01-15T10:00:00Z").getTime();
- expect(Object.keys(result)).to.have.lengthOf(1);
- expect(result[expectedTime].checks).to.have.lengthOf(2);
- });
- });
-
- describe("calculateGroupStats", function () {
- // Mock getUptimePercentage function
- let uptimePercentageStub;
-
- beforeEach(function () {
- uptimePercentageStub = sinon.stub();
- uptimePercentageStub.returns(95); // Default return value
- });
-
- it("should calculate stats correctly for a group of checks", function () {
- const mockGroup = {
- time: "2024-01-15",
- checks: [
- { status: true, responseTime: 100 },
- { status: false, responseTime: 200 },
- { status: true, responseTime: 300 },
- ],
- };
-
- const result = calculateGroupStats(mockGroup, uptimePercentageStub);
-
- expect(result).to.deep.equal({
- time: "2024-01-15",
- uptimePercentage: (2 / 3) * 100,
- totalChecks: 3,
- totalIncidents: 1,
- avgResponseTime: 200, // (100 + 200 + 300) / 3
- });
- });
-
- it("should handle empty checks array", function () {
- const mockGroup = {
- time: "2024-01-15",
- checks: [],
- };
-
- const result = calculateGroupStats(mockGroup, uptimePercentageStub);
-
- expect(result).to.deep.equal({
- time: "2024-01-15",
- uptimePercentage: 0,
- totalChecks: 0,
- totalIncidents: 0,
- avgResponseTime: 0,
- });
- });
-
- it("should handle missing responseTime values", function () {
- const mockGroup = {
- time: "2024-01-15",
- checks: [{ status: true }, { status: false, responseTime: 200 }, { status: true, responseTime: undefined }],
- };
-
- const result = calculateGroupStats(mockGroup, uptimePercentageStub);
-
- expect(result).to.deep.equal({
- time: "2024-01-15",
- uptimePercentage: (2 / 3) * 100,
- totalChecks: 3,
- totalIncidents: 1,
- avgResponseTime: 200, // 200 / 1
- });
- });
-
- it("should handle all checks with status false", function () {
- const mockGroup = {
- time: "2024-01-15",
- checks: [
- { status: false, responseTime: 100 },
- { status: false, responseTime: 200 },
- { status: false, responseTime: 300 },
- ],
- };
-
- const result = calculateGroupStats(mockGroup, uptimePercentageStub);
-
- expect(result).to.deep.equal({
- time: "2024-01-15",
- uptimePercentage: 0,
- totalChecks: 3,
- totalIncidents: 3,
- avgResponseTime: 200,
- });
- });
-
- it("should handle all checks with status true", function () {
- const mockGroup = {
- time: "2024-01-15",
- checks: [
- { status: true, responseTime: 100 },
- { status: true, responseTime: 200 },
- { status: true, responseTime: 300 },
- ],
- };
-
- const result = calculateGroupStats(mockGroup, uptimePercentageStub);
-
- expect(result).to.deep.equal({
- time: "2024-01-15",
- uptimePercentage: 100,
- totalChecks: 3,
- totalIncidents: 0,
- avgResponseTime: 200,
- });
- });
- });
-
- describe("getMonitorStatsById", function () {
- const now = new Date();
- const oneHourAgo = new Date(now - 3600000);
- const twoHoursAgo = new Date(now - 7200000);
-
- const mockMonitor = {
- _id: "monitor123",
- type: "http",
- name: "Test Monitor",
- url: "https://test.com",
- toObject: () => ({
- _id: "monitor123",
- type: "http",
- name: "Test Monitor",
- url: "https://test.com",
- }),
- };
-
- const mockMonitorPing = {
- _id: "monitor123",
- type: "ping",
- name: "Test Monitor",
- url: "https://test.com",
- toObject: () => ({
- _id: "monitor123",
- type: "http",
- name: "Test Monitor",
- url: "https://test.com",
- }),
- };
- const mockMonitorDocker = {
- _id: "monitor123",
- type: "docker",
- name: "Test Monitor",
- url: "https://test.com",
- toObject: () => ({
- _id: "monitor123",
- type: "http",
- name: "Test Monitor",
- url: "https://test.com",
- }),
- };
-
- const checkDocs = [
- {
- monitorId: "monitor123",
- status: true,
- responseTime: 100,
- createdAt: new Date("2024-01-01T12:00:00Z"),
- toObject: function () {
- return {
- monitorId: this.monitorId,
- status: this.status,
- responseTime: this.responseTime,
- createdAt: this.createdAt,
- };
- },
- },
- {
- monitorId: "monitor123",
- status: true,
- responseTime: 150,
- createdAt: new Date("2024-01-01T11:00:00Z"),
- toObject: function () {
- return {
- monitorId: this.monitorId,
- status: this.status,
- responseTime: this.responseTime,
- createdAt: this.createdAt,
- };
- },
- },
- {
- monitorId: "monitor123",
- status: false,
- responseTime: 200,
- createdAt: new Date("2024-01-01T10:00:00Z"),
- toObject: function () {
- return {
- monitorId: this.monitorId,
- status: this.status,
- responseTime: this.responseTime,
- createdAt: this.createdAt,
- };
- },
- },
- ];
- const req = {
- params: { monitorId: "monitor123" },
- query: {
- dateRange: "day",
- sortOrder: "desc",
- numToDisplay: 10,
- normalize: true,
- },
- };
-
- beforeEach(function () {
- checkFindStub.returns({
- sort: () => checkDocs,
- });
- monitorFindByIdStub.returns(mockMonitor);
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should return monitor stats with calculated values, sort order desc", async function () {
- req.query.sortOrder = "desc";
- const result = await getMonitorStatsById(req);
- expect(result).to.include.keys([
- "_id",
- "type",
- "name",
- "url",
- "uptimeDuration",
- "lastChecked",
- "latestResponseTime",
- "periodIncidents",
- "periodTotalChecks",
- "periodAvgResponseTime",
- "periodUptime",
- "aggregateData",
- ]);
- expect(result.latestResponseTime).to.equal(100);
- expect(result.periodTotalChecks).to.equal(3);
- expect(result.periodIncidents).to.equal(1);
- expect(result.periodUptime).to.be.a("number");
- expect(result.aggregateData).to.be.an("array");
- });
-
- it("should return monitor stats with calculated values, ping type", async function () {
- monitorFindByIdStub.returns(mockMonitorPing);
- req.query.sortOrder = "desc";
- const result = await getMonitorStatsById(req);
- expect(result).to.include.keys([
- "_id",
- "type",
- "name",
- "url",
- "uptimeDuration",
- "lastChecked",
- "latestResponseTime",
- "periodIncidents",
- "periodTotalChecks",
- "periodAvgResponseTime",
- "periodUptime",
- "aggregateData",
- ]);
- expect(result.latestResponseTime).to.equal(100);
- expect(result.periodTotalChecks).to.equal(3);
- expect(result.periodIncidents).to.equal(1);
- expect(result.periodUptime).to.be.a("number");
- expect(result.aggregateData).to.be.an("array");
- });
-
- it("should return monitor stats with calculated values, docker type", async function () {
- monitorFindByIdStub.returns(mockMonitorDocker);
- req.query.sortOrder = "desc";
- const result = await getMonitorStatsById(req);
- expect(result).to.include.keys([
- "_id",
- "type",
- "name",
- "url",
- "uptimeDuration",
- "lastChecked",
- "latestResponseTime",
- "periodIncidents",
- "periodTotalChecks",
- "periodAvgResponseTime",
- "periodUptime",
- "aggregateData",
- ]);
- expect(result.latestResponseTime).to.equal(100);
- expect(result.periodTotalChecks).to.equal(3);
- expect(result.periodIncidents).to.equal(1);
- expect(result.periodUptime).to.be.a("number");
- expect(result.aggregateData).to.be.an("array");
- });
-
- it("should return monitor stats with calculated values", async function () {
- req.query.sortOrder = "asc";
- const result = await getMonitorStatsById(req);
- expect(result).to.include.keys([
- "_id",
- "type",
- "name",
- "url",
- "uptimeDuration",
- "lastChecked",
- "latestResponseTime",
- "periodIncidents",
- "periodTotalChecks",
- "periodAvgResponseTime",
- "periodUptime",
- "aggregateData",
- ]);
- expect(result.latestResponseTime).to.equal(100);
- expect(result.periodTotalChecks).to.equal(3);
- expect(result.periodIncidents).to.equal(1);
- expect(result.periodUptime).to.be.a("number");
- expect(result.aggregateData).to.be.an("array");
- });
-
- it("should throw error when monitor is not found", async function () {
- monitorFindByIdStub.returns(Promise.resolve(null));
-
- const req = {
- params: { monitorId: "nonexistent" },
- };
-
- try {
- await getMonitorStatsById(req);
- expect.fail("Should have thrown an error");
- } catch (error) {
- expect(error).to.be.an("Error");
- expect(error.service).to.equal("monitorModule");
- expect(error.method).to.equal("getMonitorStatsById");
- }
- });
- });
-
- describe("getMonitorById", function () {
- let notificationFindStub;
- let monitorSaveStub;
-
- beforeEach(function () {
- // Create stubs
- notificationFindStub = sinon.stub(Notification, "find");
- monitorSaveStub = sinon.stub().resolves();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should return monitor with notifications when found", async function () {
- // Arrange
- const monitorId = "123";
- const mockMonitor = {
- _id: monitorId,
- name: "Test Monitor",
- save: monitorSaveStub,
- };
- const mockNotifications = [
- { _id: "notif1", message: "Test notification 1" },
- { _id: "notif2", message: "Test notification 2" },
- ];
-
- monitorFindByIdStub.resolves(mockMonitor);
- notificationFindStub.resolves(mockNotifications);
-
- const result = await getMonitorById(monitorId);
- expect(result._id).to.equal(monitorId);
- expect(result.name).to.equal("Test Monitor");
- expect(monitorFindByIdStub.calledWith(monitorId)).to.be.true;
- expect(notificationFindStub.calledWith({ monitorId })).to.be.true;
- expect(monitorSaveStub.calledOnce).to.be.true;
- });
-
- it("should throw 404 error when monitor not found", async function () {
- // Arrange
- const monitorId = "nonexistent";
- monitorFindByIdStub.resolves(null);
-
- // Act & Assert
- try {
- await getMonitorById(monitorId);
- expect.fail("Should have thrown an error");
- } catch (error) {
- expect(error.message).to.equal(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
- expect(error.status).to.equal(404);
- expect(error.service).to.equal("monitorModule");
- expect(error.method).to.equal("getMonitorById");
- }
- });
-
- it("should handle database errors properly", async function () {
- // Arrange
- const monitorId = "123";
- const dbError = new Error("Database connection failed");
- monitorFindByIdStub.rejects(dbError);
-
- // Act & Assert
- try {
- await getMonitorById(monitorId);
- expect.fail("Should have thrown an error");
- } catch (error) {
- expect(error.service).to.equal("monitorModule");
- expect(error.method).to.equal("getMonitorById");
- expect(error.message).to.equal("Database connection failed");
- }
- });
-
- it("should handle notification fetch errors", async function () {
- // Arrange
- const monitorId = "123";
- const mockMonitor = {
- _id: monitorId,
- name: "Test Monitor",
- save: monitorSaveStub,
- };
- const notificationError = new Error("Notification fetch failed");
-
- monitorFindByIdStub.resolves(mockMonitor);
- notificationFindStub.rejects(notificationError);
-
- // Act & Assert
- try {
- await getMonitorById(monitorId);
- expect.fail("Should have thrown an error");
- } catch (error) {
- expect(error.service).to.equal("monitorModule");
- expect(error.method).to.equal("getMonitorById");
- expect(error.message).to.equal("Notification fetch failed");
- }
- });
-
- it("should handle monitor save errors", async function () {
- // Arrange
- const monitorId = "123";
- const mockMonitor = {
- _id: monitorId,
- name: "Test Monitor",
- save: sinon.stub().rejects(new Error("Save failed")),
- };
- const mockNotifications = [];
-
- monitorFindByIdStub.resolves(mockMonitor);
- notificationFindStub.resolves(mockNotifications);
-
- // Act & Assert
- try {
- await getMonitorById(monitorId);
- expect.fail("Should have thrown an error");
- } catch (error) {
- expect(error.service).to.equal("monitorModule");
- expect(error.method).to.equal("getMonitorById");
- expect(error.message).to.equal("Save failed");
- }
- });
- });
-
- describe("getMonitorsAndSummaryByTeamId", function () {
- it("should return monitors and correct summary counts", async function () {
- // Arrange
- const teamId = "team123";
- const type = "http";
- const mockMonitors = [
- { teamId, type, status: true, isActive: true }, // up
- { teamId, type, status: false, isActive: true }, // down
- { teamId, type, status: null, isActive: false }, // paused
- { teamId, type, status: true, isActive: true }, // up
- ];
- monitorFindStub.resolves(mockMonitors);
-
- // Act
- const result = await getMonitorsAndSummaryByTeamId(teamId, type);
-
- // Assert
- expect(result.monitors).to.have.lengthOf(4);
- expect(result.monitorCounts).to.deep.equal({
- up: 2,
- down: 1,
- paused: 1,
- total: 4,
- });
- expect(monitorFindStub.calledOnceWith({ teamId, type })).to.be.true;
- });
-
- it("should return empty results for non-existent team", async function () {
- // Arrange
- monitorFindStub.resolves([]);
-
- // Act
- const result = await getMonitorsAndSummaryByTeamId("nonexistent", "http");
-
- // Assert
- expect(result.monitors).to.have.lengthOf(0);
- expect(result.monitorCounts).to.deep.equal({
- up: 0,
- down: 0,
- paused: 0,
- total: 0,
- });
- });
-
- it("should handle database errors", async function () {
- // Arrange
- const error = new Error("Database error");
- error.service = "MonitorModule";
- error.method = "getMonitorsAndSummaryByTeamId";
- monitorFindStub.rejects(error);
-
- // Act & Assert
- try {
- await getMonitorsAndSummaryByTeamId("team123", "http");
- expect.fail("Should have thrown an error");
- } catch (err) {
- expect(err).to.equal(error);
- expect(err.service).to.equal("monitorModule");
- expect(err.method).to.equal("getMonitorsAndSummaryByTeamId");
- }
- });
- });
-
- describe("getMonitorsByTeamId", function () {
- beforeEach(function () {
- // Chain stubs for Monitor.find().skip().limit().sort()
-
- // Stub for CHECK_MODEL_LOOKUP model find
- checkFindStub.returns({
- sort: sinon.stub().returns({
- limit: sinon.stub().returns([]),
- }),
- });
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should return monitors with basic query parameters", async function () {
- const mockMonitors = [
- { _id: "1", type: "http", toObject: () => ({ _id: "1", type: "http" }) },
- { _id: "2", type: "ping", toObject: () => ({ _id: "2", type: "ping" }) },
- ];
- monitorFindStub.returns({
- skip: sinon.stub().returns({
- limit: sinon.stub().returns({
- sort: sinon.stub().returns(mockMonitors),
- }),
- }),
- });
-
- const req = {
- params: { teamId: "team123" },
- query: {
- type: "http",
- page: 0,
- rowsPerPage: 10,
- field: "name",
- status: false,
- checkOrder: "desc",
- },
- };
-
- monitorCountStub.resolves(2);
-
- const result = await getMonitorsByTeamId(req);
-
- expect(result).to.have.property("monitors");
- expect(result).to.have.property("monitorCount", 2);
- });
-
- it("should return monitors with basic query parameters", async function () {
- const mockMonitors = [
- { _id: "1", type: "http", toObject: () => ({ _id: "1", type: "http" }) },
- { _id: "2", type: "ping", toObject: () => ({ _id: "2", type: "ping" }) },
- ];
- monitorFindStub.returns({
- skip: sinon.stub().returns({
- limit: sinon.stub().returns({
- sort: sinon.stub().returns(mockMonitors),
- }),
- }),
- });
-
- const req = {
- params: { teamId: "team123" },
- query: {
- type: "http",
- page: 0,
- rowsPerPage: 10,
- field: "name",
- status: true,
- checkOrder: "asc",
- },
- };
-
- monitorCountStub.resolves(2);
-
- const result = await getMonitorsByTeamId(req);
-
- expect(result).to.have.property("monitors");
- expect(result).to.have.property("monitorCount", 2);
- });
-
- it("should handle type filter with array input", async function () {
- const req = {
- params: { teamId: "team123" },
- query: {
- type: ["http", "ping"],
- },
- };
-
- monitorFindStub.returns({
- skip: sinon.stub().returns({
- limit: sinon.stub().returns({
- sort: sinon.stub().returns([]),
- }),
- }),
- });
- monitorCountStub.resolves(0);
-
- await getMonitorsByTeamId(req);
-
- expect(Monitor.find.firstCall.args[0]).to.deep.equal({
- teamId: "team123",
- type: { $in: ["http", "ping"] },
- });
- });
-
- it("should handle text search filter", async function () {
- const req = {
- params: { teamId: "team123" },
- query: {
- filter: "search",
- },
- };
-
- monitorFindStub.returns({
- skip: sinon.stub().returns({
- limit: sinon.stub().returns({
- sort: sinon.stub().returns([]),
- }),
- }),
- });
- monitorCountStub.resolves(0);
-
- await getMonitorsByTeamId(req);
-
- expect(Monitor.find.firstCall.args[0]).to.deep.equal({
- teamId: "team123",
- $or: [{ name: { $regex: "search", $options: "i" } }, { url: { $regex: "search", $options: "i" } }],
- });
- });
-
- it("should handle pagination parameters", async function () {
- const req = {
- params: { teamId: "team123" },
- query: {
- page: 2,
- rowsPerPage: 5,
- },
- };
-
- monitorFindStub.returns({
- skip: sinon.stub().returns({
- limit: sinon.stub().returns({
- sort: sinon.stub().returns([]),
- }),
- }),
- });
- monitorCountStub.resolves(0);
-
- const result = await getMonitorsByTeamId(req);
- expect(result).to.deep.equal({
- monitors: [],
- monitorCount: 0,
- });
- });
-
- it("should handle sorting parameters", async function () {
- const req = {
- params: { teamId: "team123" },
- query: {
- field: "name",
- order: "asc",
- },
- };
-
- monitorFindStub.returns({
- skip: sinon.stub().returns({
- limit: sinon.stub().returns({
- sort: sinon.stub().returns([]),
- }),
- }),
- });
- monitorCountStub.resolves(0);
-
- await getMonitorsByTeamId(req);
-
- const result = await getMonitorsByTeamId(req);
- expect(result).to.deep.equal({
- monitors: [],
- monitorCount: 0,
- });
- });
-
- it("should return early when limit is -1", async function () {
- // Arrange
- const req = {
- params: { teamId: "team123" },
- query: {
- limit: "-1",
- },
- };
-
- const mockMonitors = [
- { _id: "1", type: "http" },
- { _id: "2", type: "ping" },
- ];
-
- monitorFindStub.returns({
- skip: sinon.stub().returns({
- limit: sinon.stub().returns({
- sort: sinon.stub().returns(mockMonitors),
- }),
- }),
- });
-
- monitorCountStub.resolves(2);
-
- // Act
- const result = await getMonitorsByTeamId(req);
-
- // Assert
- expect(result).to.deep.equal({
- monitors: mockMonitors,
- monitorCount: 2,
- });
- });
-
- it("should normalize checks when normalize parameter is provided", async function () {
- const req = {
- params: { teamId: "team123" },
- query: { normalize: "true" },
- };
- monitorCountStub.resolves(2);
-
- const mockMonitors = [
- { _id: "1", type: "http", toObject: () => ({ _id: "1", type: "http" }) },
- { _id: "2", type: "ping", toObject: () => ({ _id: "2", type: "ping" }) },
- ];
-
- monitorFindStub.returns({
- skip: sinon.stub().returns({
- limit: sinon.stub().returns({
- sort: sinon.stub().returns(mockMonitors),
- }),
- }),
- });
-
- const result = await getMonitorsByTeamId(req);
- expect(result.monitorCount).to.equal(2);
- expect(result.monitors).to.have.lengthOf(2);
- });
-
- it("should handle database errors", async function () {
- const req = {
- params: { teamId: "team123" },
- query: {},
- };
-
- const error = new Error("Database error");
- monitorFindStub.returns({
- skip: sinon.stub().returns({
- limit: sinon.stub().returns({
- sort: sinon.stub().throws(error),
- }),
- }),
- });
-
- try {
- await getMonitorsByTeamId(req);
- expect.fail("Should have thrown an error");
- } catch (err) {
- expect(err.service).to.equal("monitorModule");
- expect(err.method).to.equal("getMonitorsByTeamId");
- expect(err.message).to.equal("Database error");
- }
- });
- });
-
- describe("createMonitor", function () {
- it("should create a monitor without notifications", async function () {
- let monitorSaveStub = sinon.stub(Monitor.prototype, "save").resolves();
-
- const req = {
- body: {
- name: "Test Monitor",
- url: "http://test.com",
- type: "http",
- notifications: ["someNotification"],
- },
- };
-
- const expectedMonitor = {
- name: "Test Monitor",
- url: "http://test.com",
- type: "http",
- notifications: undefined,
- save: monitorSaveStub,
- };
-
- const result = await createMonitor(req);
- expect(result.name).to.equal(expectedMonitor.name);
- expect(result.url).to.equal(expectedMonitor.url);
- });
-
- it("should handle database errors", async function () {
- const req = {
- body: {
- name: "Test Monitor",
- },
- };
-
- try {
- await createMonitor(req);
- expect.fail("Should have thrown an error");
- } catch (err) {
- expect(err.service).to.equal("monitorModule");
- expect(err.method).to.equal("createMonitor");
- }
- });
- });
-
- describe("deleteMonitor", function () {
- it("should delete a monitor successfully", async function () {
- const monitorId = "123456789";
- const mockMonitor = {
- _id: monitorId,
- name: "Test Monitor",
- url: "http://test.com",
- };
-
- const req = {
- params: { monitorId },
- };
-
- monitorFindByIdAndDeleteStub.resolves(mockMonitor);
-
- const result = await deleteMonitor(req);
-
- expect(result).to.deep.equal(mockMonitor);
- sinon.assert.calledWith(monitorFindByIdAndDeleteStub, monitorId);
- });
-
- it("should throw error when monitor not found", async function () {
- const monitorId = "nonexistent123";
- const req = {
- params: { monitorId },
- };
-
- monitorFindByIdAndDeleteStub.resolves(null);
-
- try {
- await deleteMonitor(req);
- expect.fail("Should have thrown an error");
- } catch (err) {
- expect(err.message).to.equal(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
- expect(err.service).to.equal("monitorModule");
- expect(err.method).to.equal("deleteMonitor");
- }
- });
-
- it("should handle database errors", async function () {
- const monitorId = "123456789";
- const req = {
- params: { monitorId },
- };
-
- const dbError = new Error("Database connection error");
- monitorFindByIdAndDeleteStub.rejects(dbError);
-
- try {
- await deleteMonitor(req);
- expect.fail("Should have thrown an error");
- } catch (err) {
- expect(err.message).to.equal("Database connection error");
- expect(err.service).to.equal("monitorModule");
- expect(err.method).to.equal("deleteMonitor");
- }
- });
- });
-
- describe("deleteAllMonitors", function () {
- it("should delete all monitors for a team successfully", async function () {
- const teamId = "team123";
- const mockMonitors = [
- { _id: "1", name: "Monitor 1", teamId },
- { _id: "2", name: "Monitor 2", teamId },
- ];
-
- monitorFindStub.resolves(mockMonitors);
- monitorDeleteManyStub.resolves({ deletedCount: 2 });
-
- const result = await deleteAllMonitors(teamId);
-
- expect(result).to.deep.equal({
- monitors: mockMonitors,
- deletedCount: 2,
- });
- sinon.assert.calledWith(monitorFindStub, { teamId });
- sinon.assert.calledWith(monitorDeleteManyStub, { teamId });
- });
-
- it("should return empty array when no monitors found", async function () {
- const teamId = "emptyTeam";
-
- monitorFindStub.resolves([]);
- monitorDeleteManyStub.resolves({ deletedCount: 0 });
-
- const result = await deleteAllMonitors(teamId);
-
- expect(result).to.deep.equal({
- monitors: [],
- deletedCount: 0,
- });
- sinon.assert.calledWith(monitorFindStub, { teamId });
- sinon.assert.calledWith(monitorDeleteManyStub, { teamId });
- });
-
- it("should handle database errors", async function () {
- const teamId = "team123";
- const dbError = new Error("Database connection error");
- monitorFindStub.rejects(dbError);
-
- try {
- await deleteAllMonitors(teamId);
- } catch (err) {
- expect(err.message).to.equal("Database connection error");
- expect(err.service).to.equal("monitorModule");
- expect(err.method).to.equal("deleteAllMonitors");
- }
- });
-
- it("should handle deleteMany errors", async function () {
- const teamId = "team123";
- monitorFindStub.resolves([{ _id: "1", name: "Monitor 1" }]);
- monitorDeleteManyStub.rejects(new Error("Delete operation failed"));
-
- try {
- await deleteAllMonitors(teamId);
- } catch (err) {
- expect(err.message).to.equal("Delete operation failed");
- expect(err.service).to.equal("monitorModule");
- expect(err.method).to.equal("deleteAllMonitors");
- }
- });
- });
-
- describe("deleteMonitorsByUserId", function () {
- beforeEach(function () {});
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should delete all monitors for a user successfully", async function () {
- // Arrange
- const userId = "user123";
- const mockResult = {
- deletedCount: 3,
- acknowledged: true,
- };
-
- monitorDeleteManyStub.resolves(mockResult);
-
- // Act
- const result = await deleteMonitorsByUserId(userId);
-
- // Assert
- expect(result).to.deep.equal(mockResult);
- sinon.assert.calledWith(monitorDeleteManyStub, { userId: userId });
- });
-
- it("should return zero deletedCount when no monitors found", async function () {
- // Arrange
- const userId = "nonexistentUser";
- const mockResult = {
- deletedCount: 0,
- acknowledged: true,
- };
-
- monitorDeleteManyStub.resolves(mockResult);
-
- // Act
- const result = await deleteMonitorsByUserId(userId);
-
- // Assert
- expect(result.deletedCount).to.equal(0);
- sinon.assert.calledWith(monitorDeleteManyStub, { userId: userId });
- });
-
- it("should handle database errors", async function () {
- // Arrange
- const userId = "user123";
- const dbError = new Error("Database connection error");
- monitorDeleteManyStub.rejects(dbError);
-
- // Act & Assert
- try {
- await deleteMonitorsByUserId(userId);
- expect.fail("Should have thrown an error");
- } catch (err) {
- expect(err.message).to.equal("Database connection error");
- expect(err.service).to.equal("monitorModule");
- expect(err.method).to.equal("deleteMonitorsByUserId");
- }
- });
- });
-
- describe("editMonitor", function () {
- it("should edit a monitor successfully", async function () {
- // Arrange
- const candidateId = "monitor123";
- const candidateMonitor = {
- name: "Updated Monitor",
- url: "http://updated.com",
- type: "http",
- notifications: ["someNotification"],
- };
-
- const expectedUpdateData = {
- name: "Updated Monitor",
- url: "http://updated.com",
- type: "http",
- notifications: undefined,
- };
-
- const mockUpdatedMonitor = {
- _id: candidateId,
- ...expectedUpdateData,
- };
-
- monitorFindByIdAndUpdateStub.resolves(mockUpdatedMonitor);
-
- // Act
- const result = await editMonitor(candidateId, candidateMonitor);
-
- // Assert
- expect(result).to.deep.equal(mockUpdatedMonitor);
- sinon.assert.calledWith(monitorFindByIdAndUpdateStub, candidateId, expectedUpdateData, {
- new: true,
- });
- });
-
- it("should return null when monitor not found", async function () {
- // Arrange
- const candidateId = "nonexistent123";
- const candidateMonitor = {
- name: "Updated Monitor",
- };
-
- monitorFindByIdAndUpdateStub.resolves(null);
-
- // Act
- const result = await editMonitor(candidateId, candidateMonitor);
-
- // Assert
- expect(result).to.be.null;
- sinon.assert.calledWith(monitorFindByIdAndUpdateStub, candidateId, { name: "Updated Monitor", notifications: undefined }, { new: true });
- });
-
- it("should remove notifications from update data", async function () {
- // Arrange
- const candidateId = "monitor123";
- const candidateMonitor = {
- name: "Updated Monitor",
- notifications: ["notification1", "notification2"],
- };
-
- const expectedUpdateData = {
- name: "Updated Monitor",
- notifications: undefined,
- };
-
- monitorFindByIdAndUpdateStub.resolves({
- _id: candidateId,
- ...expectedUpdateData,
- });
-
- // Act
- await editMonitor(candidateId, candidateMonitor);
-
- // Assert
- sinon.assert.calledWith(monitorFindByIdAndUpdateStub, candidateId, expectedUpdateData, {
- new: true,
- });
- });
-
- it("should handle database errors", async function () {
- // Arrange
- const candidateId = "monitor123";
- const candidateMonitor = {
- name: "Updated Monitor",
- };
-
- const dbError = new Error("Database connection error");
- monitorFindByIdAndUpdateStub.rejects(dbError);
-
- // Act & Assert
- try {
- await editMonitor(candidateId, candidateMonitor);
- expect.fail("Should have thrown an error");
- } catch (err) {
- expect(err.message).to.equal("Database connection error");
- expect(err.service).to.equal("monitorModule");
- expect(err.method).to.equal("editMonitor");
- }
- });
- });
-
- describe("addDemoMonitors", function () {
- it("should add demo monitors successfully", async function () {
- // Arrange
- const userId = "user123";
- const teamId = "team123";
- monitorInsertManyStub.resolves([{ _id: "123" }]);
- const result = await addDemoMonitors(userId, teamId);
- expect(result).to.deep.equal([{ _id: "123" }]);
- });
-
- it("should handle database errors", async function () {
- const userId = "user123";
- const teamId = "team123";
-
- const dbError = new Error("Database connection error");
- monitorInsertManyStub.rejects(dbError);
-
- try {
- await addDemoMonitors(userId, teamId);
- } catch (err) {
- expect(err.message).to.equal("Database connection error");
- expect(err.service).to.equal("monitorModule");
- expect(err.method).to.equal("addDemoMonitors");
- }
- });
- });
-});
diff --git a/server/tests/db/notificationModule.test.js b/server/tests/db/notificationModule.test.js
deleted file mode 100755
index fbb97f33a..000000000
--- a/server/tests/db/notificationModule.test.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import sinon from "sinon";
-import Notification from "../../db/models/Notification.js";
-import { createNotification, getNotificationsByMonitorId, deleteNotificationsByMonitorId } from "../../db/mongo/modules/notificationModule.js";
-
-describe("notificationModule", function () {
- const mockNotification = {
- monitorId: "123",
- };
- const mockNotifications = [mockNotification];
- let notificationSaveStub, notificationFindStub, notificationDeleteManyStub;
-
- beforeEach(function () {
- notificationSaveStub = sinon.stub(Notification.prototype, "save").resolves();
- notificationFindStub = sinon.stub(Notification, "find").resolves();
- notificationDeleteManyStub = sinon.stub(Notification, "deleteMany").resolves();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("createNotification", function () {
- it("should create a new notification", async function () {
- const notificationData = { _id: "123", name: "test" };
- notificationSaveStub.resolves(notificationData);
- const res = await createNotification(notificationData);
- expect(res).to.deep.equal(notificationData);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- notificationSaveStub.rejects(err);
- try {
- await createNotification(mockNotification);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("getNotificationsByMonitorId", function () {
- it("should return notifications by monitor ID", async function () {
- notificationFindStub.resolves(mockNotifications);
- const res = await getNotificationsByMonitorId(mockNotification.monitorId);
- expect(res).to.deep.equal(mockNotifications);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- notificationFindStub.rejects(err);
- try {
- await getNotificationsByMonitorId(mockNotification.monitorId);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("deleteNotificationsByMonitorId", function () {
- it("should delete notifications by monitor ID", async function () {
- notificationDeleteManyStub.resolves({ deletedCount: mockNotifications.length });
- const res = await deleteNotificationsByMonitorId(mockNotification.monitorId);
- expect(res).to.deep.equal(mockNotifications.length);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- notificationDeleteManyStub.rejects(err);
- try {
- await deleteNotificationsByMonitorId(mockNotification.monitorId);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-});
diff --git a/server/tests/db/pageSpeedCheckModule.test.js b/server/tests/db/pageSpeedCheckModule.test.js
deleted file mode 100755
index 45135135d..000000000
--- a/server/tests/db/pageSpeedCheckModule.test.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import sinon from "sinon";
-import PageSpeedCheck from "../../db/models/PageSpeedCheck.js";
-import { createPageSpeedCheck, deletePageSpeedChecksByMonitorId } from "../../db/mongo/modules/pageSpeedCheckModule.js";
-
-const mockPageSpeedCheck = {
- monitorId: "monitorId",
- bestPractices: 1,
- seo: 1,
- performance: 1,
-};
-
-const mockDeletedResult = { deletedCount: 1 };
-
-describe("pageSpeedCheckModule", function () {
- let pageSpeedCheckSaveStub, pageSpeedCheckDeleteManyStub;
-
- beforeEach(function () {
- pageSpeedCheckSaveStub = sinon.stub(PageSpeedCheck.prototype, "save");
- pageSpeedCheckDeleteManyStub = sinon.stub(PageSpeedCheck, "deleteMany");
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("createPageSpeedCheck", function () {
- it("should return a page speed check", async function () {
- pageSpeedCheckSaveStub.resolves(mockPageSpeedCheck);
- const pageSpeedCheck = await createPageSpeedCheck(mockPageSpeedCheck);
- expect(pageSpeedCheck).to.deep.equal(mockPageSpeedCheck);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- pageSpeedCheckSaveStub.rejects(err);
- try {
- await expect(createPageSpeedCheck(mockPageSpeedCheck));
- } catch (error) {
- expect(error).to.exist;
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("deletePageSpeedChecksByMonitorId", function () {
- it("should return the number of deleted checks", async function () {
- pageSpeedCheckDeleteManyStub.resolves(mockDeletedResult);
- const result = await deletePageSpeedChecksByMonitorId("monitorId");
- expect(result).to.deep.equal(mockDeletedResult.deletedCount);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- pageSpeedCheckDeleteManyStub.rejects(err);
- try {
- await expect(deletePageSpeedChecksByMonitorId("monitorId"));
- } catch (error) {
- expect(error).to.exist;
- expect(error).to.deep.equal(err);
- }
- });
- });
-});
diff --git a/server/tests/db/recoveryModule.test.js b/server/tests/db/recoveryModule.test.js
deleted file mode 100755
index 88afa257a..000000000
--- a/server/tests/db/recoveryModule.test.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import sinon from "sinon";
-import RecoveryToken from "../../db/models/RecoveryToken.js";
-import User from "../../db/models/User.js";
-import { requestRecoveryToken, validateRecoveryToken, resetPassword } from "../../db/mongo/modules/recoveryModule.js";
-import { errorMessages } from "../../utils/messages.js";
-
-const mockRecoveryToken = {
- email: "test@test.com",
- token: "1234567890",
-};
-
-const mockUser = {
- email: "test@test.com",
- password: "oldPassword",
-};
-
-const mockUserWithoutPassword = {
- email: "test@test.com",
-};
-
-// Create a query builder that logs
-const createQueryChain = (finalResult, comparePasswordResult = false) => ({
- select: () => ({
- select: async () => {
- if (finalResult === mockUser) {
- // Return a new object with all required methods
- return {
- email: "test@test.com",
- password: "oldPassword",
- comparePassword: sinon.stub().resolves(comparePasswordResult),
- save: sinon.stub().resolves(),
- };
- }
- return finalResult;
- },
- }),
- // Add methods to the top level too
- comparePassword: sinon.stub().resolves(comparePasswordResult),
- save: sinon.stub().resolves(),
-});
-
-describe("recoveryModule", function () {
- let deleteManyStub, saveStub, findOneStub, userCompareStub, userSaveStub, userFindOneStub;
- let req, res;
-
- beforeEach(function () {
- req = {
- body: { email: "test@test.com" },
- };
- deleteManyStub = sinon.stub(RecoveryToken, "deleteMany");
- saveStub = sinon.stub(RecoveryToken.prototype, "save");
- findOneStub = sinon.stub(RecoveryToken, "findOne");
- userCompareStub = sinon.stub(User.prototype, "comparePassword");
- userSaveStub = sinon.stub(User.prototype, "save");
- userFindOneStub = sinon.stub().resolves();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("requestRecoveryToken", function () {
- it("should return a recovery token", async function () {
- deleteManyStub.resolves();
- saveStub.resolves(mockRecoveryToken);
- const result = await requestRecoveryToken(req, res);
- expect(result.email).to.equal(mockRecoveryToken.email);
- });
-
- it("should handle an error", async function () {
- const err = new Error("Test error");
- deleteManyStub.rejects(err);
- try {
- await requestRecoveryToken(req, res);
- } catch (error) {
- expect(error).to.exist;
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("validateRecoveryToken", function () {
- it("should return a recovery token if found", async function () {
- findOneStub.resolves(mockRecoveryToken);
- const result = await validateRecoveryToken(req, res);
- expect(result).to.deep.equal(mockRecoveryToken);
- });
-
- it("should thrown an error if a token is not found", async function () {
- findOneStub.resolves(null);
- try {
- await validateRecoveryToken(req, res);
- } catch (error) {
- expect(error).to.exist;
- expect(error.message).to.equal(errorMessages.DB_TOKEN_NOT_FOUND);
- }
- });
-
- it("should handle DB errors", async function () {
- const err = new Error("Test error");
- findOneStub.rejects(err);
- try {
- await validateRecoveryToken(req, res);
- } catch (error) {
- expect(error).to.exist;
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("resetPassword", function () {
- beforeEach(function () {
- req.body = {
- password: "test",
- newPassword: "test1",
- };
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should thrown an error if a recovery token is not found", async function () {
- findOneStub.resolves(null);
- try {
- await resetPassword(req, res);
- } catch (error) {
- expect(error).to.exist;
- expect(error.message).to.equal(errorMessages.DB_TOKEN_NOT_FOUND);
- }
- });
-
- it("should throw an error if a user is not found", async function () {
- findOneStub.resolves(mockRecoveryToken);
- userFindOneStub = sinon.stub(User, "findOne").resolves(null);
- try {
- await resetPassword(req, res);
- } catch (error) {
- expect(error).to.exist;
- expect(error.message).to.equal(errorMessages.DB_USER_NOT_FOUND);
- }
- });
-
- it("should throw an error if the passwords match", async function () {
- findOneStub.resolves(mockRecoveryToken);
- saveStub.resolves();
- userFindOneStub = sinon.stub(User, "findOne").returns(createQueryChain(mockUser, true));
- try {
- await resetPassword(req, res);
- } catch (error) {
- expect(error).to.exist;
- expect(error.message).to.equal(errorMessages.DB_RESET_PASSWORD_BAD_MATCH);
- }
- });
-
- it("should return a user without password if successful", async function () {
- findOneStub.resolves(mockRecoveryToken);
- saveStub.resolves();
- userFindOneStub = sinon
- .stub(User, "findOne")
- .returns(createQueryChain(mockUser)) // First call will resolve to mockUser
- .onSecondCall()
- .returns(createQueryChain(mockUserWithoutPassword));
- const result = await resetPassword(req, res);
- expect(result).to.deep.equal(mockUserWithoutPassword);
- });
- });
-});
diff --git a/server/tests/db/settingsModule.test.js b/server/tests/db/settingsModule.test.js
deleted file mode 100755
index 9d1a80c3e..000000000
--- a/server/tests/db/settingsModule.test.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import sinon from "sinon";
-import { getAppSettings, updateAppSettings } from "../../db/mongo/modules/settingsModule.js";
-import AppSettings from "../../db/models/AppSettings.js";
-
-const mockAppSettings = {
- appName: "Test App",
-};
-
-describe("SettingsModule", function () {
- let appSettingsFindOneStub, appSettingsFindOneAndUpdateStub;
-
- beforeEach(function () {
- appSettingsFindOneStub = sinon.stub(AppSettings, "findOne");
- appSettingsFindOneAndUpdateStub = sinon.stub(AppSettings, "findOneAndUpdate");
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("getAppSettings", function () {
- it("should return app settings", async function () {
- appSettingsFindOneStub.resolves(mockAppSettings);
- const result = await getAppSettings();
- expect(result).to.deep.equal(mockAppSettings);
- });
-
- it("should handle an error", async function () {
- const err = new Error("Test error");
- appSettingsFindOneStub.throws(err);
- try {
- await getAppSettings();
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("updateAppSettings", function () {
- it("should update app settings", async function () {
- appSettingsFindOneAndUpdateStub.resolves(mockAppSettings);
- const result = await updateAppSettings(mockAppSettings);
- expect(result).to.deep.equal(mockAppSettings);
- });
-
- it("should handle an error", async function () {
- const err = new Error("Test error");
- appSettingsFindOneAndUpdateStub.throws(err);
- try {
- await updateAppSettings(mockAppSettings);
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
- });
-});
diff --git a/server/tests/db/statusPageModule.test.js b/server/tests/db/statusPageModule.test.js
deleted file mode 100755
index 54e1a287f..000000000
--- a/server/tests/db/statusPageModule.test.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import sinon from "sinon";
-import { createStatusPage, getStatusPageByUrl } from "../../db/mongo/modules/statusPageModule.js";
-import StatusPage from "../../db/models/StatusPage.js";
-import { errorMessages } from "../../utils/messages.js";
-
-describe("statusPageModule", function () {
- let statusPageFindOneStub, statusPageSaveStub, statusPageFindStub;
-
- beforeEach(function () {
- statusPageSaveStub = sinon.stub(StatusPage.prototype, "save");
- statusPageFindOneStub = sinon.stub(StatusPage, "findOne");
- statusPageFindStub = sinon.stub(StatusPage, "find");
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("createStatusPage", function () {
- it("should throw an error if a non-unique url is provided", async function () {
- statusPageFindOneStub.resolves(true);
- try {
- await createStatusPage({ url: "test" });
- } catch (error) {
- expect(error.status).to.equal(400);
- expect(error.message).to.equal(errorMessages.STATUS_PAGE_URL_NOT_UNIQUE);
- }
- });
-
- it("should handle duplicate URL errors", async function () {
- const err = new Error("test");
- err.code = 11000;
- statusPageSaveStub.rejects(err);
- try {
- await createStatusPage({ url: "test" });
- } catch (error) {
- expect(error).to.deep.equal(err);
- }
- });
-
- it("should return a status page if a unique url is provided", async function () {
- statusPageFindOneStub.resolves(null);
- statusPageFindStub.resolves([]);
- const mockStatusPage = { url: "test" };
- const statusPage = await createStatusPage(mockStatusPage);
- expect(statusPage).to.exist;
- expect(statusPage.url).to.equal(mockStatusPage.url);
- });
- });
-
- describe("getStatusPageByUrl", function () {
- it("should throw an error if a status page is not found", async function () {
- statusPageFindOneStub.resolves(null);
- try {
- await getStatusPageByUrl("test");
- } catch (error) {
- expect(error.status).to.equal(404);
- expect(error.message).to.equal(errorMessages.STATUS_PAGE_NOT_FOUND);
- }
- });
-
- it("should return a status page if a status page is found", async function () {
- const mockStatusPage = { url: "test" };
- statusPageFindOneStub.resolves(mockStatusPage);
- const statusPage = await getStatusPageByUrl(mockStatusPage.url);
- expect(statusPage).to.exist;
- expect(statusPage).to.deep.equal(mockStatusPage);
- });
- });
-});
diff --git a/server/tests/db/userModule.test.js b/server/tests/db/userModule.test.js
deleted file mode 100755
index d0f5ecb3e..000000000
--- a/server/tests/db/userModule.test.js
+++ /dev/null
@@ -1,294 +0,0 @@
-import sinon from "sinon";
-import UserModel from "../../db/models/User.js";
-import TeamModel from "../../db/models/Team.js";
-import {
- insertUser,
- getUserByEmail,
- updateUser,
- deleteUser,
- deleteTeam,
- deleteAllOtherUsers,
- getAllUsers,
- logoutUser,
-} from "../../db/mongo/modules/userModule.js";
-import { errorMessages } from "../../utils/messages.js";
-
-const mockUser = {
- email: "test@test.com",
- password: "password",
- role: ["user"],
-};
-const mockSuperUser = {
- email: "test@test.com",
- password: "password",
- role: ["superadmin"],
-};
-const imageFile = {
- image: 1,
-};
-
-describe("userModule", function () {
- let teamSaveStub,
- teamFindByIdAndDeleteStub,
- userSaveStub,
- userFindStub,
- userFindOneStub,
- userFindByIdAndUpdateStub,
- userFindByIdAndDeleteStub,
- userDeleteManyStub,
- userUpdateOneStub,
- generateAvatarImageStub,
- parseBooleanStub;
-
- beforeEach(function () {
- teamSaveStub = sinon.stub(TeamModel.prototype, "save");
- teamFindByIdAndDeleteStub = sinon.stub(TeamModel, "findByIdAndDelete");
- userSaveStub = sinon.stub(UserModel.prototype, "save");
- userFindStub = sinon.stub(UserModel, "find");
- userFindOneStub = sinon.stub(UserModel, "findOne");
- userFindByIdAndUpdateStub = sinon.stub(UserModel, "findByIdAndUpdate");
- userFindByIdAndDeleteStub = sinon.stub(UserModel, "findByIdAndDelete");
- userDeleteManyStub = sinon.stub(UserModel, "deleteMany");
- userUpdateOneStub = sinon.stub(UserModel, "updateOne");
- generateAvatarImageStub = sinon.stub().resolves({ image: 2 });
- parseBooleanStub = sinon.stub().returns(true);
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("insertUser", function () {
- it("should insert a regular user", async function () {
- userSaveStub.resolves(mockUser);
- userFindOneStub.returns({
- select: sinon.stub().returns({
- select: sinon.stub().resolves(mockUser),
- }),
- });
- const result = await insertUser(mockUser, imageFile, generateAvatarImageStub);
- expect(result).to.deep.equal(mockUser);
- });
-
- it("should insert a superadmin user", async function () {
- userSaveStub.resolves(mockSuperUser);
- userFindOneStub.returns({
- select: sinon.stub().returns({
- select: sinon.stub().resolves(mockSuperUser),
- }),
- });
- const result = await insertUser(mockSuperUser, imageFile, generateAvatarImageStub);
- expect(result).to.deep.equal(mockSuperUser);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- userSaveStub.rejects(err);
- try {
- await insertUser(mockUser, imageFile, generateAvatarImageStub);
- } catch (error) {
- expect(error).to.exist;
- expect(error).to.deep.equal(err);
- }
- });
-
- it("should handle a duplicate key error", async function () {
- const err = new Error("test error");
- err.code = 11000;
- userSaveStub.rejects(err);
- try {
- await insertUser(mockUser, imageFile, generateAvatarImageStub);
- } catch (error) {
- expect(error).to.exist;
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("getUserByEmail", function () {
- it("should return a user", async function () {
- userFindOneStub.returns({
- select: sinon.stub().resolves(mockUser),
- });
- const result = await getUserByEmail(mockUser.email);
- expect(result).to.deep.equal(mockUser);
- });
- });
-
- describe("getUserByEmail", function () {
- it("throw an error if a user is not found", async function () {
- userFindOneStub.returns({
- select: sinon.stub().resolves(null),
- });
- try {
- await getUserByEmail(mockUser.email);
- } catch (error) {
- expect(error.message).to.equal(errorMessages.DB_USER_NOT_FOUND);
- }
- });
- });
-
- describe("updateUser", function () {
- let req, res;
-
- beforeEach(function () {
- req = {
- params: {
- userId: "testId",
- },
- body: {
- deleteProfileImage: "false",
- email: "test@test.com",
- },
- file: {
- buffer: "test",
- mimetype: "test",
- },
- };
- res = {};
- });
-
- afterEach(function () {});
-
- it("should update a user", async function () {
- parseBooleanStub.returns(false);
- userFindByIdAndUpdateStub.returns({
- select: sinon.stub().returns({
- select: sinon.stub().resolves(mockUser),
- }),
- });
- const result = await updateUser(req, res, parseBooleanStub, generateAvatarImageStub);
- expect(result).to.deep.equal(mockUser);
- });
-
- it("should delete a user profile image", async function () {
- req.body.deleteProfileImage = "true";
- userFindByIdAndUpdateStub.returns({
- select: sinon.stub().returns({
- select: sinon.stub().resolves(mockUser),
- }),
- });
- const result = await updateUser(req, res, parseBooleanStub, generateAvatarImageStub);
- expect(result).to.deep.equal(mockUser);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- userFindByIdAndUpdateStub.throws(err);
- try {
- await updateUser(req, res, parseBooleanStub, generateAvatarImageStub);
- } catch (error) {
- expect(error).to.exist;
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("deleteUser", function () {
- it("should return a deleted user", async function () {
- userFindByIdAndDeleteStub.resolves(mockUser);
- const result = await deleteUser("testId");
- expect(result).to.deep.equal(mockUser);
- });
-
- it("should throw an error if a user is not found", async function () {
- try {
- await deleteUser("testId");
- } catch (error) {
- expect(error).to.exist;
- expect(error.message).to.equal(errorMessages.DB_USER_NOT_FOUND);
- }
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- userFindByIdAndDeleteStub.throws(err);
- try {
- await deleteUser("testId");
- } catch (error) {
- expect(error).to.exist;
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("deleteTeam", function () {
- it("should return true if team deleted", async function () {
- teamFindByIdAndDeleteStub.resolves();
- const result = await deleteTeam("testId");
- expect(result).to.equal(true);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- teamFindByIdAndDeleteStub.throws(err);
- try {
- await deleteTeam("testId");
- } catch (error) {
- expect(error).to.exist;
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("deleteAllOtherUsers", function () {
- it("should return true if all other users deleted", async function () {
- userDeleteManyStub.resolves(true);
- const result = await deleteAllOtherUsers();
- expect(result).to.equal(true);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- userDeleteManyStub.throws(err);
- try {
- await deleteAllOtherUsers();
- } catch (error) {
- expect(error).to.exist;
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("getAllUsers", function () {
- it("should return all users", async function () {
- userFindStub.returns({
- select: sinon.stub().returns({
- select: sinon.stub().resolves([mockUser]),
- }),
- });
- const result = await getAllUsers();
- expect(result).to.deep.equal([mockUser]);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- userFindStub.throws(err);
- try {
- await getAllUsers();
- } catch (error) {
- expect(error).to.exist;
- expect(error).to.deep.equal(err);
- }
- });
- });
-
- describe("logoutUser", function () {
- it("should return true if user logged out", async function () {
- userUpdateOneStub.resolves(true);
- const result = await logoutUser("testId");
- expect(result).to.equal(true);
- });
-
- it("should handle an error", async function () {
- const err = new Error("test error");
- userUpdateOneStub.throws(err);
- try {
- await logoutUser("testId");
- } catch (error) {
- expect(error).to.exist;
- expect(error).to.deep.equal(err);
- }
- });
- });
-});
diff --git a/server/tests/services/emailService.test.js b/server/tests/services/emailService.test.js
deleted file mode 100755
index cca86896a..000000000
--- a/server/tests/services/emailService.test.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import sinon from "sinon";
-import EmailService from "../../service/emailService.js";
-
-describe("EmailService - Constructor", function () {
- let settingsServiceMock;
- let fsMock;
- let pathMock;
- let compileMock;
- let mjml2htmlMock;
- let nodemailerMock;
- let loggerMock;
-
- beforeEach(function () {
- settingsServiceMock = {
- getSettings: sinon.stub().returns({
- systemEmailHost: "smtp.example.com",
- systemEmailPort: 465,
- systemEmailAddress: "test@example.com",
- systemEmailPassword: "password",
- }),
- };
-
- fsMock = {
- readFileSync: sinon.stub().returns(""),
- };
-
- pathMock = {
- join: sinon.stub().callsFake((...args) => args.join("/")),
- };
-
- compileMock = sinon.stub().returns(() => "");
-
- mjml2htmlMock = sinon.stub().returns({ html: "" });
-
- nodemailerMock = {
- createTransport: sinon.stub().returns({
- sendMail: sinon.stub().resolves({ messageId: "12345" }),
- }),
- };
-
- loggerMock = {
- error: sinon.stub(),
- };
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should initialize template loaders and email transporter", function () {
- const emailService = new EmailService(settingsServiceMock, fsMock, pathMock, compileMock, mjml2htmlMock, nodemailerMock, loggerMock);
-
- // Verify that the settingsService is assigned correctly
- expect(emailService.settingsService).to.equal(settingsServiceMock);
-
- // Verify that the template loaders are initialized
- expect(emailService.templateLookup.welcomeEmailTemplate).to.be.a("function");
- expect(emailService.templateLookup.employeeActivationTemplate).to.be.a("function");
-
- // Verify that the email transporter is initialized
- expect(nodemailerMock.createTransport.calledOnce).to.be.true;
- const emailConfig = nodemailerMock.createTransport.getCall(0).args[0];
- expect(emailConfig).to.deep.equal({
- host: "smtp.example.com",
- port: 465,
- secure: true,
- auth: {
- user: "test@example.com",
- pass: "password",
- },
- });
- });
-
- it("should have undefined templates if FS fails", function () {
- fsMock = {
- readFileSync: sinon.stub().throws(new Error("File read error")),
- };
- const emailService = new EmailService(settingsServiceMock, fsMock, pathMock, compileMock, mjml2htmlMock, nodemailerMock, loggerMock);
- expect(loggerMock.error.called).to.be.true;
- expect(loggerMock.error.firstCall.args[0].message).to.equal("File read error");
- });
-});
-
-describe("EmailService - buildAndSendEmail", function () {
- let settingsServiceMock;
- let fsMock;
- let pathMock;
- let compileMock;
- let mjml2htmlMock;
- let nodemailerMock;
- let loggerMock;
- let emailService;
-
- beforeEach(function () {
- settingsServiceMock = {
- getSettings: sinon.stub().returns({
- systemEmailHost: "smtp.example.com",
- systemEmailPort: 465,
- systemEmailAddress: "test@example.com",
- systemEmailPassword: "password",
- }),
- };
-
- fsMock = {
- readFileSync: sinon.stub().returns(""),
- };
-
- pathMock = {
- join: sinon.stub().callsFake((...args) => args.join("/")),
- };
-
- compileMock = sinon.stub().returns(() => "");
-
- mjml2htmlMock = sinon.stub().returns({ html: "" });
-
- nodemailerMock = {
- createTransport: sinon.stub().returns({
- sendMail: sinon.stub().resolves({ messageId: "12345" }),
- }),
- };
-
- loggerMock = {
- error: sinon.stub(),
- };
-
- emailService = new EmailService(settingsServiceMock, fsMock, pathMock, compileMock, mjml2htmlMock, nodemailerMock, loggerMock);
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should build and send email successfully", async function () {
- const messageId = await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome");
-
- expect(messageId).to.equal("12345");
- expect(nodemailerMock.createTransport().sendMail.calledOnce).to.be.true;
- });
-
- it("should log error if building HTML fails", async function () {
- mjml2htmlMock.throws(new Error("MJML error"));
-
- const messageId = await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome");
- expect(loggerMock.error.calledOnce).to.be.true;
- expect(loggerMock.error.getCall(0).args[0].message).to.equal("MJML error");
- });
-
- it("should log error if sending email fails", async function () {
- nodemailerMock.createTransport().sendMail.rejects(new Error("SMTP error"));
- await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome");
- expect(loggerMock.error.calledOnce).to.be.true;
- expect(loggerMock.error.getCall(0).args[0].message).to.equal("SMTP error");
- });
-
- it("should log error if both building HTML and sending email fail", async function () {
- mjml2htmlMock.throws(new Error("MJML error"));
- nodemailerMock.createTransport().sendMail.rejects(new Error("SMTP error"));
-
- const messageId = await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome");
-
- expect(messageId).to.be.undefined;
- expect(loggerMock.error.calledTwice).to.be.true;
- expect(loggerMock.error.getCall(0).args[0].message).to.equal("MJML error");
- expect(loggerMock.error.getCall(1).args[0].message).to.equal("SMTP error");
- });
-
- it("should log an error if buildHtml fails", async function () {});
-});
diff --git a/server/tests/services/jobQueue.test.js b/server/tests/services/jobQueue.test.js
deleted file mode 100755
index 155f9119e..000000000
--- a/server/tests/services/jobQueue.test.js
+++ /dev/null
@@ -1,813 +0,0 @@
-import sinon from "sinon";
-import JobQueue from "../../service/jobQueue.js";
-import { log } from "console";
-
-class QueueStub {
- constructor(queueName, options) {
- this.queueName = queueName;
- this.options = options;
- this.workers = [];
- this.jobs = [];
- }
-
- // Add any methods that are expected to be called on the Queue instance
- add(job) {
- this.jobs.push(job);
- }
-
- removeRepeatable(id) {
- const removedJob = this.jobs.find((job) => job.data._id === id);
- this.jobs = this.jobs.filter((job) => job.data._id !== id);
- if (removedJob) {
- return true;
- }
- return false;
- }
-
- getRepeatableJobs() {
- return this.jobs;
- }
- async getJobs() {
- return this.jobs;
- }
-
- async pause() {
- return true;
- }
-
- async obliterate() {
- return true;
- }
-}
-
-class WorkerStub {
- constructor(QUEUE_NAME, workerTask) {
- this.queueName = QUEUE_NAME;
- this.workerTask = async () => workerTask({ data: { _id: 1 } });
- }
-
- async close() {
- return true;
- }
-}
-
-describe("JobQueue", function () {
- let settingsService, logger, db, networkService, statusService, notificationService, jobQueue;
-
- beforeEach(async function () {
- settingsService = { getSettings: sinon.stub() };
- statusService = { updateStatus: sinon.stub() };
- notificationService = { handleNotifications: sinon.stub() };
-
- logger = { error: sinon.stub(), info: sinon.stub() };
- db = {
- getAllMonitors: sinon.stub().returns([]),
- getMaintenanceWindowsByMonitorId: sinon.stub().returns([]),
- };
- networkService = { getStatus: sinon.stub() };
- jobQueue = await JobQueue.createJobQueue(db, networkService, statusService, notificationService, settingsService, logger, QueueStub, WorkerStub);
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("createJobQueue", function () {
- it("should create a new JobQueue and add jobs for active monitors", async function () {
- db.getAllMonitors.returns([
- { id: 1, isActive: true },
- { id: 2, isActive: true },
- ]);
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- // There should be double the jobs, as one is meant to be instantly executed
- // And one is meant to be enqueued
- expect(jobQueue.queue.jobs.length).to.equal(4);
- });
-
- it("should reject with an error if an error occurs", async function () {
- db.getAllMonitors.throws("Error");
- try {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- } catch (error) {
- expect(error.service).to.equal("JobQueue");
- expect(error.method).to.equal("createJobQueue");
- }
- });
-
- it("should reject with an error if an error occurs, should not overwrite error data", async function () {
- const error = new Error("Error");
- error.service = "otherService";
- error.method = "otherMethod";
- db.getAllMonitors.throws(error);
-
- try {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- } catch (error) {
- expect(error.service).to.equal("otherService");
- expect(error.method).to.equal("otherMethod");
- }
- });
- });
-
- describe("Constructor", function () {
- it("should construct a new JobQueue with default port and host if not provided", async function () {
- settingsService.getSettings.returns({});
-
- expect(jobQueue.connection.host).to.equal("127.0.0.1");
- expect(jobQueue.connection.port).to.equal(6379);
- });
-
- it("should construct a new JobQueue with provided port and host", async function () {
- settingsService.getSettings.returns({ redisHost: "localhost", redisPort: 1234 });
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- expect(jobQueue.connection.host).to.equal("localhost");
- expect(jobQueue.connection.port).to.equal(1234);
- });
- });
-
- describe("isMaintenanceWindow", function () {
- it("should throw an error if error occurs", async function () {
- db.getMaintenanceWindowsByMonitorId.throws("Error");
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- try {
- jobQueue.isInMaintenanceWindow(1);
- } catch (error) {
- expect(error.service).to.equal("JobQueue");
- expect(error.method).to.equal("createWorker");
- }
- });
-
- it("should return true if in maintenance window with no repeat", async function () {
- db.getMaintenanceWindowsByMonitorId.returns([
- {
- active: true,
- start: new Date(Date.now() - 1000).toISOString(),
- end: new Date(Date.now() + 1000).toISOString(),
- repeat: 0,
- },
- ]);
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- const inWindow = await jobQueue.isInMaintenanceWindow(1);
- expect(inWindow).to.be.true;
- });
-
- it("should return true if in maintenance window with repeat", async function () {
- db.getMaintenanceWindowsByMonitorId.returns([
- {
- active: true,
- start: new Date(Date.now() - 10000).toISOString(),
- end: new Date(Date.now() - 5000).toISOString(),
- repeat: 1000,
- },
- ]);
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- const inWindow = await jobQueue.isInMaintenanceWindow(1);
- expect(inWindow).to.be.true;
- });
-
- it("should return false if in end < start", async function () {
- db.getMaintenanceWindowsByMonitorId.returns([
- {
- active: true,
- start: new Date(Date.now() - 5000).toISOString(),
- end: new Date(Date.now() - 10000).toISOString(),
- repeat: 1000,
- },
- ]);
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- const inWindow = await jobQueue.isInMaintenanceWindow(1);
- expect(inWindow).to.be.false;
- });
-
- it("should return false if not in maintenance window", async function () {
- db.getMaintenanceWindowsByMonitorId.returns([
- {
- active: false,
- start: new Date(Date.now() - 5000).toISOString(),
- end: new Date(Date.now() - 10000).toISOString(),
- repeat: 1000,
- },
- ]);
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- const inWindow = await jobQueue.isInMaintenanceWindow(1);
- expect(inWindow).to.be.false;
- });
- });
-
- describe("createJobHandler", function () {
- it("resolve to an error if an error is thrown within", async function () {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- jobQueue.isInMaintenanceWindow = sinon.stub().throws("Error");
- try {
- const handler = jobQueue.createJobHandler();
- await handler({ data: { _id: 1 } });
- } catch (error) {
- expect(error.service).to.equal("JobQueue");
- expect(error.details).to.equal(`Error processing job 1: Error`);
- }
- });
-
- it("should log info if job is in maintenance window", async function () {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- jobQueue.isInMaintenanceWindow = sinon.stub().returns(true);
- const handler = jobQueue.createJobHandler();
- await handler({ data: { _id: 1 } });
- expect(logger.info.calledOnce).to.be.true;
- expect(logger.info.firstCall.args[0].message).to.equal("Monitor 1 is in maintenance window");
- });
-
- it("should return if status has not changed", async function () {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- jobQueue.isInMaintenanceWindow = sinon.stub().returns(false);
- statusService.updateStatus = sinon.stub().returns({ statusChanged: false });
- const handler = jobQueue.createJobHandler();
- await handler({ data: { _id: 1 } });
- expect(jobQueue.notificationService.handleNotifications.notCalled).to.be.true;
- });
-
- it("should return if status has changed, but prevStatus was undefined (monitor paused)", async function () {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- jobQueue.isInMaintenanceWindow = sinon.stub().returns(false);
- statusService.updateStatus = sinon.stub().returns({ statusChanged: true, prevStatus: undefined });
- const handler = jobQueue.createJobHandler();
- await handler({ data: { _id: 1 } });
- expect(jobQueue.notificationService.handleNotifications.notCalled).to.be.true;
- });
-
- it("should call notification service if status changed and monitor was not paused", async function () {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- jobQueue.isInMaintenanceWindow = sinon.stub().returns(false);
- statusService.updateStatus = sinon.stub().returns({ statusChanged: true, prevStatus: false });
- const handler = jobQueue.createJobHandler();
- await handler({ data: { _id: 1 } });
- expect(jobQueue.notificationService.handleNotifications.calledOnce).to.be.true;
- });
- });
-
- describe("getWorkerStats", function () {
- it("should throw an error if getRepeatable Jobs fails", async function () {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- jobQueue.queue.getRepeatableJobs = async () => {
- throw new Error("Error");
- };
- try {
- const stats = await jobQueue.getWorkerStats();
- } catch (error) {
- expect(error.service).to.equal("JobQueue");
- expect(error.method).to.equal("getWorkerStats");
- }
- });
-
- it("should throw an error if getRepeatable Jobs fails but respect existing error data", async function () {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- jobQueue.queue.getRepeatableJobs = async () => {
- const error = new Error("Existing Error");
- error.service = "otherService";
- error.method = "otherMethod";
- throw error;
- };
- try {
- await jobQueue.getWorkerStats();
- } catch (error) {
- expect(error.service).to.equal("otherService");
- expect(error.method).to.equal("otherMethod");
- }
- });
- });
-
- describe("scaleWorkers", function () {
- it("should scale workers to 5 if no workers", async function () {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- expect(jobQueue.workers.length).to.equal(5);
- });
-
- it("should scale workers up", async function () {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- jobQueue.scaleWorkers({
- load: 100,
- jobs: Array.from({ length: 100 }, (_, i) => i + 1),
- });
- expect(jobQueue.workers.length).to.equal(20);
- });
-
- it("should scale workers down, even with error of worker.close fails", async function () {
- WorkerStub.prototype.close = async () => {
- throw new Error("Error");
- };
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- await jobQueue.scaleWorkers({
- load: 100,
- jobs: Array.from({ length: 100 }, (_, i) => i + 1),
- });
-
- const res = await jobQueue.scaleWorkers({
- load: 0,
- jobs: [],
- });
- expect(jobQueue.workers.length).to.equal(5);
- });
-
- it("should scale workers down", async function () {
- WorkerStub.prototype.close = async () => {
- return true;
- };
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- await jobQueue.scaleWorkers({
- load: 40,
- jobs: Array.from({ length: 40 }, (_, i) => i + 1),
- });
-
- const res = await jobQueue.scaleWorkers({
- load: 0,
- jobs: [],
- });
- expect(jobQueue.workers.length).to.equal(5);
- });
-
- it("should return false if scaling doesn't happen", async function () {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- const res = await jobQueue.scaleWorkers({ load: 5 });
- expect(jobQueue.workers.length).to.equal(5);
- expect(res).to.be.false;
- });
- });
-
- describe("getJobs", function () {
- it("should return jobs", async function () {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- const jobs = await jobQueue.getJobs();
- expect(jobs.length).to.equal(0);
- });
-
- it("should throw an error if getRepeatableJobs fails", async function () {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- try {
- jobQueue.queue.getRepeatableJobs = async () => {
- throw new Error("error");
- };
-
- await jobQueue.getJobs(true);
- } catch (error) {
- expect(error.service).to.equal("JobQueue");
- expect(error.method).to.equal("getJobs");
- }
- });
-
- it("should throw an error if getRepeatableJobs fails but respect existing error data", async function () {
- const jobQueue = await JobQueue.createJobQueue(
- db,
- networkService,
- statusService,
- notificationService,
- settingsService,
- logger,
- QueueStub,
- WorkerStub
- );
- try {
- jobQueue.queue.getRepeatableJobs = async () => {
- const error = new Error("Existing error");
- error.service = "otherService";
- error.method = "otherMethod";
- throw error;
- };
-
- await jobQueue.getJobs(true);
- } catch (error) {
- expect(error.service).to.equal("otherService");
- expect(error.method).to.equal("otherMethod");
- }
- });
- });
-
- describe("getJobStats", function () {
- it("should return job stats for no jobs", async function () {
- const jobStats = await jobQueue.getJobStats();
- expect(jobStats).to.deep.equal({ jobs: [], workers: 5 });
- });
-
- it("should return job stats for jobs", async function () {
- jobQueue.queue.getJobs = async () => {
- return [{ data: { url: "test" }, getState: async () => "completed" }];
- };
- const jobStats = await jobQueue.getJobStats();
- expect(jobStats).to.deep.equal({
- jobs: [{ url: "test", state: "completed" }],
- workers: 5,
- });
- });
-
- it("should reject with an error if mapping jobs fails", async function () {
- jobQueue.queue.getJobs = async () => {
- return [
- {
- data: { url: "test" },
- getState: async () => {
- throw new Error("Mapping Error");
- },
- },
- ];
- };
- try {
- await jobQueue.getJobStats();
- } catch (error) {
- expect(error.message).to.equal("Mapping Error");
- expect(error.service).to.equal("JobQueue");
- expect(error.method).to.equal("getJobStats");
- }
- });
-
- it("should reject with an error if mapping jobs fails but respect existing error data", async function () {
- jobQueue.queue.getJobs = async () => {
- return [
- {
- data: { url: "test" },
- getState: async () => {
- const error = new Error("Mapping Error");
- error.service = "otherService";
- error.method = "otherMethod";
- throw error;
- },
- },
- ];
- };
- try {
- await jobQueue.getJobStats();
- } catch (error) {
- expect(error.message).to.equal("Mapping Error");
- expect(error.service).to.equal("otherService");
- expect(error.method).to.equal("otherMethod");
- }
- });
- });
-
- describe("addJob", function () {
- it("should add a job to the queue", async function () {
- jobQueue.addJob("test", { url: "test" });
- expect(jobQueue.queue.jobs.length).to.equal(1);
- });
-
- it("should reject with an error if adding fails", async function () {
- jobQueue.queue.add = async () => {
- throw new Error("Error adding job");
- };
- try {
- await jobQueue.addJob("test", { url: "test" });
- } catch (error) {
- expect(error.message).to.equal("Error adding job");
- expect(error.service).to.equal("JobQueue");
- expect(error.method).to.equal("addJob");
- }
- });
-
- it("should reject with an error if adding fails but respect existing error data", async function () {
- jobQueue.queue.add = async () => {
- const error = new Error("Error adding job");
- error.service = "otherService";
- error.method = "otherMethod";
- throw error;
- };
- try {
- await jobQueue.addJob("test", { url: "test" });
- } catch (error) {
- expect(error.message).to.equal("Error adding job");
- expect(error.service).to.equal("otherService");
- expect(error.method).to.equal("otherMethod");
- }
- });
- });
-
- describe("deleteJob", function () {
- it("should delete a job from the queue", async function () {
- jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] });
- jobQueue.scaleWorkers = sinon.stub();
- const monitor = { _id: 1 };
- const job = { data: monitor };
- jobQueue.queue.jobs = [job];
- await jobQueue.deleteJob(monitor);
- // expect(jobQueue.queue.jobs.length).to.equal(0);
- // expect(logger.info.calledOnce).to.be.true;
- // expect(jobQueue.getWorkerStats.calledOnce).to.be.true;
- // expect(jobQueue.scaleWorkers.calledOnce).to.be.true;
- });
-
- it("should log an error if job is not found", async function () {
- jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] });
- jobQueue.scaleWorkers = sinon.stub();
- const monitor = { _id: 1 };
- const job = { data: monitor };
- jobQueue.queue.jobs = [job];
- await jobQueue.deleteJob({ id_: 2 });
- expect(logger.error.calledOnce).to.be.true;
- });
-
- it("should reject with an error if removeRepeatable fails", async function () {
- jobQueue.queue.removeRepeatable = async () => {
- const error = new Error("removeRepeatable error");
- throw error;
- };
-
- try {
- await jobQueue.deleteJob({ _id: 1 });
- } catch (error) {
- expect(error.message).to.equal("removeRepeatable error");
- expect(error.service).to.equal("JobQueue");
- expect(error.method).to.equal("deleteJob");
- }
- });
-
- it("should reject with an error if removeRepeatable fails but respect existing error data", async function () {
- jobQueue.queue.removeRepeatable = async () => {
- const error = new Error("removeRepeatable error");
- error.service = "otherService";
- error.method = "otherMethod";
- throw error;
- };
-
- try {
- await jobQueue.deleteJob({ _id: 1 });
- } catch (error) {
- expect(error.message).to.equal("removeRepeatable error");
- expect(error.service).to.equal("otherService");
- expect(error.method).to.equal("otherMethod");
- }
- });
- });
-
- describe("getMetrics", function () {
- it("should return metrics for the job queue", async function () {
- jobQueue.queue.getWaitingCount = async () => 1;
- jobQueue.queue.getActiveCount = async () => 2;
- jobQueue.queue.getCompletedCount = async () => 3;
- jobQueue.queue.getFailedCount = async () => 4;
- jobQueue.queue.getDelayedCount = async () => 5;
- jobQueue.queue.getRepeatableJobs = async () => [1, 2, 3];
- const metrics = await jobQueue.getMetrics();
- expect(metrics).to.deep.equal({
- waiting: 1,
- active: 2,
- completed: 3,
- failed: 4,
- delayed: 5,
- repeatableJobs: 3,
- });
- });
-
- it("should log an error if metrics operations fail", async function () {
- jobQueue.queue.getWaitingCount = async () => {
- throw new Error("Error");
- };
- await jobQueue.getMetrics();
- expect(logger.error.calledOnce).to.be.true;
- expect(logger.error.firstCall.args[0].message).to.equal("Error");
- });
- });
-
- describe("obliterate", function () {
- it("should return true if obliteration is successful", async function () {
- jobQueue.queue.pause = async () => true;
- jobQueue.getJobs = async () => [{ key: 1, id: 1 }];
- jobQueue.queue.removeRepeatableByKey = async () => true;
- jobQueue.queue.remove = async () => true;
- jobQueue.queue.obliterate = async () => true;
- const obliteration = await jobQueue.obliterate();
- expect(obliteration).to.be.true;
- });
-
- it("should throw an error if obliteration fails", async function () {
- jobQueue.getMetrics = async () => {
- throw new Error("Error");
- };
-
- try {
- await jobQueue.obliterate();
- } catch (error) {
- expect(error.service).to.equal("JobQueue");
- expect(error.method).to.equal("obliterate");
- }
- });
-
- it("should throw an error if obliteration fails but respect existing error data", async function () {
- jobQueue.getMetrics = async () => {
- const error = new Error("Error");
- error.service = "otherService";
- error.method = "otherMethod";
- throw error;
- };
-
- try {
- await jobQueue.obliterate();
- } catch (error) {
- expect(error.service).to.equal("otherService");
- expect(error.method).to.equal("otherMethod");
- }
- });
- });
-});
diff --git a/server/tests/services/networkService.test.js b/server/tests/services/networkService.test.js
deleted file mode 100755
index cb48ed9b2..000000000
--- a/server/tests/services/networkService.test.js
+++ /dev/null
@@ -1,439 +0,0 @@
-import sinon from "sinon";
-import NetworkService from "../../service/networkService.js";
-import { expect } from "chai";
-import http from "http";
-import { errorMessages } from "../../utils/messages.js";
-describe("Network Service", function () {
- let axios, ping, Docker, logger, networkService;
-
- beforeEach(function () {
- axios = {
- get: sinon.stub().resolves({
- data: { foo: "bar" },
- status: 200,
- }),
- };
- Docker = class {
- listContainers = sinon.stub().resolves([
- {
- Names: ["http://test.com"],
- Id: "http://test.com",
- },
- ]);
- getContainer = sinon.stub().returns({
- inspect: sinon.stub().resolves({ State: { Status: "running" } }),
- });
- };
- ping = {
- promise: {
- probe: sinon.stub().resolves({ response: { alive: true }, responseTime: 100, alive: true }),
- },
- };
- logger = { error: sinon.stub() };
- networkService = new NetworkService(axios, ping, logger, http, Docker);
- });
-
- describe("constructor", function () {
- it("should create a new NetworkService instance", function () {
- const networkService = new NetworkService();
- expect(networkService).to.be.an.instanceOf(NetworkService);
- });
- });
-
- describe("timeRequest", function () {
- it("should time an asynchronous operation", async function () {
- const operation = sinon.stub().resolves("success");
- const { response, responseTime } = await networkService.timeRequest(operation);
- expect(response).to.equal("success");
- expect(responseTime).to.be.a("number");
- });
-
- it("should handle errors if operation throws error", async function () {
- const error = new Error("Test error");
- const operation = sinon.stub().throws(error);
- const { response, responseTime } = await networkService.timeRequest(operation);
- expect(response).to.be.null;
- expect(responseTime).to.be.a("number");
- expect(error.message).to.equal("Test error");
- });
- });
-
- describe("requestPing", function () {
- it("should return a response object if ping successful", async function () {
- const pingResult = await networkService.requestPing({
- data: { url: "http://test.com", _id: "123" },
- });
- expect(pingResult.monitorId).to.equal("123");
- expect(pingResult.type).to.equal("ping");
- expect(pingResult.responseTime).to.be.a("number");
- expect(pingResult.status).to.be.true;
- });
-
- it("should return a response object if ping unsuccessful", async function () {
- const error = new Error("Test error");
- networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error });
- const pingResult = await networkService.requestPing({
- data: { url: "http://test.com", _id: "123" },
- });
- expect(pingResult.monitorId).to.equal("123");
- expect(pingResult.type).to.equal("ping");
- expect(pingResult.responseTime).to.be.a("number");
- expect(pingResult.status).to.be.false;
- expect(pingResult.code).to.equal(networkService.PING_ERROR);
- });
-
- it("should throw an error if ping cannot resolve", async function () {
- const error = new Error("test error");
- networkService.timeRequest = sinon.stub().throws(error);
- try {
- await networkService.requestPing({
- data: { url: "http://test.com", _id: "123" },
- });
- } catch (error) {
- expect(error).to.exist;
- expect(error.method).to.equal("requestPing");
- }
- });
- });
-
- describe("requestHttp", function () {
- it("should return a response object if http successful", async function () {
- const job = { data: { url: "http://test.com", _id: "123", type: "http" } };
- const httpResult = await networkService.requestHttp(job);
- expect(httpResult.monitorId).to.equal("123");
- expect(httpResult.type).to.equal("http");
- expect(httpResult.responseTime).to.be.a("number");
- expect(httpResult.status).to.be.true;
- });
-
- it("should return a response object if http unsuccessful", async function () {
- const error = new Error("Test error");
- error.response = { status: 404 };
- networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error });
- const job = { data: { url: "http://test.com", _id: "123", type: "http" } };
- const httpResult = await networkService.requestHttp(job);
- expect(httpResult.monitorId).to.equal("123");
- expect(httpResult.type).to.equal("http");
- expect(httpResult.responseTime).to.be.a("number");
- expect(httpResult.status).to.be.false;
- expect(httpResult.code).to.equal(404);
- });
-
- it("should return a response object if http unsuccessful with unknown code", async function () {
- const error = new Error("Test error");
- error.response = {};
- networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error });
- const job = { data: { url: "http://test.com", _id: "123", type: "http" } };
- const httpResult = await networkService.requestHttp(job);
- expect(httpResult.monitorId).to.equal("123");
- expect(httpResult.type).to.equal("http");
- expect(httpResult.responseTime).to.be.a("number");
- expect(httpResult.status).to.be.false;
- expect(httpResult.code).to.equal(networkService.NETWORK_ERROR);
- });
-
- it("should throw an error if an error occurs", async function () {
- const error = new Error("test error");
- networkService.timeRequest = sinon.stub().throws(error);
- try {
- await networkService.requestHttp({
- data: { url: "http://test.com", _id: "123" },
- });
- } catch (error) {
- expect(error).to.exist;
- expect(error.method).to.equal("requestHttp");
- }
- });
- });
-
- describe("requestPagespeed", function () {
- it("should return a response object if pagespeed successful", async function () {
- const job = { data: { url: "http://test.com", _id: "123", type: "pagespeed" } };
- const pagespeedResult = await networkService.requestPagespeed(job);
- expect(pagespeedResult.monitorId).to.equal("123");
- expect(pagespeedResult.type).to.equal("pagespeed");
- expect(pagespeedResult.responseTime).to.be.a("number");
- expect(pagespeedResult.status).to.be.true;
- });
-
- it("should return a response object if pagespeed unsuccessful", async function () {
- const error = new Error("Test error");
- error.response = { status: 404 };
- networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error });
- const job = { data: { url: "http://test.com", _id: "123", type: "pagespeed" } };
- const pagespeedResult = await networkService.requestPagespeed(job);
- expect(pagespeedResult.monitorId).to.equal("123");
- expect(pagespeedResult.type).to.equal("pagespeed");
- expect(pagespeedResult.responseTime).to.be.a("number");
- expect(pagespeedResult.status).to.be.false;
- expect(pagespeedResult.code).to.equal(404);
- });
-
- it("should return a response object if pagespeed unsuccessful with an unknown code", async function () {
- const error = new Error("Test error");
- error.response = {};
- networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error });
- const job = { data: { url: "http://test.com", _id: "123", type: "pagespeed" } };
- const pagespeedResult = await networkService.requestPagespeed(job);
- expect(pagespeedResult.monitorId).to.equal("123");
- expect(pagespeedResult.type).to.equal("pagespeed");
- expect(pagespeedResult.responseTime).to.be.a("number");
- expect(pagespeedResult.status).to.be.false;
- expect(pagespeedResult.code).to.equal(networkService.NETWORK_ERROR);
- });
-
- it("should throw an error if pagespeed cannot resolve", async function () {
- const error = new Error("test error");
- networkService.timeRequest = sinon.stub().throws(error);
- try {
- await networkService.requestPagespeed({
- data: { url: "http://test.com", _id: "123" },
- });
- } catch (error) {
- expect(error).to.exist;
- expect(error.method).to.equal("requestPagespeed");
- }
- });
- });
-
- describe("requestHardware", function () {
- it("should return a response object if hardware successful", async function () {
- const job = { data: { url: "http://test.com", _id: "123", type: "hardware" } };
- const httpResult = await networkService.requestHardware(job);
- expect(httpResult.monitorId).to.equal("123");
- expect(httpResult.type).to.equal("hardware");
- expect(httpResult.responseTime).to.be.a("number");
- expect(httpResult.status).to.be.true;
- });
-
- it("should return a response object if hardware successful and job has a secret", async function () {
- const job = {
- data: {
- url: "http://test.com",
- _id: "123",
- type: "hardware",
- secret: "my_secret",
- },
- };
- const httpResult = await networkService.requestHardware(job);
- expect(httpResult.monitorId).to.equal("123");
- expect(httpResult.type).to.equal("hardware");
- expect(httpResult.responseTime).to.be.a("number");
- expect(httpResult.status).to.be.true;
- });
-
- it("should return a response object if hardware unsuccessful", async function () {
- const error = new Error("Test error");
- error.response = { status: 404 };
- networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error });
- const job = { data: { url: "http://test.com", _id: "123", type: "hardware" } };
- const httpResult = await networkService.requestHardware(job);
- expect(httpResult.monitorId).to.equal("123");
- expect(httpResult.type).to.equal("hardware");
- expect(httpResult.responseTime).to.be.a("number");
- expect(httpResult.status).to.be.false;
- expect(httpResult.code).to.equal(404);
- });
-
- it("should return a response object if hardware unsuccessful with unknown code", async function () {
- const error = new Error("Test error");
- error.response = {};
- networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error });
- const job = { data: { url: "http://test.com", _id: "123", type: "hardware" } };
- const httpResult = await networkService.requestHardware(job);
- expect(httpResult.monitorId).to.equal("123");
- expect(httpResult.type).to.equal("hardware");
- expect(httpResult.responseTime).to.be.a("number");
- expect(httpResult.status).to.be.false;
- expect(httpResult.code).to.equal(networkService.NETWORK_ERROR);
- });
-
- it("should throw an error if hardware cannot resolve", async function () {
- const error = new Error("test error");
- networkService.timeRequest = sinon.stub().throws(error);
- try {
- await networkService.requestHardware({
- data: { url: "http://test.com", _id: "123" },
- });
- } catch (error) {
- expect(error).to.exist;
- expect(error.method).to.equal("requestHardware");
- }
- });
- });
-
- describe("requestDocker", function () {
- it("should return a response object if docker successful", async function () {
- const job = { data: { url: "http://test.com", _id: "123", type: "docker" } };
- const dockerResult = await networkService.requestDocker(job);
- expect(dockerResult.monitorId).to.equal("123");
- expect(dockerResult.type).to.equal("docker");
- expect(dockerResult.responseTime).to.be.a("number");
- expect(dockerResult.status).to.be.true;
- });
-
- it("should return a response object with status false if container not running", async function () {
- Docker = class {
- listContainers = sinon.stub().resolves([
- {
- Names: ["/my_container"],
- Id: "abc123",
- },
- ]);
- getContainer = sinon.stub().returns({
- inspect: sinon.stub().resolves({ State: { Status: "stopped" } }),
- });
- };
- networkService = new NetworkService(axios, ping, logger, http, Docker);
- const job = { data: { url: "abc123", _id: "123", type: "docker" } };
- const dockerResult = await networkService.requestDocker(job);
- expect(dockerResult.status).to.be.false;
- expect(dockerResult.code).to.equal(200);
- });
-
- it("should handle an error when fetching the container", async function () {
- Docker = class {
- listContainers = sinon.stub().resolves([
- {
- Names: ["/my_container"],
- Id: "abc123",
- },
- ]);
- getContainer = sinon.stub().returns({
- inspect: sinon.stub().throws(new Error("test error")),
- });
- };
- networkService = new NetworkService(axios, ping, logger, http, Docker);
- const job = { data: { url: "abc123", _id: "123", type: "docker" } };
- const dockerResult = await networkService.requestDocker(job);
- expect(dockerResult.status).to.be.false;
- expect(dockerResult.code).to.equal(networkService.NETWORK_ERROR);
- });
-
- it("should throw an error if operations fail", async function () {
- Docker = class {
- listContainers = sinon.stub().resolves([
- {
- Names: ["/my_container"],
- Id: "abc123",
- },
- ]);
- getContainer = sinon.stub().throws(new Error("test error"));
- };
- networkService = new NetworkService(axios, ping, logger, http, Docker);
- const job = { data: { url: "abc123", _id: "123", type: "docker" } };
- try {
- await networkService.requestDocker(job);
- } catch (error) {
- expect(error.message).to.equal("test error");
- }
- });
-
- it("should throw an error if no matching images found", async function () {
- Docker = class {
- listContainers = sinon.stub().resolves([]);
- getContainer = sinon.stub().throws(new Error("test error"));
- };
- networkService = new NetworkService(axios, ping, logger, http, Docker);
- const job = { data: { url: "abc123", _id: "123", type: "docker" } };
- try {
- await networkService.requestDocker(job);
- } catch (error) {
- expect(error.message).to.equal(errorMessages.DOCKER_NOT_FOUND);
- }
- });
- });
-
- describe("getStatus", function () {
- beforeEach(function () {
- networkService.requestPing = sinon.stub();
- networkService.requestHttp = sinon.stub();
- networkService.requestPagespeed = sinon.stub();
- networkService.requestHardware = sinon.stub();
- networkService.requestDocker = sinon.stub();
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should call requestPing if type is ping", async function () {
- await networkService.getStatus({ data: { type: "ping" } });
- expect(networkService.requestPing.calledOnce).to.be.true;
- expect(networkService.requestDocker.notCalled).to.be.true;
- expect(networkService.requestHttp.notCalled).to.be.true;
- expect(networkService.requestPagespeed.notCalled).to.be.true;
- });
-
- it("should call requestHttp if type is http", async function () {
- await networkService.getStatus({ data: { type: "http" } });
- expect(networkService.requestPing.notCalled).to.be.true;
- expect(networkService.requestDocker.notCalled).to.be.true;
- expect(networkService.requestHttp.calledOnce).to.be.true;
- expect(networkService.requestPagespeed.notCalled).to.be.true;
- });
-
- it("should call requestPagespeed if type is pagespeed", async function () {
- await networkService.getStatus({ data: { type: "pagespeed" } });
- expect(networkService.requestPing.notCalled).to.be.true;
- expect(networkService.requestDocker.notCalled).to.be.true;
- expect(networkService.requestHttp.notCalled).to.be.true;
- expect(networkService.requestPagespeed.calledOnce).to.be.true;
- });
-
- it("should call requestHardware if type is hardware", async function () {
- await networkService.getStatus({ data: { type: "hardware" } });
- expect(networkService.requestHardware.calledOnce).to.be.true;
- expect(networkService.requestDocker.notCalled).to.be.true;
- expect(networkService.requestPing.notCalled).to.be.true;
- expect(networkService.requestPagespeed.notCalled).to.be.true;
- });
-
- it("should call requestDocker if type is Docker", async function () {
- await networkService.getStatus({ data: { type: "docker" } });
- expect(networkService.requestDocker.calledOnce).to.be.true;
- expect(networkService.requestHardware.notCalled).to.be.true;
- expect(networkService.requestPing.notCalled).to.be.true;
- expect(networkService.requestPagespeed.notCalled).to.be.true;
- });
-
- it("should throw an error if an unknown type is provided", async function () {
- try {
- await networkService.getStatus({ data: { type: "unknown" } });
- } catch (error) {
- expect(error.service).to.equal("NetworkService");
- expect(error.method).to.equal("getStatus");
- expect(error.message).to.equal("Unsupported type: unknown");
- }
- });
-
- it("should throw an error if job type is undefined", async function () {
- try {
- await networkService.getStatus({ data: { type: undefined } });
- } catch (error) {
- expect(error.service).to.equal("NetworkService");
- expect(error.method).to.equal("getStatus");
- expect(error.message).to.equal("Unsupported type: unknown");
- }
- });
-
- it("should throw an error if job is empty", async function () {
- try {
- await networkService.getStatus({});
- } catch (error) {
- expect(error.method).to.equal("getStatus");
- expect(error.message).to.equal("Unsupported type: unknown");
- }
- });
-
- it("should throw an error if job is null", async function () {
- try {
- await networkService.getStatus(null);
- } catch (error) {
- expect(error.service).to.equal("NetworkService");
- expect(error.method).to.equal("getStatus");
- expect(error.message).to.equal("Unsupported type: unknown");
- }
- });
- });
-});
diff --git a/server/tests/services/notificationService.test.js b/server/tests/services/notificationService.test.js
deleted file mode 100755
index 211cc8b46..000000000
--- a/server/tests/services/notificationService.test.js
+++ /dev/null
@@ -1,285 +0,0 @@
-import sinon from "sinon";
-import NotificationService from "../../service/notificationService.js";
-import { expect } from "chai";
-
-describe("NotificationService", function () {
- let emailService, db, logger, notificationService;
-
- beforeEach(function () {
- db = {
- getNotificationsByMonitorId: sinon.stub(),
- };
- emailService = {
- buildAndSendEmail: sinon.stub(),
- };
- logger = {
- warn: sinon.stub(),
- };
-
- notificationService = new NotificationService(emailService, db, logger);
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("constructor", function () {
- it("should create a new instance of NotificationService", function () {
- expect(notificationService).to.be.an.instanceOf(NotificationService);
- });
- });
-
- describe("sendEmail", function () {
- it("should send an email notification with Up Template", async function () {
- const networkResponse = {
- monitor: {
- name: "Test Monitor",
- url: "http://test.com",
- },
- status: true,
- prevStatus: false,
- };
- const address = "test@test.com";
- await notificationService.sendEmail(networkResponse, address);
- expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true;
- expect(
- notificationService.emailService.buildAndSendEmail.calledWith("serverIsUpTemplate", { monitor: "Test Monitor", url: "http://test.com" })
- );
- });
-
- it("should send an email notification with Down Template", async function () {
- const networkResponse = {
- monitor: {
- name: "Test Monitor",
- url: "http://test.com",
- },
- status: false,
- prevStatus: true,
- };
- const address = "test@test.com";
- await notificationService.sendEmail(networkResponse, address);
- expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true;
- });
-
- it("should send an email notification with Up Template", async function () {
- const networkResponse = {
- monitor: {
- name: "Test Monitor",
- url: "http://test.com",
- },
- status: true,
- prevStatus: false,
- };
- const address = "test@test.com";
- await notificationService.sendEmail(networkResponse, address);
- expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true;
- });
- });
-
- describe("handleNotifications", function () {
- it("should handle notifications based on the network response", async function () {
- notificationService.sendEmail = sinon.stub();
- const res = await notificationService.handleNotifications({
- monitor: {
- type: "email",
- address: "www.google.com",
- },
- });
- expect(res).to.be.true;
- });
-
- it("should handle hardware notifications", async function () {
- notificationService.sendEmail = sinon.stub();
- const res = await notificationService.handleNotifications({
- monitor: {
- type: "hardware",
- address: "www.google.com",
- },
- });
- expect(res).to.be.true;
- });
-
- it("should handle an error when getting notifications", async function () {
- const testError = new Error("Test Error");
- notificationService.db.getNotificationsByMonitorId.rejects(testError);
- await notificationService.handleNotifications({ monitorId: "123" });
- expect(notificationService.logger.warn.calledOnce).to.be.true;
- });
- });
-
- describe("sendHardwareEmail", function () {
- let networkResponse, address, alerts;
-
- beforeEach(function () {
- networkResponse = {
- monitor: {
- name: "Test Monitor",
- url: "http://test.com",
- },
- status: true,
- prevStatus: false,
- };
- address = "test@test.com";
- alerts = ["test"];
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should send an email notification with Hardware Template", async function () {
- emailService.buildAndSendEmail.resolves(true);
- const res = await notificationService.sendHardwareEmail(networkResponse, address, alerts);
- expect(res).to.be.true;
- });
-
- it("should return false if no alerts are provided", async function () {
- alerts = [];
- emailService.buildAndSendEmail.resolves(true);
- const res = await notificationService.sendHardwareEmail(networkResponse, address, alerts);
- expect(res).to.be.false;
- });
- });
-
- describe("handleStatusNotifications", function () {
- let networkResponse;
-
- beforeEach(function () {
- networkResponse = {
- monitor: {
- name: "Test Monitor",
- url: "http://test.com",
- },
- statusChanged: true,
- status: true,
- prevStatus: false,
- };
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should handle status notifications", async function () {
- db.getNotificationsByMonitorId.resolves([{ type: "email", address: "test@test.com" }]);
- const res = await notificationService.handleStatusNotifications(networkResponse);
- expect(res).to.be.true;
- });
-
- it("should return false if status hasn't changed", async function () {
- networkResponse.statusChanged = false;
- const res = await notificationService.handleStatusNotifications(networkResponse);
- expect(res).to.be.false;
- });
-
- it("should return false if prevStatus is undefined", async function () {
- networkResponse.prevStatus = undefined;
- const res = await notificationService.handleStatusNotifications(networkResponse);
- expect(res).to.be.false;
- });
-
- it("should handle an error", async function () {
- const testError = new Error("Test Error");
- db.getNotificationsByMonitorId.rejects(testError);
- try {
- await notificationService.handleStatusNotifications(networkResponse);
- } catch (error) {
- expect(error).to.be.an.instanceOf(Error);
- expect(error.message).to.equal("Test Error");
- }
- });
- });
-
- describe("handleHardwareNotifications", function () {
- let networkResponse;
-
- beforeEach(function () {
- networkResponse = {
- monitor: {
- name: "Test Monitor",
- url: "http://test.com",
- thresholds: {
- usage_cpu: 1,
- usage_memory: 1,
- usage_disk: 1,
- },
- },
- payload: {
- data: {
- cpu: {
- usage_percent: 0.655,
- },
- memory: {
- usage_percent: 0.783,
- },
- disk: [
- {
- name: "/dev/sda1",
- usage_percent: 0.452,
- },
- {
- name: "/dev/sdb1",
- usage_percent: 0.627,
- },
- ],
- },
- },
- };
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("it should return false if no thresholds are set", function () {
- it("should return false if no thresholds are set", async function () {
- networkResponse.monitor.thresholds = undefined;
- const res = await notificationService.handleHardwareNotifications(networkResponse);
- expect(res).to.be.false;
- });
-
- it("should return false if metrics are null", async function () {
- networkResponse.payload.data = null;
- const res = await notificationService.handleHardwareNotifications(networkResponse);
- expect(res).to.be.false;
- });
-
- it("should return true if request is well formed and thresholds > 0", async function () {
- db.getNotificationsByMonitorId.resolves([
- {
- type: "email",
- address: "test@test.com",
- alertThreshold: 1,
- cpuAlertThreshold: 1,
- memoryAlertThreshold: 1,
- diskAlertThreshold: 1,
- save: sinon.stub().resolves(),
- },
- ]);
- const res = await notificationService.handleHardwareNotifications(networkResponse);
- expect(res).to.be.true;
- });
-
- it("should return true if thresholds are exceeded", async function () {
- db.getNotificationsByMonitorId.resolves([
- {
- type: "email",
- address: "test@test.com",
- alertThreshold: 1,
- cpuAlertThreshold: 1,
- memoryAlertThreshold: 1,
- diskAlertThreshold: 1,
- save: sinon.stub().resolves(),
- },
- ]);
- networkResponse.monitor.thresholds = {
- usage_cpu: 0.01,
- usage_memory: 0.01,
- usage_disk: 0.01,
- };
- const res = await notificationService.handleHardwareNotifications(networkResponse);
- expect(res).to.be.true;
- });
- });
- });
-});
diff --git a/server/tests/services/settingsService.test.js b/server/tests/services/settingsService.test.js
deleted file mode 100755
index 6c9960e5e..000000000
--- a/server/tests/services/settingsService.test.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import sinon from "sinon";
-import SettingsService from "../../service/settingsService.js";
-import { expect } from "chai";
-import NetworkService from "../../service/networkService.js";
-const SERVICE_NAME = "SettingsService";
-
-describe("SettingsService", function () {
- let sandbox, mockAppSettings;
-
- beforeEach(function () {
- sandbox = sinon.createSandbox();
- sandbox.stub(process.env, "CLIENT_HOST").value("http://localhost");
- sandbox.stub(process.env, "JWT_SECRET").value("secret");
- sandbox.stub(process.env, "REFRESH_TOKEN_SECRET").value("refreshSecret");
- sandbox.stub(process.env, "DB_TYPE").value("postgres");
- sandbox.stub(process.env, "DB_CONNECTION_STRING").value("postgres://user:pass@localhost/db");
- sandbox.stub(process.env, "REDIS_HOST").value("localhost");
- sandbox.stub(process.env, "REDIS_PORT").value("6379");
- sandbox.stub(process.env, "TOKEN_TTL").value("3600");
- sandbox.stub(process.env, "REFRESH_TOKEN_TTL").value("86400");
- sandbox.stub(process.env, "PAGESPEED_API_KEY").value("apiKey");
- sandbox.stub(process.env, "SYSTEM_EMAIL_HOST").value("smtp.mailtrap.io");
- sandbox.stub(process.env, "SYSTEM_EMAIL_PORT").value("2525");
- sandbox.stub(process.env, "SYSTEM_EMAIL_ADDRESS").value("test@example.com");
- sandbox.stub(process.env, "SYSTEM_EMAIL_PASSWORD").value("password");
- });
-
- mockAppSettings = {
- settingOne: 123,
- settingTwo: 456,
- };
-
- afterEach(function () {
- sandbox.restore();
- sinon.restore();
- });
-
- describe("constructor", function () {
- it("should construct a new SettingsService", function () {
- const settingsService = new SettingsService(mockAppSettings);
- expect(settingsService.appSettings).to.equal(mockAppSettings);
- });
- });
-
- describe("loadSettings", function () {
- it("should load settings from DB when environment variables are not set", async function () {
- const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" };
- const appSettings = { findOne: sinon.stub().returns(dbSettings) };
- const settingsService = new SettingsService(appSettings);
- settingsService.settings = {};
- const result = await settingsService.loadSettings();
- expect(result).to.deep.equal(dbSettings);
- });
-
- it("should throw an error if settings are not found", async function () {
- const appSettings = { findOne: sinon.stub().returns(null) };
- const settingsService = new SettingsService(appSettings);
- settingsService.settings = null;
-
- try {
- await settingsService.loadSettings();
- } catch (error) {
- expect(error.message).to.equal("Settings not found");
- expect(error.service).to.equal(SERVICE_NAME);
- expect(error.method).to.equal("loadSettings");
- }
- });
-
- it("should add its method and service name to error if not present", async function () {
- const appSettings = { findOne: sinon.stub().throws(new Error("Test error")) };
- const settingsService = new SettingsService(appSettings);
- try {
- await settingsService.loadSettings();
- } catch (error) {
- expect(error.message).to.equal("Test error");
- expect(error.service).to.equal(SERVICE_NAME);
- expect(error.method).to.equal("loadSettings");
- }
- });
-
- it("should not add its method and service name to error if present", async function () {
- const error = new Error("Test error");
- error.method = "otherMethod";
- error.service = "OTHER_SERVICE";
- const appSettings = { findOne: sinon.stub().throws(error) };
- const settingsService = new SettingsService(appSettings);
- try {
- await settingsService.loadSettings();
- } catch (error) {
- expect(error.message).to.equal("Test error");
- expect(error.service).to.equal("OTHER_SERVICE");
- expect(error.method).to.equal("otherMethod");
- }
- });
-
- it("should merge DB settings with environment variables", async function () {
- const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" };
- const appSettings = { findOne: sinon.stub().returns(dbSettings) };
- const settingsService = new SettingsService(appSettings);
- const result = await settingsService.loadSettings();
- expect(result).to.deep.equal(settingsService.settings);
- expect(settingsService.settings.logLevel).to.equal("debug");
- expect(settingsService.settings.apiBaseUrl).to.equal("http://localhost");
- });
- });
-
- describe("reloadSettings", function () {
- it("should call loadSettings", async function () {
- const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" };
- const appSettings = { findOne: sinon.stub().returns(dbSettings) };
- const settingsService = new SettingsService(appSettings);
- settingsService.settings = {};
- const result = await settingsService.reloadSettings();
- expect(result).to.deep.equal(dbSettings);
- });
- });
-
- describe("getSettings", function () {
- it("should return the current settings", function () {
- const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" };
- const appSettings = { findOne: sinon.stub().returns(dbSettings) };
- const settingsService = new SettingsService(appSettings);
- settingsService.settings = dbSettings;
- const result = settingsService.getSettings();
- expect(result).to.deep.equal(dbSettings);
- });
-
- it("should throw an error if settings have not been loaded", function () {
- const appSettings = { findOne: sinon.stub().returns(null) };
- const settingsService = new SettingsService(appSettings);
- settingsService.settings = null;
-
- try {
- settingsService.getSettings();
- } catch (error) {
- expect(error.message).to.equal("Settings have not been loaded");
- }
- });
- });
-});
diff --git a/server/tests/services/statusService.test.js b/server/tests/services/statusService.test.js
deleted file mode 100755
index 289a36884..000000000
--- a/server/tests/services/statusService.test.js
+++ /dev/null
@@ -1,255 +0,0 @@
-import sinon from "sinon";
-import StatusService from "../../service/statusService.js";
-import { afterEach, describe } from "node:test";
-
-describe("StatusService", () => {
- let db, logger, statusService;
-
- beforeEach(function () {
- db = {
- getMonitorById: sinon.stub(),
- createCheck: sinon.stub(),
- createPagespeedCheck: sinon.stub(),
- };
- logger = {
- info: sinon.stub(),
- error: sinon.stub(),
- };
- statusService = new StatusService(db, logger);
- });
-
- afterEach(() => {
- sinon.restore();
- });
-
- describe("constructor", () => {
- it("should create an instance of StatusService", function () {
- expect(statusService).to.be.an.instanceOf(StatusService);
- });
- });
-
- describe("getStatusString", () => {
- it("should return 'up' if status is true", function () {
- expect(statusService.getStatusString(true)).to.equal("up");
- });
-
- it("should return 'down' if status is false", function () {
- expect(statusService.getStatusString(false)).to.equal("down");
- });
-
- it("should return 'unknown' if status is undefined or null", function () {
- expect(statusService.getStatusString(undefined)).to.equal("unknown");
- });
- });
-
- describe("updateStatus", () => {
- beforeEach(function () {
- // statusService.insertCheck = sinon.stub().resolves;
- });
-
- afterEach(() => {
- sinon.restore();
- });
-
- it("should throw an error if an error occurs", async function () {
- const error = new Error("Test error");
- statusService.db.getMonitorById = sinon.stub().throws(error);
- try {
- await statusService.updateStatus({ monitorId: "test", status: true });
- } catch (error) {
- expect(error.message).to.equal("Test error");
- }
- // expect(statusService.insertCheck.calledOnce).to.be.true;
- });
-
- it("should return {statusChanged: false} if status hasn't changed", async function () {
- statusService.db.getMonitorById = sinon.stub().returns({ status: true });
- const result = await statusService.updateStatus({
- monitorId: "test",
- status: true,
- });
- expect(result).to.deep.equal({ statusChanged: false });
- // expect(statusService.insertCheck.calledOnce).to.be.true;
- });
-
- it("should return {statusChanged: true} if status has changed from down to up", async function () {
- statusService.db.getMonitorById = sinon.stub().returns({ status: false, save: sinon.stub() });
- const result = await statusService.updateStatus({
- monitorId: "test",
- status: true,
- });
- expect(result.statusChanged).to.be.true;
- expect(result.monitor.status).to.be.true;
- expect(result.prevStatus).to.be.false;
- // expect(statusService.insertCheck.calledOnce).to.be.true;
- });
-
- it("should return {statusChanged: true} if status has changed from up to down", async function () {
- statusService.db.getMonitorById = sinon.stub().returns({ status: true, save: sinon.stub() });
- const result = await statusService.updateStatus({
- monitorId: "test",
- status: false,
- });
- expect(result.statusChanged).to.be.true;
- expect(result.monitor.status).to.be.false;
- expect(result.prevStatus).to.be.true;
- // expect(statusService.insertCheck.calledOnce).to.be.true;
- });
- });
-
- describe("buildCheck", () => {
- it("should build a check object", function () {
- const check = statusService.buildCheck({
- monitorId: "test",
- type: "test",
- status: true,
- responseTime: 100,
- code: 200,
- message: "Test message",
- payload: { test: "test" },
- });
- expect(check.monitorId).to.equal("test");
- expect(check.status).to.be.true;
- expect(check.statusCode).to.equal(200);
- expect(check.responseTime).to.equal(100);
- expect(check.message).to.equal("Test message");
- });
-
- it("should build a check object for pagespeed type", function () {
- const check = statusService.buildCheck({
- monitorId: "test",
- type: "pagespeed",
- status: true,
- responseTime: 100,
- code: 200,
- message: "Test message",
- payload: {
- lighthouseResult: {
- categories: {
- accessibility: { score: 1 },
- "best-practices": { score: 1 },
- performance: { score: 1 },
- seo: { score: 1 },
- },
- audits: {
- "cumulative-layout-shift": { score: 1 },
- "speed-index": { score: 1 },
- "first-contentful-paint": { score: 1 },
- "largest-contentful-paint": { score: 1 },
- "total-blocking-time": { score: 1 },
- },
- },
- },
- });
- expect(check.monitorId).to.equal("test");
- expect(check.status).to.be.true;
- expect(check.statusCode).to.equal(200);
- expect(check.responseTime).to.equal(100);
- expect(check.message).to.equal("Test message");
- expect(check.accessibility).to.equal(100);
- expect(check.bestPractices).to.equal(100);
- expect(check.performance).to.equal(100);
- expect(check.seo).to.equal(100);
- expect(check.audits).to.deep.equal({
- cls: { score: 1 },
- si: { score: 1 },
- fcp: { score: 1 },
- lcp: { score: 1 },
- tbt: { score: 1 },
- });
- });
-
- it("should build a check object for pagespeed type with missing data", function () {
- const check = statusService.buildCheck({
- monitorId: "test",
- type: "pagespeed",
- status: true,
- responseTime: 100,
- code: 200,
- message: "Test message",
- payload: {
- lighthouseResult: {
- categories: {},
- audits: {},
- },
- },
- });
- expect(check.monitorId).to.equal("test");
- expect(check.status).to.be.true;
- expect(check.statusCode).to.equal(200);
- expect(check.responseTime).to.equal(100);
- expect(check.message).to.equal("Test message");
- expect(check.accessibility).to.equal(0);
- expect(check.bestPractices).to.equal(0);
- expect(check.performance).to.equal(0);
- expect(check.seo).to.equal(0);
- expect(check.audits).to.deep.equal({
- cls: 0,
- si: 0,
- fcp: 0,
- lcp: 0,
- tbt: 0,
- });
- });
-
- it("should build a check for hardware type", function () {
- const check = statusService.buildCheck({
- monitorId: "test",
- type: "hardware",
- status: true,
- responseTime: 100,
- code: 200,
- message: "Test message",
- payload: { data: { cpu: "cpu", memory: "memory", disk: "disk", host: "host" } },
- });
- expect(check.monitorId).to.equal("test");
- expect(check.status).to.be.true;
- expect(check.statusCode).to.equal(200);
- expect(check.responseTime).to.equal(100);
- expect(check.message).to.equal("Test message");
- expect(check.cpu).to.equal("cpu");
- expect(check.memory).to.equal("memory");
- expect(check.disk).to.equal("disk");
- expect(check.host).to.equal("host");
- });
-
- it("should build a check for hardware type with missing data", function () {
- const check = statusService.buildCheck({
- monitorId: "test",
- type: "hardware",
- status: true,
- responseTime: 100,
- code: 200,
- message: "Test message",
- payload: {},
- });
- expect(check.monitorId).to.equal("test");
- expect(check.status).to.be.true;
- expect(check.statusCode).to.equal(200);
- expect(check.responseTime).to.equal(100);
- expect(check.message).to.equal("Test message");
- expect(check.cpu).to.deep.equal({});
- expect(check.memory).to.deep.equal({});
- expect(check.disk).to.deep.equal({});
- expect(check.host).to.deep.equal({});
- });
- });
-
- describe("insertCheck", () => {
- it("should log an error if one is thrown", async function () {
- const testError = new Error("Test error");
- statusService.db.createCheck = sinon.stub().throws(testError);
- try {
- await statusService.insertCheck({ monitorId: "test" });
- } catch (error) {
- expect(error.message).to.equal(testError.message);
- }
- expect(statusService.logger.error.calledOnce).to.be.true;
- });
-
- it("should insert a check into the database", async function () {
- await statusService.insertCheck({ monitorId: "test", type: "http" });
- expect(statusService.db.createCheck.calledOnce).to.be.true;
- });
- });
-});
diff --git a/server/tests/utils/dataUtils.test.js b/server/tests/utils/dataUtils.test.js
deleted file mode 100755
index ec9eabb40..000000000
--- a/server/tests/utils/dataUtils.test.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { NormalizeData, calculatePercentile } from "../../utils/dataUtils.js";
-import sinon from "sinon";
-
-describe("NormalizeData", function () {
- it("should normalize response times when checks length is greater than 1", function () {
- const checks = [
- { responseTime: 20, _doc: { id: 1 } },
- { responseTime: 40, _doc: { id: 2 } },
- { responseTime: 60, _doc: { id: 3 } },
- ];
- const rangeMin = 1;
- const rangeMax = 100;
-
- const result = NormalizeData(checks, rangeMin, rangeMax);
-
- expect(result).to.be.an("array");
- expect(result).to.have.lengthOf(3);
- result.forEach((check) => {
- expect(check).to.have.property("responseTime").that.is.a("number");
- expect(check).to.have.property("originalResponseTime").that.is.a("number");
- });
- });
-
- it("should return checks with original response times when checks length is 1", function () {
- const checks = [{ responseTime: 20, _doc: { id: 1 } }];
- const rangeMin = 1;
- const rangeMax = 100;
-
- const result = NormalizeData(checks, rangeMin, rangeMax);
- expect(result).to.be.an("array");
- expect(result).to.have.lengthOf(1);
- expect(result[0]).to.have.property("originalResponseTime", 20);
- });
-
- it("should handle edge cases with extreme response times", function () {
- const checks = [
- { responseTime: 5, _doc: { id: 1 } },
- { responseTime: 95, _doc: { id: 2 } },
- ];
- const rangeMin = 1;
- const rangeMax = 100;
-
- const result = NormalizeData(checks, rangeMin, rangeMax);
-
- expect(result).to.be.an("array");
- expect(result).to.have.lengthOf(2);
- expect(result[0]).to.have.property("responseTime").that.is.at.least(rangeMin);
- expect(result[1]).to.have.property("responseTime").that.is.at.most(rangeMax);
- });
-});
-
-describe("calculatePercentile", function () {
- it("should return the lower value when upper is greater than or equal to the length of the sorted array", function () {
- const checks = [{ responseTime: 10 }, { responseTime: 20 }, { responseTime: 30 }, { responseTime: 40 }, { responseTime: 50 }];
-
- const percentile = 100;
- const result = calculatePercentile(checks, percentile);
- const expected = 50;
- expect(result).to.equal(expected);
- });
-});
diff --git a/server/tests/utils/imageProcessing.test.js b/server/tests/utils/imageProcessing.test.js
deleted file mode 100755
index 09330c317..000000000
--- a/server/tests/utils/imageProcessing.test.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import { expect } from "chai";
-import sinon from "sinon";
-import sharp from "sharp";
-import { GenerateAvatarImage } from "../../utils/imageProcessing.js";
-
-describe("imageProcessing - GenerateAvatarImage", function () {
- it("should resize the image to 64x64 and return a base64 string", async function () {
- const file = {
- buffer: Buffer.from("test image buffer"),
- };
-
- // Stub the sharp function
- const toBufferStub = sinon.stub().resolves(Buffer.from("resized image buffer"));
- const resizeStub = sinon.stub().returns({ toBuffer: toBufferStub });
- const sharpStub = sinon.stub(sharp.prototype, "resize").returns({ toBuffer: toBufferStub });
-
- const result = await GenerateAvatarImage(file);
-
- // Verify the result
- const expected = Buffer.from("resized image buffer").toString("base64");
- expect(result).to.equal(expected);
-
- // Verify that the sharp function was called with the correct arguments
- expect(sharpStub.calledOnceWith({ width: 64, height: 64, fit: "cover" })).to.be.true;
- expect(toBufferStub.calledOnce).to.be.true;
-
- // Restore the stubbed functions
- sharpStub.restore();
- });
-
- it("should throw an error if resizing fails", async function () {
- const file = {
- buffer: Buffer.from("test image buffer"),
- };
-
- // Stub the sharp function to throw an error
- const toBufferStub = sinon.stub().rejects(new Error("Resizing failed"));
- const resizeStub = sinon.stub().returns({ toBuffer: toBufferStub });
- const sharpStub = sinon.stub(sharp.prototype, "resize").returns({ toBuffer: toBufferStub });
-
- try {
- await GenerateAvatarImage(file);
- // If no error is thrown, fail the test
- expect.fail("Expected error to be thrown");
- } catch (error) {
- // Verify that the error message is correct
- expect(error.message).to.equal("Resizing failed");
- }
-
- // Restore the stubbed functions
- sharpStub.restore();
- });
-});
diff --git a/server/tests/utils/logger.test.js b/server/tests/utils/logger.test.js
deleted file mode 100755
index 2d5bd82a9..000000000
--- a/server/tests/utils/logger.test.js
+++ /dev/null
@@ -1,139 +0,0 @@
-import sinon from "sinon";
-import logger from "../../utils/logger.js";
-import { Logger } from "../../utils/logger.js";
-import winston from "winston";
-
-describe("Logger", function () {
- let infoStub, warnStub, errorStub;
-
- beforeEach(function () {
- infoStub = sinon.stub(logger.logger, "info");
- warnStub = sinon.stub(logger.logger, "warn");
- errorStub = sinon.stub(logger.logger, "error");
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- describe("constructor", function () {
- let createLoggerStub;
-
- beforeEach(function () {
- createLoggerStub = sinon.stub(winston, "createLogger");
- });
-
- afterEach(function () {
- sinon.restore();
- });
-
- it("should convert message to JSON string if it is an object", function () {
- const logMessage = { key: "value" };
- const expectedMessage = JSON.stringify(logMessage, null, 2);
-
- createLoggerStub.callsFake((config) => {
- const consoleTransport = config.transports[0];
- const logEntry = {
- level: "info",
- message: logMessage,
- timestamp: new Date().toISOString(),
- };
- const formattedMessage = consoleTransport.format.transform(logEntry);
- expect(formattedMessage).to.include(expectedMessage);
- return { log: sinon.spy() };
- });
-
- const logger = new Logger();
- logger.logger.info(logMessage);
- });
-
- it("should convert details to JSON string if it is an object", function () {
- const logDetails = { key: "value" };
- const expectedDetails = JSON.stringify(logDetails, null, 2); // Removed .s
-
- createLoggerStub.callsFake((config) => {
- const consoleTransport = config.transports[0];
- const logEntry = {
- level: "info",
- message: "", // Add empty message since it's required
- details: logDetails,
- timestamp: new Date().toISOString(),
- };
- const formattedMessage = consoleTransport.format.transform(logEntry);
- expect(formattedMessage).to.include(expectedDetails);
- return { info: sinon.spy() }; // Changed to return info method
- });
-
- const logger = new Logger();
- logger.logger.info("", { details: logDetails }); // Updated to pass details properly
- });
- });
-
- describe("info", function () {
- it("should log an informational message", function () {
- const config = {
- message: "Info message",
- service: "TestService",
- method: "TestMethod",
- details: { key: "value" },
- };
-
- logger.info(config);
-
- expect(infoStub.calledOnce).to.be.true;
- expect(
- infoStub.calledWith(config.message, {
- service: config.service,
- method: config.method,
- details: config.details,
- })
- ).to.be.true;
- });
- });
-
- describe("warn", function () {
- it("should log a warning message", function () {
- const config = {
- message: "Warning message",
- service: "TestService",
- method: "TestMethod",
- details: { key: "value" },
- };
-
- logger.warn(config);
-
- expect(warnStub.calledOnce).to.be.true;
- expect(
- warnStub.calledWith(config.message, {
- service: config.service,
- method: config.method,
- details: config.details,
- })
- ).to.be.true;
- });
- });
-
- describe("error", function () {
- it("should log an error message", function () {
- const config = {
- message: "Error message",
- service: "TestService",
- method: "TestMethod",
- details: { key: "value" },
- stack: "Error stack trace",
- };
-
- logger.error(config);
-
- expect(errorStub.calledOnce).to.be.true;
- expect(
- errorStub.calledWith(config.message, {
- service: config.service,
- method: config.method,
- details: config.details,
- stack: config.stack,
- })
- ).to.be.true;
- });
- });
-});
diff --git a/server/tests/utils/messages.test.js b/server/tests/utils/messages.test.js
deleted file mode 100755
index a7e98c477..000000000
--- a/server/tests/utils/messages.test.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { errorMessages, successMessages } from "../../utils/messages.js";
-describe("Messages", function () {
- describe("messages - errorMessages", function () {
- it("should have a DB_FIND_MONITOR_BY_ID function", function () {
- const monitorId = "12345";
- expect(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)).to.equal(`Monitor with id ${monitorId} not found`);
- });
-
- it("should have a DB_DELETE_CHECKS function", function () {
- const monitorId = "12345";
- expect(errorMessages.DB_DELETE_CHECKS(monitorId)).to.equal(`No checks found for monitor with id ${monitorId}`);
- });
- });
-
- describe("messages - successMessages", function () {
- it("should have a MONITOR_GET_BY_USER_ID function", function () {
- const userId = "12345";
- expect(successMessages.MONITOR_GET_BY_USER_ID(userId)).to.equal(`Got monitor for ${userId} successfully"`);
- });
-
- // Add more tests for other success messages as needed
- });
-});
diff --git a/server/tests/utils/utils.test.js b/server/tests/utils/utils.test.js
deleted file mode 100755
index 2630bb8e6..000000000
--- a/server/tests/utils/utils.test.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { ParseBoolean, getTokenFromHeaders } from "../../utils/utils.js";
-
-describe("utils - ParseBoolean", function () {
- it("should return true", function () {
- const result = ParseBoolean("true");
- expect(result).to.be.true;
- });
-
- it("should return false", function () {
- const result = ParseBoolean("false");
- expect(result).to.be.false;
- });
-
- it("should return false", function () {
- const result = ParseBoolean(null);
- expect(result).to.be.false;
- });
-
- it("should return false", function () {
- const result = ParseBoolean(undefined);
- expect(result).to.be.false;
- });
-});
-
-describe("utils - getTokenFromHeaders", function () {
- it("should throw an error if authorization header is missing", function () {
- const headers = {};
- expect(() => getTokenFromHeaders(headers)).to.throw("No auth headers");
- });
-
- it("should throw an error if authorization header does not start with Bearer", function () {
- const headers = { authorization: "Basic abcdef" };
- expect(() => getTokenFromHeaders(headers)).to.throw("Invalid auth headers");
- });
-
- it("should return the token if authorization header is correctly formatted", function () {
- const headers = { authorization: "Bearer abcdef" };
- expect(getTokenFromHeaders(headers)).to.equal("abcdef");
- });
-
- it("should throw an error if authorization header has more than two parts", function () {
- const headers = { authorization: "Bearer abc def" };
- expect(() => getTokenFromHeaders(headers)).to.throw("Invalid auth headers");
- });
-
- it("should throw an error if authorization header has less than two parts", function () {
- const headers = { authorization: "Bearer" };
- expect(() => getTokenFromHeaders(headers)).to.throw("Invalid auth headers");
- });
-});
diff --git a/server/tsconfig.json b/server/tsconfig.json
new file mode 100644
index 000000000..260ccb964
--- /dev/null
+++ b/server/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./dist",
+ "module": "nodenext",
+ "target": "esnext",
+ "types": ["node"],
+ "sourceMap": true,
+ "declaration": false,
+ "declarationMap": false,
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+ "strict": true,
+ "isolatedModules": true,
+ "noUncheckedSideEffectImports": true,
+ "moduleDetection": "force",
+ "skipLibCheck": true,
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "forceConsistentCasingInFileNames": true,
+ "allowJs": true,
+ "checkJs": false
+ },
+ "exclude": ["node_modules", "dist/", "**/*.config.js"]
+}