Merge pull request #3080 from bluewave-labs/feat/remove-v2

Remove business V2 logic
This commit is contained in:
Alexander Holliday
2025-12-05 09:05:17 -08:00
committed by GitHub
119 changed files with 1 additions and 8831 deletions

View File

@@ -1,53 +0,0 @@
import React, { useState } from "react";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import IconButton from "@mui/material/IconButton";
import Settings from "@/assets/icons/settings-bold.svg?react";
export type ActionMenuItem = {
id: number | string;
label: React.ReactNode;
action: Function;
closeMenu?: boolean;
};
export const ActionsMenu = ({ items }: { items: ActionMenuItem[] }) => {
const [anchorEl, setAnchorEl] = useState<null | any>(null);
const open = Boolean(anchorEl);
const handleClick = (e: React.MouseEvent<any>) => {
e.stopPropagation();
setAnchorEl(e.currentTarget);
};
const handleClose = (e: React.MouseEvent<any>) => {
e.stopPropagation();
setAnchorEl(null);
};
return (
<div>
<IconButton onClick={handleClick}>
<Settings />
</IconButton>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
>
{items.map((item) => (
<MenuItem
key={item.id}
onClick={(e: React.MouseEvent<HTMLLIElement>) => {
e.stopPropagation();
if (item.closeMenu) handleClose(e);
item.action();
}}
>
{item.label}
</MenuItem>
))}
</Menu>
</div>
);
};

View File

@@ -1,36 +0,0 @@
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 (
<LeftArrowDouble
style={{ color }}
{...props}
/>
);
} else if (type === "long") {
return (
<LeftArrowLong
style={{ color }}
{...props}
/>
);
} else {
return (
<LeftArrow
style={{ color }}
{...props}
/>
);
}
};

View File

@@ -1,28 +0,0 @@
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 (
<RightArrowDouble
style={{ color }}
{...props}
/>
);
} else {
return (
<RightArrow
style={{ color }}
{...props}
/>
);
}
};

View File

@@ -1,48 +0,0 @@
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<AuthBasePageProps> = ({
children,
title,
subtitle,
...props
}) => {
const theme = useTheme();
return (
<Stack
gap={theme.spacing(10)}
minHeight="100vh"
{...props}
>
<HeaderAuth />
<Stack
alignItems="center"
margin="auto"
width="100%"
gap={theme.spacing(4)}
>
<Box
width={{ xs: 60, sm: 70, md: 80 }}
mb={theme.spacing(10)}
>
<Logo style={{ width: "100%", height: "100%" }} />
</Box>
<Typography variant="h1">{title}</Typography>
<Typography variant="h1">{subtitle}</Typography>
{children}
</Stack>
</Stack>
);
};

View File

@@ -1,26 +0,0 @@
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 (
<Stack
width={"100%"}
direction="row"
alignItems="center"
justifyContent="flex-end"
py={theme.spacing(4)}
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<LanguageSelector />
<ThemeSwitch color="red" />
</Stack>
);
};

View File

@@ -1,2 +0,0 @@
export { HeaderAuth } from "./HeaderAuth";
export { AuthBasePage } from "./AuthBasePage";

View File

@@ -1,23 +0,0 @@
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<BaseBoxProps> = ({ children, sx }) => {
const theme = useTheme();
return (
<Box
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
...sx,
}}
>
{children}
</Box>
);
};

View File

@@ -1,80 +0,0 @@
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<BasePageProps> = ({
children,
...props
}: {
children: React.ReactNode;
}) => {
const theme = useTheme();
return (
<Stack
spacing={theme.spacing(10)}
{...props}
>
{children}
</Stack>
);
};
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<BasePageWithStatesProps> = ({
loading,
error,
items,
page,
actionLink,
children,
...props
}: BasePageWithStatesProps) => {
const { t } = useTranslation();
if (loading) {
return null;
}
if (error) {
return (
<ErrorFallback
title="Something went wrong..."
subtitle="Please try again later"
/>
);
}
if (isEmpty(items)) {
return (
<EmptyFallback
page={page}
title={t(`${page}Monitor.fallback.title`)}
bullets={t(`${page}Monitor.fallback.checks`, { returnObjects: true })}
actionButtonText={t(`${page}Monitor.fallback.actionButton`)}
actionLink={actionLink || ""}
/>
);
}
return <BasePage {...props}>{children}</BasePage>;
};

View File

@@ -1,45 +0,0 @@
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<string, string | undefined> = {
success: theme.palette.success.main,
error: theme.palette.error.main,
info: theme.palette.primary.contrastTextSecondary,
};
return (
<Stack
direction="row"
className="check"
gap={theme.spacing(6)}
alignItems="center"
>
<CheckOutlined />
<Typography
component="span"
color={
variant === "info"
? theme.palette.primary.contrastTextSecondary
: colors[variant]
}
fontWeight={450}
sx={{
opacity: 0.9,
}}
>
{text}
</Typography>
</Stack>
);
};

View File

@@ -1,23 +0,0 @@
export const Dot = ({
color = "gray",
size = "4px",
style,
}: {
color?: string;
size?: string;
style?: React.CSSProperties;
}) => {
return (
<span
style={{
content: '""',
width: size,
height: size,
borderRadius: "50%",
backgroundColor: color,
opacity: 0.8,
...style,
}}
/>
);
};

View File

@@ -1,158 +0,0 @@
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<BaseFallbackProps> = ({ children, ...props }) => {
const theme = useTheme();
const mode = useSelector((state: any) => state.ui.mode);
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
return (
<Box
margin={isSmall ? "inherit" : "auto"}
marginTop={isSmall ? "33%" : "auto"}
width={{
sm: "90%",
md: "70%",
lg: "50%",
xl: "40%",
}}
padding={theme.spacing(16)}
bgcolor={theme.palette.primary.main}
position="relative"
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
overflow="hidden"
sx={{
borderStyle: "dashed",
}}
{...props}
>
<Stack
alignItems="center"
gap={theme.spacing(20)}
sx={{
width: "fit-content",
margin: "auto",
marginTop: "100px",
}}
>
<Box
component="img"
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
bgcolor="transparent"
alt="Loading animation"
width="100%"
sx={{
zIndex: 1,
border: "none",
borderRadius: theme.spacing(8),
}}
/>
<Stack
gap={theme.spacing(4)}
alignItems="center"
maxWidth={"300px"}
zIndex={1}
>
{children}
</Stack>
</Stack>
</Box>
);
};
export const ErrorFallback = ({
title,
subtitle,
}: {
title: string;
subtitle: string;
}) => {
const theme = useTheme();
return (
<BaseFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{title}
</Typography>
<Typography>{subtitle}</Typography>
</BaseFallback>
);
};
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 (
<BaseFallback>
<Stack
gap={theme.spacing(10)}
zIndex={1}
alignItems="center"
>
<Typography
component="h1"
color={theme.palette.primary.contrastText}
>
{title}
</Typography>
<Stack
sx={{
flexWrap: "wrap",
gap: theme.spacing(2),
maxWidth: { xs: "90%", md: "80%", lg: "75%" },
}}
>
{bullets?.map((bullet: string, index: number) => (
<BulletPointCheck
text={bullet}
key={`${(page + "Monitors").trim().split(" ")[0]}-${index}`}
/>
))}
</Stack>
<Stack>
<Button
variant="contained"
color="accent"
onClick={() => navigate(actionLink)}
>
{actionButtonText}
</Button>
</Stack>
</Stack>
</BaseFallback>
);
};

View File

@@ -1,44 +0,0 @@
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 (
<Stack
width="26px"
height="24px"
alignItems="center"
justifyContent="center"
>
<Box
minWidth="18px"
minHeight="18px"
sx={{
position: "relative",
backgroundColor: color,
borderRadius: "50%",
"&::before": {
content: `""`,
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "inherit",
borderRadius: "50%",
animation: "ripple 1.8s ease-out infinite",
},
"&::after": {
content: `""`,
position: "absolute",
width: "7px",
height: "7px",
borderRadius: "50%",
backgroundColor: theme.palette.accent.contrastText,
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
},
}}
/>
</Stack>
);
};

View File

@@ -1,68 +0,0 @@
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 (
<Stack
direction={isSmall ? "column" : "row"}
bgcolor={theme.palette.primary.main}
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.spacing(2)}
>
<Box
padding={theme.spacing(15)}
borderRight={isSmall ? 0 : 1}
borderBottom={isSmall ? 1 : 0}
borderColor={theme.palette.primary.lowContrast}
flex={0.7}
>
{left}
</Box>
<Box
flex={1}
padding={theme.spacing(15)}
>
{right}
</Box>
</Stack>
);
};
export const ConfigBox = ({
title,
subtitle,
rightContent,
}: {
title: string;
subtitle: string;
rightContent: React.ReactNode;
}) => {
return (
<SplitBox
left={
<Fragment>
<Typography
component="h2"
variant="h2"
>
{title}
</Typography>
<Typography component="p">{subtitle}</Typography>
</Fragment>
}
right={rightContent}
/>
);
};

View File

