diff --git a/client/package-lock.json b/client/package-lock.json
index 7d6350f45..7cf0e8631 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -26,6 +26,7 @@
"i18next": "25.4.2",
"joi": "17.13.3",
"mui-color-input": "^6.0.0",
+ "pretty-ms": "9.3.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
"react-hook-form": "7.63.0",
@@ -5184,6 +5185,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parse-ms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
+ "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -5311,6 +5324,21 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
+ "node_modules/pretty-ms": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
+ "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "parse-ms": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
diff --git a/client/package.json b/client/package.json
index 6d4858027..376671942 100644
--- a/client/package.json
+++ b/client/package.json
@@ -31,6 +31,7 @@
"i18next": "25.4.2",
"joi": "17.13.3",
"mui-color-input": "^6.0.0",
+ "pretty-ms": "9.3.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
"react-hook-form": "7.63.0",
@@ -46,14 +47,6 @@
"vite-plugin-svgr": "^4.2.0",
"zod": "4.1.11"
},
- "unusedDepencies": {
- "@solana/wallet-adapter-base": "0.9.25",
- "@solana/wallet-adapter-material-ui": "0.16.35",
- "@solana/wallet-adapter-react": "0.15.37",
- "@solana/wallet-adapter-react-ui": "0.9.37",
- "@solana/wallet-adapter-wallets": "0.19.34",
- "@solana/web3.js": "1.98.0"
- },
"devDependencies": {
"@types/node": "24.5.2",
"@types/react": "^18.2.66",
diff --git a/client/src/App.jsx b/client/src/App.jsx
index 04f0d5b65..d21850ebb 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -9,7 +9,6 @@ import { CssBaseline, GlobalStyles } from "@mui/material";
import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Routes } from "./Routes";
-import WalletProvider from "./Components/WalletProvider";
import AppLayout from "@/Components/v1/Layouts/AppLayout";
function App() {
@@ -24,16 +23,12 @@ function App() {
}, []);
return (
- /* Extract Themeprovider, baseline and global styles to Styles */
-
-
-
-
-
-
-
-
+
+
+
+
+
);
}
diff --git a/client/src/Components/v2/DesignElements/BaseBox.tsx b/client/src/Components/v2/DesignElements/BaseBox.tsx
new file mode 100644
index 000000000..14c4af1c4
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/BaseBox.tsx
@@ -0,0 +1,23 @@
+import Box from "@mui/material/Box";
+import { useTheme } from "@mui/material/styles";
+import type { SxProps } from "@mui/material/styles";
+
+type BaseBoxProps = React.PropsWithChildren<{ sx?: SxProps }>;
+
+export const BaseBox: React.FC = ({ children, sx }) => {
+ const theme = useTheme();
+ return (
+
+ {children}
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/Dot.tsx b/client/src/Components/v2/DesignElements/Dot.tsx
new file mode 100644
index 000000000..ca3884cfb
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/Dot.tsx
@@ -0,0 +1,23 @@
+export const Dot = ({
+ color = "gray",
+ size = "4px",
+ style,
+}: {
+ color?: string;
+ size?: string;
+ style?: React.CSSProperties;
+}) => {
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/PulseDot.tsx b/client/src/Components/v2/DesignElements/PulseDot.tsx
new file mode 100644
index 000000000..a785b5a0d
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/PulseDot.tsx
@@ -0,0 +1,44 @@
+import Stack from "@mui/material/Stack";
+import Box from "@mui/material/Box";
+import { useTheme } from "@mui/material/styles";
+export const PulseDot = ({ color }: { color: string }) => {
+ const theme = useTheme();
+ return (
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/StatBox.tsx b/client/src/Components/v2/DesignElements/StatBox.tsx
new file mode 100644
index 000000000..3450444b2
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/StatBox.tsx
@@ -0,0 +1,57 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { useTheme } from "@mui/material/styles";
+import { useMediaQuery } from "@mui/material";
+import type { PaletteKey } from "@/Utils/Theme/v2/theme";
+import { BaseBox } from "@/Components/v2/DesignElements";
+
+type GradientBox = React.PropsWithChildren<{ palette?: PaletteKey }>;
+
+export const GradientBox: React.FC = ({ children, palette }) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ const bg = palette
+ ? `linear-gradient(to bottom right, ${theme.palette[palette].main} 30%, ${theme.palette[palette].lowContrast} 70%)`
+ : `linear-gradient(340deg, ${theme.palette.tertiary.main} 10%, ${theme.palette.primary.main} 45%)`;
+
+ return (
+
+ {children}
+
+ );
+};
+
+type StatBoxProps = React.PropsWithChildren<{
+ title: string;
+ subtitle: string;
+ palette?: PaletteKey;
+}>;
+
+export const StatBox: React.FC = ({
+ title,
+ subtitle,
+ palette,
+ children,
+}) => {
+ const theme = useTheme();
+ const textColor = palette ? theme.palette[palette].contrastText : "inherit";
+
+ return (
+
+
+ {title}
+ {subtitle}
+ {children}
+
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/StatusBox.tsx b/client/src/Components/v2/DesignElements/StatusBox.tsx
index 540255dfd..8cd5cfbc1 100644
--- a/client/src/Components/v2/DesignElements/StatusBox.tsx
+++ b/client/src/Components/v2/DesignElements/StatusBox.tsx
@@ -1,6 +1,7 @@
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";
@@ -11,15 +12,13 @@ type StatusBoxProps = React.PropsWithChildren<{}>;
export const BGBox: React.FC = ({ children }) => {
const theme = useTheme();
return (
-
= ({ children }) => {
{children}
-
+
);
};
diff --git a/client/src/Components/v2/DesignElements/StatusLabel.tsx b/client/src/Components/v2/DesignElements/StatusLabel.tsx
new file mode 100644
index 000000000..69b788e96
--- /dev/null
+++ b/client/src/Components/v2/DesignElements/StatusLabel.tsx
@@ -0,0 +1,35 @@
+import Box from "@mui/material/Box";
+import { BaseBox } from "@/Components/v2/DesignElements";
+import type { MonitorStatus } from "@/Types/Monitor";
+
+import { getStatusPalette } from "@/Utils/MonitorUtils";
+import { useTheme } from "@mui/material/styles";
+
+export const StatusLabel = ({ status }: { status: MonitorStatus }) => {
+ const theme = useTheme();
+ const palette = getStatusPalette(status);
+ const transformedText = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
+
+ return (
+
+
+ {transformedText}
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/Table.tsx b/client/src/Components/v2/DesignElements/Table.tsx
index 6296d260c..58cc6aed1 100644
--- a/client/src/Components/v2/DesignElements/Table.tsx
+++ b/client/src/Components/v2/DesignElements/Table.tsx
@@ -5,7 +5,19 @@ import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
+
+import IconButton from "@mui/material/IconButton";
+import LastPageIcon from "@mui/icons-material/LastPage";
+import FirstPageIcon from "@mui/icons-material/FirstPage";
+import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft";
+import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
+
+import Box from "@mui/material/Box";
+import TablePagination from "@mui/material/TablePagination";
+import type { TablePaginationProps } from "@mui/material/TablePagination";
+
import { useTheme } from "@mui/material/styles";
+import { useMediaQuery } from "@mui/material";
export type Header = {
id: number | string;
content: React.ReactNode;
@@ -87,3 +99,109 @@ export function DataTable
);
}
+
+interface TablePaginationActionsProps {
+ count: number;
+ page: number;
+ rowsPerPage: number;
+ onPageChange: (event: React.MouseEvent, newPage: number) => void;
+}
+
+function TablePaginationActions(props: TablePaginationActionsProps) {
+ const theme = useTheme();
+ const { count, page, rowsPerPage, onPageChange } = props;
+
+ const handleFirstPageButtonClick = (event: React.MouseEvent) => {
+ onPageChange(event, 0);
+ };
+
+ const handleBackButtonClick = (event: React.MouseEvent) => {
+ onPageChange(event, page - 1);
+ };
+
+ const handleNextButtonClick = (event: React.MouseEvent) => {
+ onPageChange(event, page + 1);
+ };
+
+ const handleLastPageButtonClick = (event: React.MouseEvent) => {
+ onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
+ };
+
+ return (
+
+
+ {theme.direction === "rtl" ? : }
+
+
+ {theme.direction === "rtl" ? : }
+
+ = Math.ceil(count / rowsPerPage) - 1}
+ aria-label="next page"
+ >
+ {theme.direction === "rtl" ? : }
+
+ = Math.ceil(count / rowsPerPage) - 1}
+ aria-label="last page"
+ >
+ {theme.direction === "rtl" ? : }
+
+
+ );
+}
+
+export const Pagination: React.FC = ({ ...props }) => {
+ const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm"));
+ const theme = useTheme();
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/DesignElements/index.tsx b/client/src/Components/v2/DesignElements/index.tsx
index 41b4b319a..6ba0a19ac 100644
--- a/client/src/Components/v2/DesignElements/index.tsx
+++ b/client/src/Components/v2/DesignElements/index.tsx
@@ -1,4 +1,7 @@
export { SplitBox as HorizontalSplitBox, ConfigBox } from "./SplitBox";
export { BasePage } from "./BasePage";
export { BGBox, UpStatusBox, DownStatusBox, PausedStatusBox } from "./StatusBox";
-export { DataTable as Table } from "./Table";
+export { DataTable as Table, Pagination } from "./Table";
+export { GradientBox, StatBox } from "./StatBox";
+export { BaseBox } from "./BaseBox";
+export { StatusLabel } from "./StatusLabel";
diff --git a/client/src/Components/v2/Inputs/Button.tsx b/client/src/Components/v2/Inputs/Button.tsx
index 0bf1d14e1..2f28e8eda 100644
--- a/client/src/Components/v2/Inputs/Button.tsx
+++ b/client/src/Components/v2/Inputs/Button.tsx
@@ -1,10 +1,11 @@
import Button from "@mui/material/Button";
import type { ButtonProps } from "@mui/material/Button";
-export const ButtonInput: React.FC = ({ ...props }) => {
+
+export const ButtonInput: React.FC = ({ sx, ...props }) => {
return (
);
};
diff --git a/client/src/Components/v2/Inputs/ButtonGroup.tsx b/client/src/Components/v2/Inputs/ButtonGroup.tsx
new file mode 100644
index 000000000..3d8acd8cc
--- /dev/null
+++ b/client/src/Components/v2/Inputs/ButtonGroup.tsx
@@ -0,0 +1,13 @@
+import ButtonGroup from "@mui/material/ButtonGroup";
+import type { ButtonGroupProps } from "@mui/material/ButtonGroup";
+export const ButtonGroupInput: React.FC = ({
+ orientation,
+ ...props
+}) => {
+ return (
+
+ );
+};
diff --git a/client/src/Components/v2/Inputs/index.tsx b/client/src/Components/v2/Inputs/index.tsx
index 9b582f3d7..5f3eb2810 100644
--- a/client/src/Components/v2/Inputs/index.tsx
+++ b/client/src/Components/v2/Inputs/index.tsx
@@ -1 +1,2 @@
export { ButtonInput as Button } from "./Button";
+export { ButtonGroupInput as ButtonGroup } from "./ButtonGroup";
diff --git a/client/src/Components/v2/Monitors/ChartAvgResponse.tsx b/client/src/Components/v2/Monitors/ChartAvgResponse.tsx
new file mode 100644
index 000000000..10938f359
--- /dev/null
+++ b/client/src/Components/v2/Monitors/ChartAvgResponse.tsx
@@ -0,0 +1,84 @@
+import { BaseChart } from "./HistogramStatus";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import AverageResponseIcon from "@/assets/icons/average-response-icon.svg?react";
+import { Cell, RadialBarChart, RadialBar, ResponsiveContainer } from "recharts";
+
+import { getResponseTimeColor } from "@/Utils/MonitorUtils";
+import { useTheme } from "@mui/material/styles";
+
+export const ChartAvgResponse = ({ avg, max }: { avg: number; max: number }) => {
+ const theme = useTheme();
+ const chartData = [
+ { name: "max", value: max - avg, color: "transparent" },
+ { name: "avg", value: avg, color: "red" },
+ ];
+
+ const palette = getResponseTimeColor(avg);
+ const msg: Record = {
+ success: "Excellent",
+ warning: "Average",
+ danger: "Poor",
+ };
+
+ return (
+ }
+ title={"Average response time"}
+ >
+
+
+
+
+ |
+ |
+
+
+
+
+ Low
+ High
+
+
+
+ {msg[palette]}
+
+ {`${avg?.toFixed()}ms`}
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/ChartResponseTime.tsx b/client/src/Components/v2/Monitors/ChartResponseTime.tsx
new file mode 100644
index 000000000..97aef314d
--- /dev/null
+++ b/client/src/Components/v2/Monitors/ChartResponseTime.tsx
@@ -0,0 +1,159 @@
+import { BaseChart } from "./HistogramStatus";
+import { BaseBox } from "../DesignElements";
+import ResponseTimeIcon from "@/assets/icons/response-time-icon.svg?react";
+import {
+ AreaChart,
+ Area,
+ XAxis,
+ Tooltip,
+ CartesianGrid,
+ ResponsiveContainer,
+ Text,
+} from "recharts";
+import Typography from "@mui/material/Typography";
+
+import {
+ formatDateWithTz,
+ tickDateFormatLookup,
+ tooltipDateFormatLookup,
+} from "@/Utils/TimeUtils";
+import { useTheme } from "@mui/material/styles";
+import type { GroupedCheck } from "@/Types/Check";
+import { useSelector } from "react-redux";
+
+type XTickProps = {
+ x: number;
+ y: number;
+ payload: { value: any };
+ range: string;
+};
+
+const XTick: React.FC = ({ x, y, payload, range }) => {
+ const format = tickDateFormatLookup(range);
+ const theme = useTheme();
+ const uiTimezone = useSelector((state: any) => state.ui.timezone);
+ return (
+
+ {formatDateWithTz(payload?.value, format, uiTimezone)}
+
+ );
+};
+
+type ResponseTimeToolTipProps = {
+ active?: boolean | undefined;
+ payload?: any[];
+ label?: string;
+ range: string;
+ theme: any;
+ uiTimezone: string;
+};
+
+const ResponseTimeToolTip: React.FC = ({
+ active,
+ payload,
+ label,
+ range,
+ theme,
+ uiTimezone,
+}) => {
+ if (!label) return null;
+ if (!payload) return null;
+ if (!active) return null;
+
+ const format = tooltipDateFormatLookup(range);
+ const responseTime = Math.floor(payload?.[0]?.value || 0);
+ return (
+
+ {formatDateWithTz(label, format, uiTimezone)}
+ Response time: {responseTime} ms
+
+ );
+};
+
+export const ChartResponseTime = ({
+ checks,
+ range,
+}: {
+ checks: GroupedCheck[];
+ range: string;
+}) => {
+ const theme = useTheme();
+ const uiTimezone = useSelector((state: any) => state.ui.timezone);
+
+ return (
+ }
+ title="Response times"
+ >
+
+
+
+
+
+
+
+
+
+ (
+
+ )}
+ />
+
+ (
+
+ )}
+ />
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/HeaderControls.tsx b/client/src/Components/v2/Monitors/HeaderControls.tsx
new file mode 100644
index 000000000..95a03b5cf
--- /dev/null
+++ b/client/src/Components/v2/Monitors/HeaderControls.tsx
@@ -0,0 +1,67 @@
+import Stack from "@mui/material/Stack";
+import { MonitorStatus } from "@/Components/v2/Monitors/MonitorStatus";
+import { ButtonGroup, Button } from "@/Components/v2/Inputs";
+import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
+import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
+import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
+import EmailIcon from "@mui/icons-material/Email";
+import BugReportOutlinedIcon from "@mui/icons-material/BugReportOutlined";
+
+import { useMediaQuery } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import { useTheme } from "@mui/material/styles";
+
+import type { IMonitor } from "@/Types/Monitor";
+
+export const HeaderControls = ({
+ monitor,
+ patch,
+ isPatching,
+ refetch,
+}: {
+ monitor: IMonitor;
+ patch: Function;
+ isPatching: boolean;
+ refetch: Function;
+}) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+
+ return (
+
+
+
+
+ }>{t("sendTestNotifications")}
+ }>{t("menu.incidents")}
+
+ }>{t("configure")}
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/HeaderRange.tsx b/client/src/Components/v2/Monitors/HeaderRange.tsx
new file mode 100644
index 000000000..21ce01fe2
--- /dev/null
+++ b/client/src/Components/v2/Monitors/HeaderRange.tsx
@@ -0,0 +1,62 @@
+import Stack from "@mui/material/Stack";
+import { ButtonGroup, Button } from "@/Components/v2/Inputs";
+import { useTheme } from "@mui/material/styles";
+import Typography from "@mui/material/Typography";
+import { useMediaQuery } from "@mui/material";
+export const HeaderRange = ({
+ range,
+ setRange,
+ loading,
+}: {
+ range: string;
+ setRange: Function;
+ loading: boolean;
+}) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ return (
+
+ {`Showing statistics for past ${range}`}
+
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/HistogramStatus.tsx b/client/src/Components/v2/Monitors/HistogramStatus.tsx
new file mode 100644
index 000000000..aeacf31d2
--- /dev/null
+++ b/client/src/Components/v2/Monitors/HistogramStatus.tsx
@@ -0,0 +1,216 @@
+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 { useState } from "react";
+import { formatDateWithTz } from "@/Utils/TimeUtils";
+import { useSelector } from "react-redux";
+import { useTheme } from "@mui/material/styles";
+import { getResponseTimeColor } from "@/Utils/MonitorUtils";
+
+const XLabel = ({
+ p1,
+ p2,
+ range,
+}: {
+ p1: GroupedCheck;
+ p2: GroupedCheck;
+ range: string;
+}) => {
+ const theme = useTheme();
+ const uiTimezone = useSelector((state: any) => state.ui.timezone);
+ const dateFormat = range === "day" ? "MMM D, h:mm A" : "MMM D";
+ return (
+ <>
+
+ {formatDateWithTz(p1._id, dateFormat, uiTimezone)}
+
+
+ {formatDateWithTz(p2._id, dateFormat, uiTimezone)}
+
+ >
+ );
+};
+
+type BaseChartProps = React.PropsWithChildren<{
+ icon: React.ReactNode;
+ title: string;
+}>;
+
+export const BaseChart: React.FC = ({ children, icon, title }) => {
+ const theme = useTheme();
+
+ return (
+
+
+
+
+ {icon}
+
+ {title}
+
+ {children}
+
+
+ );
+};
+
+export const HistogramStatus = ({
+ checks,
+ status,
+ range,
+ title,
+}: {
+ checks: GroupedCheck[];
+ status: MonitorStatus;
+ range: string;
+ title: string;
+}) => {
+ const uiTimezone = useSelector((state: any) => state.ui.timezone);
+
+ const icon = status === "up" ? : ;
+ const theme = useTheme();
+ const [idx, setIdx] = useState(null);
+ const dateFormat = range === "1d" || range === "2h" ? "MMM D, h A" : "MMM D";
+
+ if (checks.length === 0) {
+ return (
+
+
+
+ {status === "up" ? "No checks yet" : "Great, no downtime yet!"}
+
+
+
+ );
+ }
+
+ const totalChecks = checks.reduce((count, check) => {
+ return count + check.count;
+ }, 0);
+
+ return (
+
+
+
+
+ Total checks
+ {idx ? (
+
+ {checks[idx].count}
+
+ {formatDateWithTz(checks[idx]._id, dateFormat, uiTimezone)}
+
+
+ ) : (
+ {totalChecks}
+ )}
+
+
+
+
+
+ }
+ />
+
+ {checks?.map((groupedCheck, idx) => {
+ const fillColor = getResponseTimeColor(groupedCheck.avgResponseTime);
+ return (
+ setIdx(idx)}
+ onMouseLeave={() => setIdx(null)}
+ key={groupedCheck._id}
+ fill={theme.palette[fillColor].main}
+ />
+ );
+ })}
+ |
+
+
+
+
+ );
+};
diff --git a/client/src/Components/v2/Monitors/MonitorStatus.tsx b/client/src/Components/v2/Monitors/MonitorStatus.tsx
new file mode 100644
index 000000000..ff4d83352
--- /dev/null
+++ b/client/src/Components/v2/Monitors/MonitorStatus.tsx
@@ -0,0 +1,60 @@
+import type { IMonitor } from "@/Types/Monitor";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import { PulseDot } from "@/Components/v2/DesignElements/PulseDot";
+import { Dot } from "@/Components/v2/DesignElements/Dot";
+import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
+import { useTheme } from "@mui/material/styles";
+import prettyMilliseconds from "pretty-ms";
+import { typographyLevels } from "@/Utils/Theme/v2/palette";
+import { useMediaQuery } from "@mui/material";
+export const MonitorStatus = ({ monitor }: { monitor: IMonitor }) => {
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+
+ if (!monitor) {
+ return null;
+ }
+ return (
+
+
+ {monitor.name}
+
+
+
+
+ {formatUrl(monitor?.url)}
+
+ {!isSmall && (
+ <>
+
+
+ Checking every {prettyMilliseconds(monitor?.interval, { verbose: true })}
+
+ >
+ )}
+
+
+ );
+};
diff --git a/client/src/Hooks/v2/UseApi.tsx b/client/src/Hooks/v2/UseApi.tsx
index eab661793..9808dbf49 100644
--- a/client/src/Hooks/v2/UseApi.tsx
+++ b/client/src/Hooks/v2/UseApi.tsx
@@ -2,7 +2,7 @@ import { useState } from "react";
import useSWR from "swr";
import type { SWRConfiguration } from "swr";
import type { AxiosRequestConfig } from "axios";
-import { get, post } from "@/Utils/ApiClient"; // your axios wrapper
+import { get, post, patch } from "@/Utils/ApiClient"; // your axios wrapper
export type ApiResponse = {
message: string;
@@ -20,7 +20,7 @@ export const useGet = (
axiosConfig?: AxiosRequestConfig,
swrConfig?: SWRConfiguration
) => {
- const { data, error, isLoading, mutate } = useSWR(
+ const { data, error, isLoading, isValidating, mutate } = useSWR(
url,
(url) => fetcher(url, axiosConfig),
swrConfig
@@ -29,6 +29,7 @@ export const useGet = (
return {
response: data ?? null,
loading: isLoading,
+ isValidating,
error: error?.message ?? null,
refetch: mutate,
};
@@ -56,3 +57,26 @@ export const usePost = (endpoint: string) => {
return { post: postFn, loading, error };
};
+
+export const usePatch = (endpoint: string) => {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const patchFn = async (body: B, config?: AxiosRequestConfig): Promise => {
+ setLoading(true);
+ setError(null);
+
+ try {
+ const res = await patch(endpoint, body, config);
+ return res.data;
+ } catch (err: any) {
+ const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
+ setError(errMsg);
+ return null;
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return { patch: patchFn, loading, error };
+};
diff --git a/client/src/Pages/v2/Uptime/CheckTable.tsx b/client/src/Pages/v2/Uptime/CheckTable.tsx
new file mode 100644
index 000000000..42da6da12
--- /dev/null
+++ b/client/src/Pages/v2/Uptime/CheckTable.tsx
@@ -0,0 +1,93 @@
+import { Table, Pagination } from "@/Components/v2/DesignElements";
+import { StatusLabel } from "@/Components/v2/DesignElements";
+import Box from "@mui/material/Box";
+
+import type { Header } from "@/Components/v2/DesignElements/Table";
+import type { Check } from "@/Types/Check";
+import type { ApiResponse } from "@/Hooks/v2/UseApi";
+import type { MonitorStatus } from "@/Types/Monitor";
+
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useGet } from "@/Hooks/v2/UseApi";
+import { formatDateWithTz } from "@/Utils/TimeUtils";
+import { useSelector } from "react-redux";
+const getHeaders = (t: Function, uiTimezone: string) => {
+ const headers: Header[] = [
+ {
+ id: "status",
+ content: t("status"),
+ render: (row) => {
+ return ;
+ },
+ },
+ {
+ id: "date",
+ content: t("date&Time"),
+ render: (row) => {
+ return formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone);
+ },
+ },
+ {
+ id: "statusCode",
+ content: t("statusCode"),
+ render: (row) => {
+ return row.httpStatusCode || "N/A";
+ },
+ },
+ ];
+ return headers;
+};
+
+export const CheckTable = ({ monitorId }: { monitorId: string }) => {
+ const [page, setPage] = useState(0);
+ const [rowsPerPage, setRowsPerPage] = useState(5);
+ const { t } = useTranslation();
+ const uiTimezone = useSelector((state: any) => state.ui.timezone);
+ const headers = getHeaders(t, uiTimezone);
+
+ const { response, error } = useGet(
+ `/monitors/${monitorId}/checks?page=${page}&rowsPerPage=${rowsPerPage}`,
+ {},
+ { keepPreviousData: true }
+ );
+
+ const checks = response?.data?.checks || [];
+ const count = response?.data?.count || 0;
+
+ const handlePageChange = (
+ _e: React.MouseEvent | null,
+ newPage: number
+ ) => {
+ setPage(newPage);
+ };
+
+ const handleRowsPerPageChange = (
+ e: React.ChangeEvent
+ ) => {
+ const value = Number(e.target.value);
+ setPage(0);
+ setRowsPerPage(value);
+ };
+
+ if (error) {
+ console.error(error);
+ }
+
+ return (
+
+
+
+
+ );
+};
diff --git a/client/src/Pages/v2/Uptime/Create.tsx b/client/src/Pages/v2/Uptime/Create.tsx
index 3aa73b5fb..a135ec84c 100644
--- a/client/src/Pages/v2/Uptime/Create.tsx
+++ b/client/src/Pages/v2/Uptime/Create.tsx
@@ -5,7 +5,7 @@ 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 "@mui/material/Button";
+import { Button } from "@/Components/v2/Inputs";
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
import { Typography } from "@mui/material";
import humanInterval from "human-interval";
@@ -19,7 +19,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useGet, usePost } from "@/Hooks/v2/UseApi";
import type { ApiResponse } from "@/Hooks/v2/UseApi";
-const CreateUptimePage = () => {
+const UptimeCreatePage = () => {
const { t } = useTranslation();
const theme = useTheme();
@@ -259,4 +259,4 @@ const CreateUptimePage = () => {
);
};
-export default CreateUptimePage;
+export default UptimeCreatePage;
diff --git a/client/src/Pages/v2/Uptime/Details.tsx b/client/src/Pages/v2/Uptime/Details.tsx
new file mode 100644
index 000000000..c3ef14e72
--- /dev/null
+++ b/client/src/Pages/v2/Uptime/Details.tsx
@@ -0,0 +1,161 @@
+import { BasePage } from "@/Components/v2/DesignElements";
+import { HeaderControls } from "@/Components/v2/Monitors/HeaderControls";
+import Stack from "@mui/material/Stack";
+import { StatBox } from "@/Components/v2/DesignElements";
+import { HistogramStatus } from "@/Components/v2/Monitors/HistogramStatus";
+import { ChartAvgResponse } from "@/Components/v2/Monitors/ChartAvgResponse";
+import { ChartResponseTime } from "@/Components/v2/Monitors/ChartResponseTime";
+import { HeaderRange } from "@/Components/v2/Monitors/HeaderRange";
+import { CheckTable } from "@/Pages/v2/Uptime/CheckTable";
+
+import type { IMonitor } from "@/Types/Monitor";
+import { useMediaQuery } from "@mui/material";
+import { useTheme } from "@mui/material/styles";
+import { useParams } from "react-router";
+import { useGet, usePatch, type ApiResponse } from "@/Hooks/v2/UseApi";
+import { useState } from "react";
+import { getStatusPalette } from "@/Utils/MonitorUtils";
+import prettyMilliseconds from "pretty-ms";
+
+const UptimeDetailsPage = () => {
+ const { id } = useParams();
+ const theme = useTheme();
+ const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+
+ // Local state
+ const [range, setRange] = useState("2h");
+
+ const { response, isValidating, error, refetch } = useGet(
+ `/monitors/${id}?embedChecks=true&range=${range}`,
+
+ {},
+ { refreshInterval: 30000 }
+ );
+
+ const {
+ response: upResponse,
+ isValidating: upIsValidating,
+ error: upError,
+ } = useGet(
+ `/monitors/${id}?embedChecks=true&range=${range}&status=up`,
+ {},
+ {}
+ );
+
+ const {
+ response: downResponse,
+ error: downError,
+ isValidating: downIsValidating,
+ } = useGet(
+ `/monitors/${id}?embedChecks=true&range=${range}&status=down`,
+ {},
+ {}
+ );
+
+ const {
+ patch,
+ loading: isPatching,
+ error: postError,
+ } = usePatch(`/monitors/${id}/active`);
+
+ const monitor: IMonitor = response?.data?.monitor;
+
+ if (!monitor) {
+ return null;
+ }
+
+ const stats = response?.data?.stats || null;
+ const avgResponseTime = stats?.avgResponseTime || 0;
+ const maxResponseTime = stats?.maxResponseTime || 0;
+
+ const streakDuration = stats?.currentStreakStartedAt
+ ? Date.now() - stats?.currentStreakStartedAt
+ : 0;
+
+ const lastChecked = stats?.lastCheckTimestamp
+ ? Date.now() - stats?.lastCheckTimestamp
+ : -1;
+
+ const checks = response?.data?.checks || [];
+ const upChecks = upResponse?.data?.checks ? [...upResponse.data.checks].reverse() : [];
+ const downChecks = downResponse?.data?.checks
+ ? [...downResponse.data.checks].reverse()
+ : [];
+
+ const palette = getStatusPalette(monitor.status);
+
+ if (error || upError || downError || postError) {
+ console.error("Error fetching monitor data:", {
+ error,
+ upError,
+ downError,
+ postError,
+ });
+ }
+
+ return (
+
+
+
+
+ = 0
+ ? `${prettyMilliseconds(lastChecked, { secondsDecimalDigits: 0 })} ago`
+ : "N/A"
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default UptimeDetailsPage;
diff --git a/client/src/Pages/v2/Uptime/MonitorTable.tsx b/client/src/Pages/v2/Uptime/MonitorTable.tsx
index 93ae498f9..038da2e24 100644
--- a/client/src/Pages/v2/Uptime/MonitorTable.tsx
+++ b/client/src/Pages/v2/Uptime/MonitorTable.tsx
@@ -1,16 +1,23 @@
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 type { IMonitor } from "@/Types/Monitor";
-import { Table } from "@/Components/v2/DesignElements";
+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 { ActionsMenu } from "@/Components/v2/ActionsMenu";
-import type { ActionMenuItem } from "@/Components/v2/ActionsMenu";
-import Typography from "@mui/material/Typography";
+import { useNavigate } from "react-router-dom";
-const getActions = (theme: any): ActionMenuItem[] => {
+import type { IMonitor } from "@/Types/Monitor";
+import type { ActionMenuItem } from "@/Components/v2/ActionsMenu";
+const getActions = (
+ theme: any,
+ monitor: IMonitor,
+ navigate: Function
+): ActionMenuItem[] => {
return [
{
id: 1,
@@ -24,7 +31,7 @@ const getActions = (theme: any): ActionMenuItem[] => {
id: 2,
label: "Details",
action: () => {
- console.log("Open details");
+ navigate(`${monitor._id}`);
},
},
{
@@ -67,7 +74,7 @@ const getActions = (theme: any): ActionMenuItem[] => {
];
};
-const getHeaders = (theme: any, t: Function) => {
+const getHeaders = (theme: any, t: Function, navigate: Function) => {
const headers: Header[] = [
{
id: "name",
@@ -80,7 +87,7 @@ const getHeaders = (theme: any, t: Function) => {
id: "status",
content: t("status"),
render: (row) => {
- return row.status;
+ return ;
},
},
{
@@ -104,8 +111,8 @@ const getHeaders = (theme: any, t: Function) => {
{
id: "actions",
content: t("actions"),
- render: () => {
- return ;
+ render: (row) => {
+ return ;
},
},
];
@@ -116,8 +123,9 @@ export const MonitorTable = ({ monitors }: { monitors: IMonitor[] }) => {
const { t } = useTranslation();
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
+ const navigate = useNavigate();
- let headers = getHeaders(theme, t);
+ let headers = getHeaders(theme, t, navigate);
if (isSmall) {
headers = headers.filter((h) => h.id !== "histogram");
diff --git a/client/src/Pages/v2/Uptime/UptimeMonitors.tsx b/client/src/Pages/v2/Uptime/UptimeMonitors.tsx
index e69da7ebb..8980c8260 100644
--- a/client/src/Pages/v2/Uptime/UptimeMonitors.tsx
+++ b/client/src/Pages/v2/Uptime/UptimeMonitors.tsx
@@ -18,7 +18,7 @@ const UptimeMonitors = () => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
- const { response, loading } = useGet("/monitors?embedChecks=true");
+ const { response, loading } = useGet("/monitors?embedChecks=true", {});
const monitors = response?.data ?? ([] as IMonitor[]);
if (monitors.length === 0 && !loading) {
diff --git a/client/src/Routes/v2router.tsx b/client/src/Routes/v2router.tsx
index 742302762..0e53deead 100644
--- a/client/src/Routes/v2router.tsx
+++ b/client/src/Routes/v2router.tsx
@@ -5,7 +5,8 @@ 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 CreateUptimePage from "@/Pages/v2/Uptime/Create";
+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" }) => {
@@ -34,9 +35,13 @@ const V2Routes = ({ mode = "light" }) => {
path="uptime"
element={}
/>
+ }
+ />
}
+ element={}
/>
diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts
index c3c3712f0..542507a4b 100644
--- a/client/src/Types/Check.ts
+++ b/client/src/Types/Check.ts
@@ -1,8 +1,38 @@
+export interface CheckTimingPhases {
+ wait: number;
+ dns: number;
+ tcp: number;
+ tls: number;
+ request: number;
+ firstByte: number;
+ download: number;
+ total: number;
+}
+
+export interface CheckTimings {
+ start: string;
+ socket: string;
+ lookup: string;
+ connect: string;
+ secureConnect: string;
+ response: string;
+ end: string;
+ phases: CheckTimingPhases;
+}
+
export interface Check {
_id: string;
+ monitorId: string;
+ type: string;
status: string;
+ message: string;
responseTime: number;
+ httpStatusCode: number;
+ ack: boolean;
+ expiry: string;
createdAt: string;
+ updatedAt: string;
+ timings: CheckTimings;
}
export interface GroupedCheck {
diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts
index 8ceb4d009..05f7d7d6f 100644
--- a/client/src/Types/Monitor.ts
+++ b/client/src/Types/Monitor.ts
@@ -1,4 +1,5 @@
import type { Check } from "@/Types/Check";
+export type MonitorStatus = "up" | "down" | "initializing";
export interface IMonitor {
checks: Check[];
@@ -9,7 +10,7 @@ export interface IMonitor {
latestChecks: Check[];
n: number;
name: string;
- status: string;
+ status: MonitorStatus;
type: string;
updatedAt: string;
updatedBy: string;
diff --git a/client/src/Utils/ApiClient.ts b/client/src/Utils/ApiClient.ts
index a44c35905..4f41efe2d 100644
--- a/client/src/Utils/ApiClient.ts
+++ b/client/src/Utils/ApiClient.ts
@@ -18,4 +18,10 @@ export const post = (
config: AxiosRequestConfig = {}
): Promise> => api.post(url, data, config);
+export const patch = (
+ url: string,
+ data: any,
+ config: AxiosRequestConfig = {}
+): Promise> => api.patch(url, data, config);
+
export default api;
diff --git a/client/src/Utils/MonitorUtils.ts b/client/src/Utils/MonitorUtils.ts
new file mode 100644
index 000000000..dcd202361
--- /dev/null
+++ b/client/src/Utils/MonitorUtils.ts
@@ -0,0 +1,38 @@
+import type { MonitorStatus } from "@/Types/Monitor";
+import type { PaletteKey } from "./Theme/v2/theme";
+export const getStatusPalette = (status: MonitorStatus): PaletteKey => {
+ const paletteMap: Record = {
+ up: "success",
+ down: "error",
+ initializing: "warning",
+ };
+ return paletteMap[status];
+};
+
+export const getStatusColor = (status: MonitorStatus, theme: any): string => {
+ const statusColors: Record = {
+ up: theme.palette.success.lowContrast,
+ down: theme.palette.error.lowContrast,
+ initializing: theme.palette.warning.lowContrast,
+ };
+ return statusColors[status];
+};
+
+export const getResponseTimeColor = (responseTime: number): PaletteKey => {
+ if (responseTime < 200) {
+ return "success";
+ } else if (responseTime < 300) {
+ return "warning";
+ } else {
+ return "error";
+ }
+};
+
+export const formatUrl = (url: string, maxLength: number = 55) => {
+ if (!url) return "";
+
+ const strippedUrl = url.replace(/^https?:\/\//, "");
+ return strippedUrl.length > maxLength
+ ? `${strippedUrl.slice(0, maxLength)}…`
+ : strippedUrl;
+};
diff --git a/client/src/Utils/Theme/v2/theme.ts b/client/src/Utils/Theme/v2/theme.ts
index 3d68613b9..d2581e236 100644
--- a/client/src/Utils/Theme/v2/theme.ts
+++ b/client/src/Utils/Theme/v2/theme.ts
@@ -1,5 +1,12 @@
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)";
diff --git a/client/src/Utils/TimeUtils.ts b/client/src/Utils/TimeUtils.ts
index 5dc9b23c1..618d9d1bc 100644
--- a/client/src/Utils/TimeUtils.ts
+++ b/client/src/Utils/TimeUtils.ts
@@ -23,3 +23,31 @@ export const formatDateWithTz = (timestamp: string, format: string, timezone: st
const formattedDate = dayjs(timestamp).tz(timezone).format(format);
return formattedDate;
};
+
+export const tickDateFormatLookup = (range: string) => {
+ const tickFormatLookup: Record = {
+ "2h": "h:mm A",
+ "24h": "h:mm A",
+ "7d": "MM/D, h:mm A",
+ "30d": "ddd. M/D",
+ };
+ const format = tickFormatLookup[range];
+ if (format === undefined) {
+ return "";
+ }
+ return format;
+};
+
+export const tooltipDateFormatLookup = (range: string) => {
+ const dateFormatLookup: Record = {
+ "2h": "ddd. MMMM D, YYYY, hh:mm A",
+ "24h": "ddd. MMMM D, YYYY, hh:mm A",
+ "7d": "ddd. MMMM D, YYYY, hh:mm A",
+ "30d": "ddd. MMMM D, YYYY",
+ };
+ const format = dateFormatLookup[range];
+ if (format === undefined) {
+ return "";
+ }
+ return format;
+};
diff --git a/server/nodemon.json b/server/nodemon.json
index 88d6ac686..389209fa7 100755
--- a/server/nodemon.json
+++ b/server/nodemon.json
@@ -1,5 +1,5 @@
{
"ignore": ["src/locales/*", "*.log", "node_modules/*"],
- "watch": ["src/**/*.js", "*.json"],
- "ext": "js,json"
+ "watch": ["src/**/*.ts", "src/**/*.js", "*.json"],
+ "ext": "ts,js,json"
}
diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js
index 78cd7091b..927acd1a2 100644
--- a/server/src/config/controllers.js
+++ b/server/src/config/controllers.js
@@ -74,7 +74,7 @@ export const initializeControllers = (services) => {
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);
+ controllers.monitorControllerV2 = new MonitorControllerV2(services.monitorServiceV2, services.checkServiceV2);
controllers.notificationChannelControllerV2 = new NotificationChannelControllerV2(services.notificationChannelServiceV2);
controllers.queueControllerV2 = new QueueControllerV2(services.jobQueueV2);
diff --git a/server/src/controllers/v2/MonitorController.ts b/server/src/controllers/v2/MonitorController.ts
index c9a4ce7da..7c0c63d2c 100644
--- a/server/src/controllers/v2/MonitorController.ts
+++ b/server/src/controllers/v2/MonitorController.ts
@@ -2,10 +2,13 @@ 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;
- constructor(monitorService: MonitorService) {
+ private checkService: CheckService;
+ constructor(monitorService: MonitorService, checkService: CheckService) {
this.monitorService = monitorService;
+ this.checkService = checkService;
}
create = async (req: Request, res: Response, next: NextFunction) => {
@@ -25,6 +28,86 @@ class MonitorController {
}
};
+ 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;
@@ -62,55 +145,6 @@ class MonitorController {
}
};
- 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);
- }
- };
-
- 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);
- }
- };
-
update = async (req: Request, res: Response, next: NextFunction) => {
try {
const tokenizedUser = req.user;
diff --git a/server/src/routes/v2/monitors.ts b/server/src/routes/v2/monitors.ts
index eb218f4c6..61e5b61d6 100644
--- a/server/src/routes/v2/monitors.ts
+++ b/server/src/routes/v2/monitors.ts
@@ -17,12 +17,14 @@ class MonitorRoutes {
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.patch("/:id", verifyToken, verifyPermission(["monitors.update"]), this.controller.update);
-
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);
};
diff --git a/server/src/service/v2/business/CheckService.ts b/server/src/service/v2/business/CheckService.ts
index 7187fb5a3..54593ed2b 100644
--- a/server/src/service/v2/business/CheckService.ts
+++ b/server/src/service/v2/business/CheckService.ts
@@ -5,6 +5,7 @@ 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 {
@@ -60,6 +61,7 @@ class CheckService implements ICheckService {
monitorId: monitorId,
type: statusResponse?.type,
status: statusResponse?.status,
+ httpStatusCode: statusResponse?.code,
message: statusResponse?.message,
responseTime: statusResponse?.responseTime,
timings: statusResponse?.timings,
@@ -130,6 +132,16 @@ class CheckService implements ICheckService {
return false;
}
};
+
+ getChecks = async (monitorId: string, page: number, rowsPerPage: number) => {
+ const count = await Check.countDocuments({ monitorId: new mongoose.Types.ObjectId(monitorId) });
+ const checks = await Check.find({ monitorId: new mongoose.Types.ObjectId(monitorId) })
+ .sort({ createdAt: -1 })
+ .skip(page * rowsPerPage)
+ .limit(rowsPerPage)
+ .exec();
+ return { checks, count };
+ };
}
export default CheckService;
diff --git a/server/src/service/v2/business/MonitorService.ts b/server/src/service/v2/business/MonitorService.ts
index 61fc8b114..c0570236b 100644
--- a/server/src/service/v2/business/MonitorService.ts
+++ b/server/src/service/v2/business/MonitorService.ts
@@ -63,8 +63,8 @@ class MonitorService implements IMonitorService {
private getStartDate(range: string): Date {
const now = new Date();
switch (range) {
- case "30m":
- return new Date(now.getTime() - 30 * 60 * 1000);
+ case "2h":
+ return new Date(now.getTime() - 2 * 60 * 60 * 1000);
case "24h":
return new Date(now.getTime() - 24 * 60 * 60 * 1000);
case "7d":
@@ -78,7 +78,7 @@ class MonitorService implements IMonitorService {
private getDateFormat(range: string): string {
switch (range) {
- case "30m":
+ case "2h":
return "%Y-%m-%dT%H:%M:00Z";
case "24h":
case "7d":