@@ -1,57 +0,0 @@
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<GradientBox> = ({ 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 (
<BaseBox
sx={{
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
width: isSmall
? `calc(50% - (1 * ${theme.spacing(8)} / 2))`
: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
background: bg,
}}
>
{children}
</BaseBox>
);
};
type StatBoxProps = React.PropsWithChildren<{
title: string;
subtitle: string;
palette?: PaletteKey;
}>;
export const StatBox: React.FC<StatBoxProps> = ({
title,
subtitle,
palette,
children,
}) => {
const theme = useTheme();
const textColor = palette ? theme.palette[palette].contrastText : "inherit";
return (
<GradientBox palette={palette}>
<Stack>
<Typography color={textColor}>{title}</Typography>
<Typography color={textColor}>{subtitle}</Typography>
{children}
</Stack>
</GradientBox>
);
};

View File

@@ -1,100 +0,0 @@
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<StatusBoxProps> = ({ children }) => {
const theme = useTheme();
return (
<BaseBox
sx={{
overflow: "hidden",
position: "relative",
flex: 1,
padding: theme.spacing(8),
}}
>
<Box
position="absolute"
top="-10%"
left="5%"
>
<Background />
</Box>
{children}
</BaseBox>
);
};
const StatusBox = ({
label,
n,
color,
}: {
label: string;
n: number;
color: string | undefined;
}) => {
const theme = useTheme();
return (
<BGBox>
<Stack spacing={theme.spacing(8)}>
<Typography
variant={"h2"}
textTransform="uppercase"
color={theme.palette.primary.contrastTextTertiary}
>
{label}
</Typography>
<Typography
variant="h1"
color={color}
>
{n}
</Typography>
</Stack>
</BGBox>
);
};
export const UpStatusBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("monitorStatus.up")}
n={n}
color={theme.palette.success.lowContrast}
/>
);
};
export const DownStatusBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("monitorStatus.down")}
n={n}
color={theme.palette.error.lowContrast}
/>
);
};
export const PausedStatusBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("monitorStatus.paused")}
n={n}
color={theme.palette.warning.lowContrast}
/>
);
};

View File

@@ -1,41 +0,0 @@
import Box from "@mui/material/Box";
import { BaseBox } from "@/Components/v2/DesignElements";
import type { MonitorStatus } from "@/Types/Monitor";
import { getStatusPalette } from "@/Utils/v2/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 (
<BaseBox
sx={{
display: "inline-flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
padding: theme.spacing(3, 5),
color: theme.palette[palette].main,
borderColor: theme.palette[palette].lowContrast,
}}
>
<Box
width={7}
height={7}
bgcolor={theme.palette[palette].lowContrast}
borderRadius="50%"
marginRight="5px"
/>
{isActive === false ? "Paused" : transformedText}
</BaseBox>
);
};

View File

@@ -1,215 +0,0 @@
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<T> = {
id: number | string;
content: React.ReactNode;
onClick?: (event: React.MouseEvent<HTMLTableCellElement | null>, row: T) => void;
render: (row: T) => React.ReactNode;
};
type DataTableProps<T extends { id?: string | number; _id?: string | number }> = {
headers: Header<T>[];
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<T>) {
const theme = useTheme();
if (data.length === 0 || headers.length === 0) return <div>No data</div>;
return (
<TableContainer component={Paper}>
<Table
stickyHeader
sx={{
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
paddingLeft: theme.spacing(8),
},
"& :is(th)": {
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
},
"& :is(td)": {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastTextSecondary,
},
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
borderBottom: "none",
},
}}
>
<TableHead>
<TableRow>
{headers.map((header, idx) => {
return (
<TableCell
align={idx === 0 ? "left" : "center"}
key={header.id}
>
{header.content}
</TableCell>
);
})}
</TableRow>
</TableHead>
<TableBody>
{data.map((row) => {
const key = row.id || row._id || Math.random();
return (
<TableRow
key={key}
sx={{ cursor: onRowClick ? "pointer" : "default" }}
onClick={() => (onRowClick ? onRowClick(row) : null)}
>
{headers.map((header, index) => {
return (
<TableCell
align={index === 0 ? "left" : "center"}
key={header.id}
onClick={
header.onClick ? (e) => header.onClick!(e, row) : undefined
}
>
{header.render(row)}
</TableCell>
);
})}
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
);
}
interface TablePaginationActionsProps {
count: number;
page: number;
rowsPerPage: number;
onPageChange: (event: React.MouseEvent<HTMLButtonElement>, newPage: number) => void;
}
function TablePaginationActions(props: TablePaginationActionsProps) {
const theme = useTheme();
const { count, page, rowsPerPage, onPageChange } = props;
const handleFirstPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, 0);
};
const handleBackButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, page - 1);
};
const handleNextButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, page + 1);
};
const handleLastPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
};
return (
<Box
sx={{ flexShrink: 0, ml: 2.5 }}
className="table-pagination-actions"
>
<IconButton
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
{theme.direction === "rtl" ? <LastPageIcon /> : <FirstPageIcon />}
</IconButton>
<IconButton
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
{theme.direction === "rtl" ? <KeyboardArrowRight /> : <KeyboardArrowLeft />}
</IconButton>
<IconButton
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
{theme.direction === "rtl" ? <KeyboardArrowLeft /> : <KeyboardArrowRight />}
</IconButton>
<IconButton
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
{theme.direction === "rtl" ? <FirstPageIcon /> : <LastPageIcon />}
</IconButton>
</Box>
);
}
export const Pagination: React.FC<TablePaginationProps> = ({ ...props }) => {
const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm"));
const theme = useTheme();
return (
<TablePagination
ActionsComponent={TablePaginationActions}
rowsPerPageOptions={[5, 10, 25]}
{...props}
sx={{
"& .MuiTablePagination-toolbar": {
display: isSmall ? "grid" : "flex",
},
"& .MuiTablePagination-selectLabel": {
gridColumn: "1",
gridRow: "1",
justifySelf: "center",
},
"& .MuiTablePagination-select": {
gridColumn: "2",
gridRow: "1",
justifySelf: "center",
},
"& .MuiTablePagination-displayedRows": {
gridColumn: "2",
gridRow: "2",
justifySelf: "center ",
},
"& .table-pagination-actions": {
gridColumn: "1",
gridRow: "2",
justifySelf: "center",
},
"& .MuiSelect-select": {
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
},
}}
/>
);
};

View File

@@ -1,9 +0,0 @@
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";

View File

@@ -1,48 +0,0 @@
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<any, boolean, boolean, boolean>,
"renderInput"
> & {
renderInput?: AutocompleteProps<any, boolean, boolean, boolean>["renderInput"];
};
export const AutoCompleteInput: React.FC<AutoCompleteInputProps> = ({ ...props }) => {
const theme = useTheme();
return (
<Autocomplete
{...props}
disableCloseOnSelect
renderInput={(params) => (
<TextInput
{...params}
placeholder="Type to search"
/>
)}
getOptionKey={(option) => option._id}
renderTags={() => null}
renderOption={(props, option, { selected }) => {
const { key, ...optionProps } = props;
return (
<ListItem
key={key}
{...optionProps}
>
<CheckboxInput checked={selected} />
{option.name}
</ListItem>
);
}}
sx={{
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
padding: `0 ${theme.spacing(5)}`,
},
}}
/>
);
};

View File

@@ -1,11 +0,0 @@
import Button from "@mui/material/Button";
import type { ButtonProps } from "@mui/material/Button";
export const ButtonInput: React.FC<ButtonProps> = ({ sx, ...props }) => {
return (
<Button
{...props}
sx={{ textTransform: "none", height: 34, fontWeight: 400, borderRadius: 2, ...sx }}
/>
);
};

View File

@@ -1,13 +0,0 @@
import ButtonGroup from "@mui/material/ButtonGroup";
import type { ButtonGroupProps } from "@mui/material/ButtonGroup";
export const ButtonGroupInput: React.FC<ButtonGroupProps> = ({
orientation,
...props
}) => {
return (
<ButtonGroup
orientation={orientation}
{...props}
/>
);
};

View File

@@ -1,23 +0,0 @@
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<CheckboxInputProps> = ({ label, ...props }) => {
const theme = useTheme();
return (
<Checkbox
{...props}
icon={<CheckboxOutline />}
checkedIcon={<CheckboxFilled />}
sx={{
"&:hover": { backgroundColor: "transparent" },
"& svg": { width: theme.spacing(8), height: theme.spacing(8) },
}}
/>
);
};

View File

@@ -1,64 +0,0 @@
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<string, string> = {
cs: "cz",
ja: "jp",
uk: "ua",
vi: "vn",
};
const handleChange = (event: SelectChangeEvent<unknown>) => {
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 (
<MenuItem
key={l}
value={l}
>
<Stack
direction="row"
gap={theme.spacing(4)}
>
{flag && <span className={flag} />}
<Typography textTransform={"uppercase"}>{l}</Typography>
</Stack>
</MenuItem>
);
});
return (
<Select
value={language}
onChange={handleChange}
>
{languagesForDisplay}
</Select>
);
};

View File

@@ -1,66 +0,0 @@
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<RadioInputProps> = ({ ...props }) => {
const theme = useTheme();
return (
<Radio
{...props}
checkedIcon={<RadioChecked />}
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 (
<FormControlLabel
control={<RadioInput {...props} />}
label={
<>
<Typography component="p">{label}</Typography>
<Typography
component="h6"
color={theme.palette.primary.contrastTextSecondary}
>
{description}
</Typography>
</>
}
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),
},
}}
/>
);
};

View File

@@ -1,88 +0,0 @@
import { Typography, Select } from "@mui/material";
import MenuItem from "@mui/material/MenuItem";
import type { SelectProps } from "@mui/material/Select";
import { useTheme } from "@mui/material/styles";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
export const SelectInput: React.FC<SelectProps> = ({ ...props }) => {
const theme = useTheme();
return (
<Select
{...props}
sx={{
height: "34px",
"& .MuiOutlinedInput-notchedOutline": {
borderRadius: theme.shape.borderRadius,
borderColor: theme.palette.primary.lowContrast,
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.primary.lowContrast,
},
}}
/>
);
};
type ItemTypes = string | number;
interface SelectItem {
_id: ItemTypes;
name: string;
}
export type CustomSelectProps = SelectProps & {
items: SelectItem[];
placeholder?: string;
isHidden?: boolean;
hasError?: boolean;
};
export const SelectFromItems: React.FC<CustomSelectProps> = ({
items,
placeholder,
isHidden = false,
hasError = false,
...props
}) => {
return (
<SelectInput
error={hasError}
IconComponent={KeyboardArrowDownIcon}
displayEmpty
MenuProps={{ disableScrollLock: true }}
renderValue={(selected) => {
if (!selected) {
return (
<Typography
noWrap
color="text.secondary"
>
{placeholder ?? ""}
</Typography>
);
}
const selectedItem = items.find((item) => item._id === selected);
const displayName = selectedItem ? selectedItem.name : placeholder;
return (
<Typography
noWrap
title={displayName}
>
{displayName}
</Typography>
);
}}
{...props}
>
{items.map((item) => (
<MenuItem
key={item._id}
value={item._id}
>
{item.name}
</MenuItem>
))}
</SelectInput>
);
};
SelectInput.displayName = "SelectInput";
SelectFromItems.displayName = "SelectFromItems";

View File

@@ -1,22 +0,0 @@
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<HTMLInputElement, TextFieldProps>(
function TextInput(props, ref) {
return (
<TextField
{...props}
inputRef={ref}
sx={{
"& .MuiOutlinedInput-root": {
height: 34,
fontSize: typographyLevels.base,
},
}}
/>
);
}
);
TextInput.displayName = "TextInput";

View File

@@ -1,37 +0,0 @@
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 (
<Stack
direction="row"
gap={theme.spacing(4)}
>
<Typography>{text}</Typography>
<Link
color="accent"
to={href}
component={RouterLink}
target={target}
>
{linkText}
</Link>
</Stack>
);
};

View File

@@ -1,135 +0,0 @@
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 (
<svg
className="sun-and-moon"
aria-hidden="true"
width="24"
height="24"
viewBox="0 0 24 24"
>
<mask
className="moon"
id="moon-mask"
>
<rect
x="0"
y="0"
width="100%"
height="100%"
fill="#fff"
/>
<circle
cx="24"
cy="10"
r="6"
fill="#000"
/>
</mask>
<circle
className="sun"
cx="12"
cy="12"
r="6"
fill={theme.palette.primary.contrastTextSecondary}
mask="url(#moon-mask)"
/>
<g
className="sun-beams"
stroke={theme.palette.primary.contrastTextSecondary}
>
<line
x1="12"
y1="1"
x2="12"
y2="3"
/>
<line
x1="12"
y1="21"
x2="12"
y2="23"
/>
<line
x1="4.22"
y1="4.22"
x2="5.64"
y2="5.64"
/>
<line
x1="18.36"
y1="18.36"
x2="19.78"
y2="19.78"
/>
<line
x1="1"
y1="12"
x2="3"
y2="12"
/>
<line
x1="21"
y1="12"
x2="23"
y2="12"
/>
<line
x1="4.22"
y1="19.78"
x2="5.64"
y2="18.36"
/>
<line
x1="18.36"
y1="5.64"
x2="19.78"
y2="4.22"
/>
</g>
</svg>
);
};
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 (
<IconButton
id="theme-toggle"
title={t("common.buttons.toggleTheme")}
className={`theme-${mode}`}
aria-label="auto"
aria-live="polite"
onClick={handleChange}
sx={{
width,
height,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<SunAndMoonIcon />
</IconButton>
);
};

View File

@@ -1,7 +0,0 @@
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";

View File

@@ -1,23 +0,0 @@
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 (
<Stack
direction="row"
minHeight="100vh"
>
<SideBar />
<Stack
flex={1}
padding={theme.spacing(12)}
>
<Outlet />
</Stack>
</Stack>
);
};
export default RootLayout;

View File

@@ -1,16 +0,0 @@
import { useTheme } from "@mui/material/styles";
import Stack from "@mui/material/Stack";
export const BottomControls = ({}) => {
const theme = useTheme();
return (
<Stack
direction="row"
height={50}
py={theme.spacing(4)}
px={theme.spacing(8)}
gap={theme.spacing(2)}
></Stack>
);
};

View File

@@ -1,50 +0,0 @@
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 ? (
<ArrowRight
height={theme.spacing(8)}
width={theme.spacing(8)}
color={theme.palette.primary.contrastTextSecondary}
/>
) : (
<ArrowLeft
height={theme.spacing(8)}
width={theme.spacing(8)}
color={theme.palette.primary.contrastTextSecondary}
/>
);
return (
<IconButton
sx={{
position: "absolute",
/* TODO 60 is a magic number. if logo chnges size this might break */
top: 60,
right: 0,
transform: `translate(50%, 0)`,
backgroundColor: theme.palette.tertiary.main,
border: `1px solid ${theme.palette.primary.lowContrast}`,
p: theme.spacing(2.5),
"&:focus": { outline: "none" },
"&:hover": {
backgroundColor: theme.palette.primary.lowContrast,
borderColor: theme.palette.primary.lowContrast,
},
}}
onClick={() => {
dispatch(toggleSidebar());
}}
>
{arrowIcon}
</IconButton>
);
};

View File

@@ -1,59 +0,0 @@
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 (
<Stack
pt={theme.spacing(6)}
pb={theme.spacing(12)}
pl={theme.spacing(8)}
direction="row"
alignItems="center"
gap={theme.spacing(4)}
onClick={() => navigate("/")}
sx={{ cursor: "pointer" }}
>
<Typography
pl={theme.spacing("1px")}
minWidth={theme.spacing(16)}
minHeight={theme.spacing(16)}
display={"flex"}
justifyContent={"center"}
alignItems={"center"}
bgcolor={theme.palette.accent.main}
borderRadius={theme.shape.borderRadius}
color={theme.palette.accent.contrastText}
fontSize={18}
>
C
</Typography>
<Box
overflow={"hidden"}
sx={{
transition: "opacity 900ms ease, width 900ms ease",
opacity: collapsed ? 0 : 1,
whiteSpace: "nowrap",
width: collapsed ? 0 : "100%",
}}
>
{" "}
<Typography
lineHeight={1}
mt={theme.spacing(2)}
color={theme.palette.primary.contrastText}
variant="h2"
>
{t("common.appName")}
</Typography>
</Box>
</Stack>
);
};

View File

@@ -1,58 +0,0 @@
// 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: <Monitors /> },
// { name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
// { name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
// {
// name: t("menu.notifications"),
// path: "notifications",
// icon: <Notifications />,
// },
// { name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
// { name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
// { name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
// { name: t("menu.logs"), path: "logs", icon: <Logs /> },
// {
// name: t("menu.settings"),
// icon: <Settings />,
// path: "settings",
// },
];
export const getBottomMenu = (t: Function) => [
{ name: t("menu.support"), path: "support", icon: <Support />, url: "invite" },
{
name: t("menu.discussions"),
path: "discussions",
icon: <Discussions />,
url: "https://github.com/bluewave-labs/checkmate/discussions",
},
{
name: t("menu.docs"),
path: "docs",
icon: <Docs />,
url: "https://bluewavelabs.gitbook.io/checkmate",
},
{
name: t("menu.changelog"),
path: "changelog",
icon: <ChangeLog />,
url: "https://github.com/bluewave-labs/checkmate/releases",
},
];

View File

@@ -1,103 +0,0 @@
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 (
<Tooltip
placement="right"
title={collapsed ? item.name : ""}
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -16],
},
},
],
},
}}
disableInteractive
>
<ListItemButton
sx={{
backgroundColor: buttonBgColor,
"&:hover": {
backgroundColor: buttonBgHoverColor,
},
height: 37,
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
px: theme.spacing(4),
pl: theme.spacing(5),
}}
onClick={onClick}
>
<ListItemIcon
sx={{
minWidth: 0,
"& svg": {
height: 20,
width: 20,
opacity: 0.81,
},
"& svg path": {
stroke: iconStroke,
},
}}
>
{item.icon}
</ListItemIcon>
<Box
sx={{
overflow: "hidden",
transition: "opacity 900ms ease",
opacity: collapsed ? 0 : 1,
whiteSpace: "nowrap",
}}
>
<Typography
variant="body1"
color={theme.palette.primary.contrastText}
sx={{
fontWeight: fontWeight,
opacity: 0.9,
}}
>
{item.name}
</Typography>
</Box>
</ListItemButton>
</Tooltip>
);
};

View File

@@ -1,106 +0,0 @@
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 (
<Stack
component="aside"
position="sticky"
top={0}
minHeight={"100vh"}
maxHeight={"100vh"}
paddingTop={theme.spacing(6)}
paddingBottom={theme.spacing(6)}
gap={theme.spacing(6)}
borderRight={`1px solid ${theme.palette.primary.lowContrast}`}
width={collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH}
sx={{
transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)",
}}
>
<CollapseButton collapsed={collapsed} />
<Logo collapsed={collapsed} />
<List
component="nav"
disablePadding
sx={{
px: theme.spacing(6),
height: "100%",
}}
>
{menu.map((item) => {
const selected = location.pathname.startsWith(`/${item.path}`);
return (
<NavItem
key={item.path}
item={item}
collapsed={collapsed}
selected={selected}
onClick={() => navigate(`/${item.path}`)}
/>
);
})}
</List>
<List
component="nav"
disablePadding
sx={{
px: theme.spacing(6),
}}
>
{bottomMenu.map((item) => {
const selected = location.pathname.startsWith(`/${item.path}`);
return (
<NavItem
key={item.path}
item={item}
collapsed={collapsed}
selected={selected}
onClick={() => {
if (item.url) {
window.open(item.url, "_blank", "noreferrer");
} else {
navigate(`/${item.path}`);
}
}}
/>
);
})}
</List>
<Divider sx={{ mt: "auto", borderColor: theme.palette.primary.lowContrast }} />
<BottomControls />
</Stack>
);
};

View File

@@ -1,84 +0,0 @@
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/v2/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<string, string> = {
success: "Excellent",
warning: "Average",
danger: "Poor",
};
return (
<BaseChart
icon={<AverageResponseIcon />}
title={"Average response time"}
>
<Stack
height="100%"
position={"relative"}
justifyContent={"space-between"}
>
<ResponsiveContainer
width="100%"
height={155}
>
<RadialBarChart
cy="89%"
data={chartData}
startAngle={180}
endAngle={0}
innerRadius={"120%"}
outerRadius={"200%"}
>
<RadialBar
dataKey="value"
background={{ fill: theme.palette[palette].lowContrast }}
>
<Cell visibility={"hidden"} />
<Cell fill={theme.palette[palette].main} />
</RadialBar>
</RadialBarChart>
</ResponsiveContainer>
<Stack
direction={"row"}
justifyContent={"space-between"}
>
<Typography variant="body2">Low</Typography>
<Typography variant="body2">High</Typography>
</Stack>
<Stack
position="absolute"
top={"50%"}
right={"50%"}
sx={{
transform: "translate(50%, 0%)",
}}
>
<Typography
variant="h6"
textAlign={"center"}
>
{msg[palette]}
</Typography>
<Typography
variant="h6"
textAlign={"center"}
>{`${avg?.toFixed()}ms`}</Typography>
</Stack>
</Stack>
</BaseChart>
);
};

View File

@@ -1,163 +0,0 @@
import { BaseChart } from "./HistogramStatus";
import { BaseBox } from "../DesignElements";
import ResponseTimeIcon from "@/assets/icons/response-time-icon.svg?react";
import { normalizeResponseTimes } from "@/Utils/v2/DataUtils";
import {
AreaChart,
Area,
XAxis,
Tooltip,
CartesianGrid,
ResponsiveContainer,
Text,
} from "recharts";
import Typography from "@mui/material/Typography";
import {
formatDateWithTz,
tickDateFormatLookup,
tooltipDateFormatLookup,
} from "@/Utils/v2/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<XTickProps> = ({ x, y, payload, range }) => {
const format = tickDateFormatLookup(range);
const theme = useTheme();
const uiTimezone = useSelector((state: any) => state.ui.timezone);
return (
<Text
x={x}
y={y + 10}
textAnchor="middle"
fill={theme.palette.primary.contrastTextTertiary}
fontSize={11}
fontWeight={400}
>
{formatDateWithTz(payload?.value, format, uiTimezone)}
</Text>
);
};
type ResponseTimeToolTipProps = {
active?: boolean | undefined;
payload?: any[];
label?: string;
range: string;
theme: any;
uiTimezone: string;
};
const ResponseTimeToolTip: React.FC<ResponseTimeToolTipProps> = ({
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 (
<BaseBox sx={{ py: theme.spacing(2), px: theme.spacing(4) }}>
<Typography>{formatDateWithTz(label, format, uiTimezone)}</Typography>
<Typography>Response time: {responseTime} ms</Typography>
</BaseBox>
);
};
export const ChartResponseTime = ({
checks,
range,
}: {
checks: GroupedCheck[];
range: string;
}) => {
const theme = useTheme();
const uiTimezone = useSelector((state: any) => state.ui.timezone);
const normalized = normalizeResponseTimes<GroupedCheck, "avgResponseTime">(
checks,
"avgResponseTime"
);
return (
<BaseChart
icon={<ResponseTimeIcon />}
title="Response times"
>
<ResponsiveContainer
width="100%"
height={300}
>
<AreaChart data={normalized?.slice().reverse()}>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<defs>
<linearGradient
id="colorUv"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme.palette.accent.main}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.accent.light}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
axisLine={false}
tickLine={false}
dataKey="_id"
tick={(props) => (
<XTick
{...props}
range={range}
/>
)}
/>
<Tooltip
content={(props) => (
<ResponseTimeToolTip
{...props}
range={range}
theme={theme}
uiTimezone={uiTimezone}
/>
)}
/>
<Area
type="monotone"
dataKey="normalResponseTime"
stroke={theme.palette.accent.main}
fill="url(#colorUv)"
/>
</AreaChart>
</ResponsiveContainer>
</BaseChart>
);
};

View File

@@ -1,67 +0,0 @@
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 (
<Stack
direction={isSmall ? "column" : "row"}
spacing={isSmall ? theme.spacing(4) : 0}
justifyContent={"space-between"}
>
<MonitorStatus monitor={monitor} />
<Stack
direction={"row"}
spacing={theme.spacing(2)}
>
<ButtonGroup
orientation={isSmall ? "vertical" : "horizontal"}
fullWidth={isSmall}
variant="contained"
color="secondary"
>
<Button startIcon={<EmailIcon />}>{t("sendTestNotifications")}</Button>
<Button startIcon={<BugReportOutlinedIcon />}>{t("menu.incidents")}</Button>
<Button
loading={isPatching}
onClick={async () => {
await patch(`/monitors/${monitor._id}/active`);
refetch();
}}
startIcon={
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
}
>
{monitor?.isActive ? t("pause") : t("resume")}
</Button>
<Button startIcon={<SettingsOutlinedIcon />}>{t("configure")}</Button>
</ButtonGroup>
</Stack>
</Stack>
);
};

View File

@@ -1,35 +0,0 @@
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 (
<Stack
direction="row"
justifyContent="end"
alignItems="center"
gap={theme.spacing(6)}
>
<Button
loading={isLoading}
variant="contained"
color="accent"
onClick={() => navigate(path)}
>
{label || t("createNew")}
</Button>
</Stack>
);
};

View File

@@ -1,62 +0,0 @@
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 (
<Stack
gap={theme.spacing(9)}
direction={isSmall ? "column" : "row"}
alignItems={"center"}
justifyContent="flex-end"
>
<Typography variant="body2">{`Showing statistics for past ${range}`}</Typography>
<ButtonGroup
orientation={isSmall ? "vertical" : "horizontal"}
fullWidth={isSmall}
variant="contained"
color={"primary"}
>
<Button
color={range === "2h" ? "secondary" : "inherit"}
onClick={() => setRange("2h")}
loading={loading}
>
Recent
</Button>
<Button
color={range === "24h" ? "secondary" : "inherit"}
onClick={() => setRange("24h")}
loading={loading}
>
Day
</Button>
<Button
color={range === "7d" ? "secondary" : "inherit"}
onClick={() => setRange("7d")}
loading={loading}
>
7 days
</Button>
<Button
color={range === "30d" ? "secondary" : "inherit"}
onClick={() => setRange("30d")}
loading={loading}
>
30 days
</Button>
</ButtonGroup>
</Stack>
);
};

View File

@@ -1,87 +0,0 @@
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/v2/DataUtils";
export const HistogramResponseTime = ({ checks }: { checks: Check[] }) => {
const normalChecks = normalizeResponseTimes(checks, "responseTime");
let data = Array<any>();
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 (
<Stack
direction="row"
flexWrap="nowrap"
gap={theme.spacing(1.5)}
height="50px"
width="fit-content"
onClick={(event) => event.stopPropagation()}
sx={{
cursor: "default",
}}
>
{data.map((check, index) => {
if (check === "placeholder") {
return (
<Box
key={`${check}-${index}`}
position="relative"
width={theme.spacing(4.5)}
height="100%"
bgcolor={theme.palette.primary.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
}}
/>
);
} else {
return (
<HistogramResponseTimeTooltip
key={`${check}-${index}`}
check={check}
>
<Box
position="relative"
width="9px"
height="100%"
bgcolor={theme.palette.primary.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
}}
>
<Box
position="absolute"
bottom={0}
width="100%"
height={`${check.normalResponseTime}%`}
bgcolor={
check.status
? theme.palette.success.lowContrast
: theme.palette.error.lowContrast
}
sx={{
borderRadius: theme.spacing(1.5),
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
</Box>
</HistogramResponseTimeTooltip>
);
}
})}
</Stack>
);
};

View File

@@ -1,29 +0,0 @@
import Stack from "@mui/material/Stack";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import { formatDateWithTz } from "@/Utils/v2/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 (
<Tooltip
title={
<Stack>
<Typography>
{formatDateWithTz(check?.checkedAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
</Typography>
{check?.responseTime && (
<Typography>Response Time: {check.responseTime} ms</Typography>
)}
</Stack>
}
>
{children}
</Tooltip>
);
};

View File

@@ -1,218 +0,0 @@
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/v2/DataUtils";
import { useState } from "react";
import { formatDateWithTz } from "@/Utils/v2/TimeUtils";
import { useSelector } from "react-redux";
import { useTheme } from "@mui/material/styles";
import { getResponseTimeColor } from "@/Utils/v2/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 (
<>
<text
x={0}
y="100%"
dy={-3}
textAnchor="start"
fontSize={11}
fill={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(p1._id, dateFormat, uiTimezone)}
</text>
<text
x="100%"
y="100%"
dy={-3}
textAnchor="end"
fontSize={11}
fill={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(p2._id, dateFormat, uiTimezone)}
</text>
</>
);
};
type BaseChartProps = React.PropsWithChildren<{
icon: React.ReactNode;
title: string;
}>;
export const BaseChart: React.FC<BaseChartProps> = ({ children, icon, title }) => {
const theme = useTheme();
return (
<BaseBox
sx={{
padding: theme.spacing(8),
display: "flex",
flex: 1,
}}
>
<Stack
gap={theme.spacing(8)}
flex={1}
>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(4)}
>
<BaseBox
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 34,
height: 34,
backgroundColor: theme.palette.tertiary.main,
"& svg": {
width: 20,
height: 20,
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
},
}}
>
{icon}
</BaseBox>
<Typography variant="h2">{title}</Typography>
</Stack>
<Box flex={1}>{children}</Box>
</Stack>
</BaseBox>
);
};
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" ? <UptimeIcon /> : <IncidentsIcon />;
const theme = useTheme();
const [idx, setIdx] = useState<number | null>(null);
const dateFormat = range === "1d" || range === "2h" ? "MMM D, h A" : "MMM D";
const normalChecks = normalizeResponseTimes(checks, "avgResponseTime");
if (normalChecks.length === 0) {
return (
<BaseChart
icon={icon}
title={title}
>
<Stack
height={"100%"}
alignItems={"center"}
justifyContent={"center"}
>
<Typography variant="h2">
{status === "up" ? "No checks yet" : "Great, no downtime yet!"}
</Typography>
</Stack>
</BaseChart>
);
}
const totalChecks = normalChecks.reduce((count, check) => {
return count + check.count;
}, 0);
return (
<BaseChart
icon={icon}
title={title}
>
<Stack gap={theme.spacing(8)}>
<Stack
position="relative"
direction="row"
justifyContent="space-between"
>
<Stack>
<Typography>Total checks</Typography>
{idx ? (
<Stack>
<Typography variant="h2">{normalChecks[idx].count}</Typography>
<Typography
position={"absolute"}
top={"100%"}
>
{formatDateWithTz(normalChecks[idx]._id, dateFormat, uiTimezone)}
</Typography>
</Stack>
) : (
<Typography variant="h2">{totalChecks}</Typography>
)}
</Stack>
</Stack>
<ResponsiveContainer
width="100%"
height={155}
>
<BarChart data={normalChecks}>
<XAxis
stroke={theme.palette.primary.lowContrast}
height={15}
tick={false}
label={
<XLabel
p1={normalChecks[0]}
p2={normalChecks[normalChecks.length - 1]}
range={range}
/>
}
/>
<Bar
dataKey="normalResponseTime"
maxBarSize={7}
background={{ fill: "transparent" }}
>
{normalChecks?.map((groupedCheck, idx) => {
const fillColor = getResponseTimeColor(groupedCheck.normalResponseTime);
return (
<Cell
onMouseEnter={() => setIdx(idx)}
onMouseLeave={() => setIdx(null)}
key={groupedCheck._id}
fill={theme.palette[fillColor].main}
/>
);
})}
</Bar>
</BarChart>
</ResponsiveContainer>
</Stack>
</BaseChart>
);
};

View File

@@ -1,60 +0,0 @@
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/v2/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 (
<Stack>
<Typography
fontSize={typographyLevels.xl}
fontWeight={500}
color={theme.palette.primary.contrastText}
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
>
{monitor.name}
</Typography>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(4)}
>
<PulseDot color={getStatusColor(monitor.status, theme)} />
<Typography
color={theme.palette.primary.contrastTextSecondary}
fontSize={typographyLevels.l}
fontWeight={"bolder"}
fontFamily={"monospace"}
overflow={"hidden"}
textOverflow={"ellipsis"}
whiteSpace={"nowrap"}
maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"}
>
{formatUrl(monitor?.url)}
</Typography>
{!isSmall && (
<>
<Dot />
<Typography>
Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })}
</Typography>
</>
)}
</Stack>
</Stack>
);
};

View File

@@ -1 +0,0 @@
export { HeaderCreate } from "./HeaderCreate";

View File

@@ -1,19 +0,0 @@
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;

View File

@@ -1,90 +0,0 @@
import { useState } from "react";
import useSWR from "swr";
import type { SWRConfiguration } from "swr";
import type { AxiosRequestConfig } from "axios";
import { get, post, patch } from "@/Utils/v2/ApiClient"; // your axios wrapper
export type ApiResponse = {
message: string;
data: any;
};
// Generic fetcher for GET requests
const fetcher = async <T,>(url: string, config?: AxiosRequestConfig) => {
const res = await get<T>(url, config);
return res.data;
};
export const useGet = <T,>(
url: string,
axiosConfig?: AxiosRequestConfig,
swrConfig?: SWRConfiguration<T, Error>
) => {
const { data, error, isLoading, isValidating, mutate } = useSWR<T>(
url,
(url) => fetcher<T>(url, axiosConfig),
swrConfig
);
return {
response: data ?? null,
loading: isLoading,
isValidating,
error: error?.message ?? null,
refetch: mutate,
};
};
export const usePost = <B = any, R = any>() => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const postFn = async (
endpoint: string,
body: B,
config?: AxiosRequestConfig
): Promise<R | null> => {
setLoading(true);
setError(null);
try {
const res = await post<R>(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 = <B = any, R = any>() => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const patchFn = async (
endpoint: string,
body?: B,
config?: AxiosRequestConfig
): Promise<R | null> => {
setLoading(true);
setError(null);
try {
const res = await patch<R>(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 };
};

View File

@@ -1,17 +0,0 @@
import { monitorSchema } from "@/Validation/v2/zod";
import { z } from "zod";
export const useInitForm = ({
initialData,
}: {
initialData: Partial<z.infer<typeof monitorSchema>> | undefined;
}) => {
const defaults: z.infer<typeof monitorSchema> = {
type: initialData?.type || "http",
url: initialData?.url || "",
n: initialData?.n || 3,
notificationChannels: initialData?.notificationChannels || [],
name: initialData?.name || "",
interval: initialData?.interval || "1 minute",
};
return { defaults };
};

View File

@@ -1,136 +0,0 @@
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<typeof schema>;
const Login = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const theme = useTheme();
const { post, loading } = usePost<FormData, ApiResponse>();
const navigate = useNavigate();
const {
handleSubmit,
control,
formState: { errors },
} = useForm<FormData>({
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 (
<AuthBasePage
title={t("auth.login.welcome")}
subtitle={t("auth.login.heading")}
>
<Stack
width={"100%"}
alignItems={"center"}
justifyContent={"center"}
gap={theme.spacing(8)}
>
<Stack
component="form"
padding={theme.spacing(8)}
gap={theme.spacing(12)}
onSubmit={handleSubmit(onSubmit)}
maxWidth={400}
sx={{
width: {
sm: "80%",
md: "70%",
lg: "65%",
xl: "65%",
},
}}
>
<Controller
name="email"
control={control}
defaultValue=""
render={({ field }) => (
<TextInput
{...field}
label={t("auth.common.inputs.email.label")}
fullWidth
placeholder={t("auth.common.inputs.email.placeholder")}
error={!!errors.email}
helperText={errors.email ? errors.email.message : ""}
/>
)}
/>
<Controller
name="password"
control={control}
defaultValue=""
render={({ field }) => (
<TextInput
{...field}
type="password"
label={t("auth.common.inputs.password.label")}
fullWidth
placeholder="••••••••••"
error={!!errors.password}
helperText={errors.password ? errors.password.message : ""}
/>
)}
/>
<Button
variant="contained"
loading={loading}
color="accent"
type="submit"
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
>
Login
</Button>
</Stack>
<TextLink
text={t("auth.login.links.forgotPassword")}
linkText={t("auth.login.links.forgotPasswordLink")}
href="/forgot-password"
/>
<TextLink
text={t("auth.login.links.register")}
linkText={t("auth.login.links.registerLink")}
href="/register"
/>
</Stack>
</AuthBasePage>
);
};
export default Login;

View File

@@ -1,177 +0,0 @@
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<typeof schema>;
const Register = () => {
const { t } = useTranslation();
const theme = useTheme();
const navigate = useNavigate();
const { post, loading, error } = usePost<FormData, ApiResponse>();
const {
handleSubmit,
control,
formState: { errors },
} = useForm<FormData>({
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 (
<AuthBasePage
title={t("auth.registration.welcome")}
subtitle={t("auth.registration.heading.user")}
>
<Stack
alignItems={"center"}
width={"100%"}
>
<Stack
component="form"
padding={theme.spacing(8)}
gap={theme.spacing(12)}
onSubmit={handleSubmit(onSubmit)}
maxWidth={400}
sx={{
width: {
sm: "80%",
md: "70%",
lg: "65%",
xl: "65%",
},
}}
>
<Controller
name="email"
control={control}
defaultValue=""
render={({ field }) => (
<TextInput
{...field}
label={t("auth.common.inputs.email.label")}
fullWidth
placeholder={t("auth.common.inputs.email.placeholder")}
error={!!errors.email}
helperText={errors.email ? errors.email.message : ""}
/>
)}
/>
<Controller
name="firstName"
control={control}
defaultValue=""
render={({ field }) => (
<TextInput
{...field}
label={t("auth.common.inputs.firstName.label")}
fullWidth
placeholder={t("auth.common.inputs.firstName.placeholder")}
error={!!errors.firstName}
helperText={errors.firstName ? errors.firstName.message : ""}
/>
)}
/>
<Controller
name="lastName"
control={control}
defaultValue=""
render={({ field }) => (
<TextInput
{...field}
label={t("auth.common.inputs.lastName.label")}
fullWidth
placeholder={t("auth.common.inputs.lastName.placeholder")}
error={!!errors.lastName}
helperText={errors.lastName ? errors.lastName.message : ""}
/>
)}
/>
<Controller
name="password"
control={control}
defaultValue=""
render={({ field }) => (
<TextInput
{...field}
type="password"
label={t("auth.common.inputs.password.label")}
fullWidth
placeholder="••••••••••"
error={!!errors.password}
helperText={errors.password ? errors.password.message : ""}
/>
)}
/>
<Controller
name="confirmPassword"
control={control}
defaultValue=""
render={({ field }) => (
<TextInput
{...field}
type="password"
label={t("auth.common.inputs.passwordConfirm.label")}
fullWidth
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
error={!!errors.confirmPassword}
helperText={errors.confirmPassword ? errors.confirmPassword.message : ""}
/>
)}
/>
<Button
variant="contained"
loading={loading}
color="accent"
type="submit"
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
>
Register
</Button>
{error && <Typography color="error">{error}</Typography>}
</Stack>
</Stack>
</AuthBasePage>
);
};
export default Register;

View File

@@ -1,93 +0,0 @@
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/v2/TimeUtils";
import { useSelector } from "react-redux";
const getHeaders = (t: Function, uiTimezone: string) => {
const headers: Header<Check>[] = [
{
id: "status",
content: t("status"),
render: (row) => {
return <StatusLabel status={row.status as MonitorStatus} />;
},
},
{
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<ApiResponse>(
`/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<HTMLButtonElement> | null,
newPage: number
) => {
setPage(newPage);
};
const handleRowsPerPageChange = (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => {
const value = Number(e.target.value);
setPage(0);
setRowsPerPage(value);
};
if (error) {
console.error(error);
}
return (
<Box>
<Table
headers={headers}
data={checks}
/>
<Pagination
component="div"
count={count}
page={page}
rowsPerPage={rowsPerPage}
onPageChange={handlePageChange}
onRowsPerPageChange={handleRowsPerPageChange}
/>
</Box>
);
};

View File

@@ -1,43 +0,0 @@
import { monitorSchema } from "@/Validation/v2/zod";
import { useGet, usePost } from "@/Hooks/v2/UseApi";
import { UptimeForm } from "@/Pages/v2/Uptime/UptimeForm";
import { useParams } from "react-router";
import type { ApiResponse } from "@/Hooks/v2/UseApi";
import humanInterval from "human-interval";
import { z } from "zod";
export const UptimeConfigurePage = () => {
type FormValues = z.infer<typeof monitorSchema>;
type SubmitValues = Omit<FormValues, "interval"> & { interval: number | undefined };
const { id } = useParams();
const { response } = useGet<ApiResponse>("/notification-channels");
const { response: monitorResponse } = useGet<ApiResponse>(`/monitors/${id}`);
const monitor = monitorResponse?.data || null;
const notificationOptions = response?.data ?? [];
const { post, loading, error } = usePost<SubmitValues, ApiResponse>();
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);
}
};
return (
<UptimeForm
initialData={{
...monitor,
}}
onSubmit={onSubmit}
notificationOptions={notificationOptions}
loading={loading}
/>
);
};

View File

@@ -1,262 +0,0 @@
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<typeof monitorSchema>;
type SubmitValues = Omit<FormValues, "interval"> & { interval: number | undefined };
const {
handleSubmit,
control,
setValue,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(monitorSchema) as any,
defaultValues: {
type: "http",
url: "",
n: 3,
notificationChannels: [],
name: "",
interval: "1 minute",
},
mode: "onChange",
});
const { response } = useGet<ApiResponse>("/notification-channels");
const { post, loading, error } = usePost<SubmitValues>();
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 (
<form onSubmit={handleSubmit(onSubmit)}>
<BasePage>
<ConfigBox
title={t("distributedUptimeCreateChecks")}
subtitle={t("distributedUptimeCreateChecksDescription")}
rightContent={
<Controller
name="type"
control={control}
render={({ field }) => (
<FormControl error={!!errors.type}>
<RadioGroup
{...field}
sx={{ gap: theme.spacing(6) }}
>
<RadioWithDescription
value="http"
label={"HTTP"}
description={"Use HTTP to monitor your website or API endpoint."}
/>
<RadioWithDescription
value="https"
label="HTTPS"
description="Use HTTPS to monitor your website or API endpoint.
"
/>
<RadioWithDescription
value="ping"
label={t("pingMonitoring")}
description={t("pingMonitoringDescription")}
/>
</RadioGroup>
</FormControl>
)}
/>
}
/>
<ConfigBox
title={t("settingsGeneralSettings")}
subtitle={t(`uptimeGeneralInstructions.${selectedType}`)}
rightContent={
<Stack gap={theme.spacing(8)}>
<Controller
name="url"
control={control}
render={({ field }) => (
<TextInput
{...field}
type="text"
label={t("url")}
fullWidth
error={!!errors.url}
helperText={errors.url ? errors.url.message : ""}
/>
)}
/>
<Controller
name="name"
control={control}
render={({ field }) => (
<TextInput
{...field}
type="text"
label={t("displayName")}
fullWidth
error={!!errors.name}
helperText={errors.name ? errors.name.message : ""}
/>
)}
/>
</Stack>
}
/>
<ConfigBox
title={t("createMonitorPage.incidentConfigTitle")}
subtitle={t("createMonitorPage.incidentConfigDescriptionV2")}
rightContent={
<Controller
name="n"
control={control}
render={({ field }) => (
<TextInput
{...field}
type="number"
label={t("createMonitorPage.incidentConfigStatusCheckNumber")}
fullWidth
error={!!errors.n}
helperText={errors.n ? errors.n.message : ""}
onChange={(e) => {
const target = e.target as HTMLInputElement;
field.onChange(target.valueAsNumber);
}}
/>
)}
/>
}
/>
<ConfigBox
title={t("notificationConfig.title")}
subtitle={t("notificationConfig.description")}
rightContent={
<Stack>
<Controller
name="notificationChannels"
control={control}
defaultValue={[]} // important!
render={({ field }) => (
<AutoCompleteInput
multiple
options={notificationOptions}
getOptionLabel={(option) => option.name}
value={notificationOptions.filter((o: any) =>
(field.value || []).includes(o._id)
)}
onChange={(_, newValue) => {
field.onChange(newValue.map((o: any) => o._id));
}}
/>
)}
/>
<Stack
gap={theme.spacing(2)}
mt={theme.spacing(2)}
>
{notificationChannels.map((notificationId) => {
const option = notificationOptions.find(
(o: any) => o._id === notificationId
);
if (!option) return null;
return (
<Stack
width={"100%"}
justifyContent={"space-between"}
direction="row"
key={notificationId}
>
<Typography>{option.name}</Typography>
<DeleteOutlineRoundedIcon
onClick={() => {
const updated = notificationChannels.filter(
(id) => id !== notificationId
);
setValue("notificationChannels", updated);
}}
sx={{ cursor: "pointer" }}
/>
</Stack>
);
})}
</Stack>
</Stack>
}
/>
<ConfigBox
title={t("createMonitorPage.intervalTitle")}
subtitle="How often to check the URL"
rightContent={
<Controller
name="interval"
control={control}
render={({ field }) => (
<TextInput
{...field}
type="text"
label={t("createMonitorPage.intervalDescription")}
fullWidth
error={!!errors.interval}
helperText={errors.interval ? errors.interval.message : ""}
/>
)}
/>
}
/>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
loading={loading}
type="submit"
variant="contained"
color="accent"
>
{t("settingsSave")}
</Button>
</Stack>
</BasePage>
</form>
);
};
export default UptimeCreatePage;

View File

@@ -1,157 +0,0 @@
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/v2/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<ApiResponse>(
`/monitors/${id}?embedChecks=true&range=${range}`,
{},
{ refreshInterval: 30000, keepPreviousData: true }
);
const {
response: upResponse,
isValidating: upIsValidating,
error: upError,
} = useGet<ApiResponse>(
`/monitors/${id}?embedChecks=true&range=${range}&status=up`,
{},
{ keepPreviousData: true }
);
const {
response: downResponse,
error: downError,
isValidating: downIsValidating,
} = useGet<ApiResponse>(
`/monitors/${id}?embedChecks=true&range=${range}&status=down`,
{},
{ keepPreviousData: true }
);
const { patch, loading: isPatching, error: postError } = usePatch<ApiResponse>();
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 (
<BasePage>
<HeaderControls
monitor={monitor}
patch={patch}
isPatching={isPatching}
refetch={refetch}
/>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<StatBox
palette={palette}
title="Active for"
subtitle={prettyMilliseconds(streakDuration, { secondsDecimalDigits: 0 })}
/>
<StatBox
title="Last check"
subtitle={
lastChecked >= 0
? `${prettyMilliseconds(lastChecked, { secondsDecimalDigits: 0 })} ago`
: "N/A"
}
/>
<StatBox
title="Last response time"
subtitle={stats?.lastResponseTime ? `${stats?.lastResponseTime} ms` : "N/A"}
/>
</Stack>
<HeaderRange
loading={isValidating || upIsValidating || downIsValidating}
range={range}
setRange={setRange}
/>
<Stack
direction={isSmall ? "column" : "row"}
gap={theme.spacing(8)}
>
<HistogramStatus
title="Uptime"
status={"up"}
checks={upChecks}
range={range}
/>
<HistogramStatus
title="Incidents"
checks={downChecks}
status={"down"}
range={range}
/>
<ChartAvgResponse
avg={avgResponseTime}
max={maxResponseTime}
/>
</Stack>
<ChartResponseTime
checks={checks}
range={range}
/>
<CheckTable monitorId={monitor?._id} />
</BasePage>
);
};
export default UptimeDetailsPage;

View File

@@ -1,158 +0,0 @@
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<ApiResponse>();
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: <Typography color={theme.palette.error.main}>Remove</Typography>,
action: () => {
console.log("Open delete");
},
closeMenu: true,
},
];
};
const getHeaders = () => {
const headers: Header<IMonitor>[] = [
{
id: "name",
content: t("host"),
render: (row) => {
return row.name;
},
},
{
id: "status",
content: t("status"),
render: (row) => {
return (
<StatusLabel
status={row.status}
isActive={row.isActive}
/>
);
},
},
{
id: "histogram",
content: t("responseTime"),
render: (row) => {
return (
<Stack alignItems={"center"}>
<HistogramResponseTime checks={row.latestChecks} />
</Stack>
);
},
},
{
id: "type",
content: t("type"),
render: (row) => {
return row.type;
},
},
{
id: "actions",
content: t("actions"),
render: (row) => {
return <ActionsMenu items={getActions(row)} />;
},
},
];
return headers;
};
let headers = getHeaders();
if (isSmall) {
headers = headers.filter((h) => h.id !== "histogram");
}
return (
<Table
headers={headers}
data={monitors}
onRowClick={(row) => {
navigate(row._id);
}}
/>
);
};

View File

@@ -1,247 +0,0 @@
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 { useTranslation } from "react-i18next";
import { monitorSchema } from "@/Validation/v2/zod";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm, Controller, useWatch, type SubmitHandler } from "react-hook-form";
import { useTheme } from "@mui/material/styles";
import { useInitForm } from "@/Hooks/v2/useInitMonitorForm";
type FormValues = z.infer<typeof monitorSchema>;
export const UptimeForm = ({
initialData,
onSubmit,
notificationOptions,
loading,
}: {
initialData?: Partial<FormValues>;
onSubmit: SubmitHandler<FormValues>;
notificationOptions: any[];
loading: boolean;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const { defaults } = useInitForm({ initialData: initialData });
const {
handleSubmit,
control,
setValue,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(monitorSchema) as any,
defaultValues: defaults,
mode: "onChange",
});
const selectedType = useWatch({
control,
name: "type",
});
const notificationChannels = useWatch({
control,
name: "notificationChannels",
});
return (
<BasePage
component={"form"}
onSubmit={handleSubmit(onSubmit)}
>
<ConfigBox
title={t("distributedUptimeCreateChecks")}
subtitle={t("distributedUptimeCreateChecksDescription")}
rightContent={
<Controller
name="type"
control={control}
render={({ field }) => (
<FormControl error={!!errors.type}>
<RadioGroup
{...field}
sx={{ gap: theme.spacing(6) }}
>
<RadioWithDescription
value="http"
label={"HTTP"}
description={"Use HTTP to monitor your website or API endpoint."}
/>
<RadioWithDescription
value="https"
label="HTTPS"
description="Use HTTPS to monitor your website or API endpoint.
"
/>
<RadioWithDescription
value="ping"
label={t("pingMonitoring")}
description={t("pingMonitoringDescription")}
/>
</RadioGroup>
</FormControl>
)}
/>
}
/>
<ConfigBox
title={t("settingsGeneralSettings")}
subtitle={t(`uptimeGeneralInstructions.${selectedType}`)}
rightContent={
<Stack gap={theme.spacing(8)}>
<Controller
name="url"
control={control}
render={({ field }) => (
<TextInput
{...field}
type="text"
label={t("url")}
fullWidth
error={!!errors.url}
helperText={errors.url ? errors.url.message : ""}
/>
)}
/>
<Controller
name="name"
control={control}
render={({ field }) => (
<TextInput
{...field}
type="text"
label={t("displayName")}
fullWidth
error={!!errors.name}
helperText={errors.name ? errors.name.message : ""}
/>
)}
/>
</Stack>
}
/>
<ConfigBox
title={t("createMonitorPage.incidentConfigTitle")}
subtitle={t("createMonitorPage.incidentConfigDescriptionV2")}
rightContent={
<Controller
name="n"
control={control}
render={({ field }) => (
<TextInput
{...field}
type="number"
label={t("createMonitorPage.incidentConfigStatusCheckNumber")}
fullWidth
error={!!errors.n}
helperText={errors.n ? errors.n.message : ""}
onChange={(e) => {
const target = e.target as HTMLInputElement;
field.onChange(target.valueAsNumber);
}}
/>
)}
/>
}
/>
<ConfigBox
title={t("notificationConfig.title")}
subtitle={t("notificationConfig.description")}
rightContent={
<Stack>
<Controller
name="notificationChannels"
control={control}
defaultValue={[]} // important!
render={({ field }) => (
<AutoCompleteInput
multiple
options={notificationOptions}
getOptionLabel={(option) => option.name}
value={notificationOptions.filter((o: any) =>
(field.value || []).includes(o._id)
)}
onChange={(_, newValue) => {
field.onChange(newValue.map((o: any) => o._id));
}}
/>
)}
/>
<Stack
gap={theme.spacing(2)}
mt={theme.spacing(2)}
>
{notificationChannels.map((notificationId) => {
const option = notificationOptions.find(
(o: any) => o._id === notificationId
);
if (!option) return null;
return (
<Stack
width={"100%"}
justifyContent={"space-between"}
direction="row"
key={notificationId}
>
<Typography>{option.name}</Typography>
<DeleteOutlineRoundedIcon
onClick={() => {
const updated = notificationChannels.filter(
(id) => id !== notificationId
);
setValue("notificationChannels", updated);
}}
sx={{ cursor: "pointer" }}
/>
</Stack>
);
})}
</Stack>
</Stack>
}
/>
<ConfigBox
title={t("createMonitorPage.intervalTitle")}
subtitle="How often to check the URL"
rightContent={
<Controller
name="interval"
control={control}
render={({ field }) => (
<TextInput
{...field}
type="text"
label={t("createMonitorPage.intervalDescription")}
fullWidth
error={!!errors.interval}
helperText={errors.interval ? errors.interval.message : ""}
/>
)}
/>
}
/>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
loading={loading}
type="submit"
variant="contained"
color="accent"
>
{t("settingsSave")}
</Button>
</Stack>
</BasePage>
);
};

View File

@@ -1,74 +0,0 @@
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<ApiResponse>(
"/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 (
<BasePageWithStates
loading={isValidating}
error={error}
items={monitors}
page="uptime"
actionLink="create"
>
<HeaderCreate
isLoading={isValidating}
path="/v2/uptime/create"
/>
<Stack
direction={isSmall ? "column" : "row"}
gap={theme.spacing(8)}
>
<UpStatusBox n={monitorStatuses.up} />
<DownStatusBox n={monitorStatuses.down} />
<PausedStatusBox n={monitorStatuses.paused} />
</Stack>
<MonitorTable
monitors={monitors}
refetch={refetch}
/>
</BasePageWithStates>
);
};
export default UptimeMonitors;

View File

@@ -1,5 +1,4 @@
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/v1/Layouts/HomeLayout";
import NotFound from "../Pages/v1/NotFound";
@@ -54,18 +53,11 @@ 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 (
<LibRoutes>
<Route
path="/v2/*"
element={<V2Routes mode={mode} />}
/>
<Route
path="/"
element={

View File

@@ -1,52 +0,0 @@
import { Routes, Route } from "react-router";
import { ThemeProvider } from "@emotion/react";
import { lightTheme, darkTheme } from "@/Utils/Theme/v2/theme";
import AuthLoginV2 from "@/Pages/v2/Auth/Login";
import AuthRegisterV2 from "@/Pages/v2/Auth/Register";
import UptimeMonitorsPage from "@/Pages/v2/Uptime/UptimeMonitors";
import UptimeCreatePage from "@/Pages/v2/Uptime/Create";
import UptimeDetailsPage from "@/Pages/v2/Uptime/Details";
import RootLayout from "@/Components/v2/Layouts/RootLayout";
const V2Routes = ({ mode = "light" }) => {
const v2Theme = mode === "light" ? lightTheme : darkTheme;
return (
<ThemeProvider theme={v2Theme}>
<Routes>
<Route
path="login"
element={<AuthLoginV2 />}
/>
<Route
path="register"
element={<AuthRegisterV2 />}
/>
<Route
path="/"
element={<RootLayout />}
>
<Route
index
element={<UptimeMonitorsPage />}
/>
<Route
path="uptime"
element={<UptimeMonitorsPage />}
/>
<Route
path="uptime/:id"
element={<UptimeDetailsPage />}
/>
<Route
path="uptime/create"
element={<UptimeCreatePage />}
/>
</Route>
</Routes>
</ThemeProvider>
);
};
export default V2Routes;

View File

@@ -1,124 +0,0 @@
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,
},
};

View File

@@ -1,103 +0,0 @@
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));

View File

@@ -1,26 +0,0 @@
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 = <T>(
url: string,
config: AxiosRequestConfig = {}
): Promise<AxiosResponse<T>> => api.get<T>(url, config);
export const post = <T>(
url: string,
data: any,
config: AxiosRequestConfig = {}
): Promise<AxiosResponse<T>> => api.post<T>(url, data, config);
export const patch = <T>(
url: string,
data: any,
config: AxiosRequestConfig = {}
): Promise<AxiosResponse<T>> => api.patch<T>(url, data, config);
export default api;

View File

@@ -1,37 +0,0 @@
const MIN_OUT = 10;
const MAX_OUT = 100;
export const normalizeResponseTimes = <T extends Record<K, number>, 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,
}));
};

View File

@@ -1,38 +0,0 @@
import type { MonitorStatus } from "@/Types/Monitor";
import type { PaletteKey } from "@/Utils/Theme/v2/theme";
export const getStatusPalette = (status: MonitorStatus): PaletteKey => {
const paletteMap: Record<MonitorStatus, PaletteKey> = {
up: "success",
down: "error",
initializing: "warning",
};
return paletteMap[status];
};
export const getStatusColor = (status: MonitorStatus, theme: any): string => {
const statusColors: Record<MonitorStatus, string> = {
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;
};

View File

@@ -1,53 +0,0 @@
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<string, string> = {
"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<string, string> = {
"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;
};

View File

@@ -1,36 +0,0 @@
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,
});

View File

@@ -1,7 +1,6 @@
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";
@@ -20,13 +19,12 @@ const authTransform = createTransform(
const persistConfig = {
key: "root",
storage,
whitelist: ["auth", "v2Auth", "ui"],
whitelist: ["auth", "ui"],
transforms: [authTransform],
};
const rootReducer = combineReducers({
auth: authReducer,
v2Auth: v2AuthReducer,
ui: uiReducer,
});

View File

@@ -16,13 +16,6 @@ import NotificationController from "../controllers/v1/notificationController.js"
import DiagnosticController from "../controllers/v1/diagnosticController.js";
import IncidentController from "../controllers/v1/incidentController.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);
@@ -75,13 +68,5 @@ export const initializeControllers = (services) => {
incidentService: services.incidentService,
});
//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;
};

View File

@@ -15,14 +15,6 @@ import NotificationRoutes from "../routes/v1/notificationRoute.js";
import IncidentRoutes from "../routes/v1/incidentRoute.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);
@@ -50,19 +42,4 @@ export const setupRoutes = (app, controllers) => {
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter());
app.use("/api/v1/incidents", verifyJWT, incidentRoutes.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());
};

View File

@@ -71,28 +71,6 @@ import RecoveryModule from "../db/v1/modules/recoveryModule.js";
import SettingsModule from "../db/v1/modules/settingsModule.js";
import IncidentModule from "../db/v1/modules/incidentModule.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 });
ServiceRegistry.instance = serviceRegistry;
@@ -244,33 +222,6 @@ 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,
@@ -292,25 +243,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService
incidentService,
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) => {

View File

@@ -1,150 +0,0 @@
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;

View File

@@ -1,62 +0,0 @@
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;

View File

@@ -1,96 +0,0 @@
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;

View File

@@ -1,191 +0,0 @@
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;

View File

@@ -1,96 +0,0 @@
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;

View File

@@ -1,29 +0,0 @@
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;

View File

@@ -1,25 +0,0 @@
import mongoose from "mongoose";
const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/checkmate";
export const connectDatabase = async (): Promise<boolean> => {
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<boolean> => {
try {
await mongoose.disconnect();
console.log("Disconnected from MongoDB");
return true;
} catch (error) {
console.error("MongoDB disconnection error:", error);
return false;
}
};

View File

@@ -1,50 +0,0 @@
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<IRole>(
{
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<IRole>("Role_v2", roleSchema);

View File

@@ -1,102 +0,0 @@
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<IUser>(
{
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<IUser>("User_v2", userSchema);

View File

@@ -1,297 +0,0 @@
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<string, ILighthouseAudit>;
}
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<ICheck>(
{
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<ICheck>("Check_v2", CheckSchema);

View File

@@ -1,33 +0,0 @@
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";

View File

@@ -1,43 +0,0 @@
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<IInvite>(
{
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<IInvite>("Invite_v2", InviteSchema);

View File

@@ -1,39 +0,0 @@
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<IMaintenance>(
{
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<IMaintenance>("Maintenance_v2", MaintenanceSchema);

View File

@@ -1,96 +0,0 @@
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<IMonitor>(
{
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<IMonitor>("Monitor_v2", MonitorSchema);

View File

@@ -1,77 +0,0 @@
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<IMonitorStats>(
{
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<IMonitorStats>("MonitorStats_v2", MonitorStatsSchema);

View File

@@ -1,50 +0,0 @@
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<INotificationChannelConfig>(
{
url: { type: String, required: false },
emailAddress: { type: String, required: false },
},
{ _id: false, strict: "throw" }
);
const NotificationChannelSchema = new Schema<INotificationChannel>(
{
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<INotificationChannel>("NotificationChannel_v2", NotificationChannelSchema);

View File

@@ -1,77 +0,0 @@
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<string, { roles: IRole[]; timestamp: number }>();
// 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 };

View File

@@ -1,21 +0,0 @@
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 };

View File

@@ -1,33 +0,0 @@
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;

View File

@@ -1,30 +0,0 @@
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;

View File

@@ -1,34 +0,0 @@
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;

View File

@@ -1,36 +0,0 @@
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;

View File

@@ -1,34 +0,0 @@
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;

View File

@@ -1,24 +0,0 @@
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;

View File

@@ -1,210 +0,0 @@
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<ITokenizedUser>;
registerWithInvite(signupData: RegisterData): Promise<ITokenizedUser>;
login(loginData: LoginData): Promise<ITokenizedUser>;
cleanup(): Promise<void>;
cleanMonitors(): Promise<void>;
}
class AuthService implements IAuthService {
static SERVICE_NAME = SERVICE_NAME;
private jobQueue: IJobQueue;
constructor(jobQueue: IJobQueue) {
this.jobQueue = jobQueue;
}
async register(signupData: RegisterData): Promise<ITokenizedUser> {
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<ITokenizedUser> {
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<ITokenizedUser> {
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;

View File

@@ -1,147 +0,0 @@
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<ICheck>;
cleanupOrphanedChecks: () => Promise<boolean>;
}
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<ISystemInfo>;
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<string, any>;
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<ICapturePayload>) => {
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<ILighthousePayload>) => {
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<ICheck> => {
switch (type) {
case "infrastructure":
return this.buildInfrastructureCheck(statusResponse as StatusResponse<ICapturePayload>);
case "pagespeed":
return this.buildPagespeedCheck(statusResponse as StatusResponse<ILighthousePayload>);
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;

View File

@@ -1,63 +0,0 @@
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<IInvite[]>;
get: (tokenHash: string) => Promise<IInvite>;
delete: (id: string) => Promise<boolean>;
}
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;

View File

@@ -1,144 +0,0 @@
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<IMaintenance>;
getAll: () => Promise<IMaintenance[]>;
get: (id: string) => Promise<IMaintenance>;
toggleActive: (tokenizedUser: ITokenizedUser, id: string) => Promise<IMaintenance>;
update: (tokenizedUser: ITokenizedUser, id: string, updateData: Partial<IMaintenance>) => Promise<IMaintenance>;
delete: (id: string) => Promise<boolean>;
isInMaintenance: (monitorId: string) => Promise<boolean>;
}
type MaintenanceCache = Map<string, IMaintenance[]>;
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<IMaintenance>) => {
const allowedFields: (keyof IMaintenance)[] = ["name", "monitors", "startTime", "endTime", "isActive"];
const safeUpdate: Partial<IMaintenance> = {};
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;

View File

@@ -1,468 +0,0 @@
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<IMonitor>;
getAll: () => Promise<IMonitor[]>;
getAllEmbedChecks: (page: number, limit: number, type: MonitorType[]) => Promise<any[]>;
get: (monitorId: string) => Promise<IMonitor>;
getEmbedChecks: (monitorId: string, range: string, status?: string) => Promise<MonitorWithChecksResponse>;
toggleActive: (monitorId: string, tokenizedUser: ITokenizedUser) => Promise<IMonitor>;
update: (tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial<IMonitor>) => Promise<IMonitor>;
delete: (monitorId: string) => Promise<boolean>;
}
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<string, any> => {
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<string, any> => {
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<string, any> => {
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<MonitorWithChecksResponse> => {
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,
});
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<IMonitor>) {
const allowedFields: (keyof IMonitor)[] = ["name", "interval", "isActive", "n", "notificationChannels"];
const safeUpdate: Partial<IMonitor> = {};
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;

Some files were not shown because too many files have changed in this diff Show More