Compare commits
123
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea698b6558 | ||
|
|
189293b04d | ||
|
|
13e9185011 | ||
|
|
a7cf78cecc | ||
|
|
0b30e23783 | ||
|
|
1155619835 | ||
|
|
e0608ad3ed | ||
|
|
d665e95f02 | ||
|
|
afc27c804d | ||
|
|
1deb22bf63 | ||
|
|
af8b128ac2 | ||
|
|
00150ba6f4 | ||
|
|
7f04dd5d9a | ||
|
|
ab5389190d | ||
|
|
cba9f43032 | ||
|
|
1cd6e723cc | ||
|
|
f79a87f98a | ||
|
|
a0ea50d745 | ||
|
|
bfa11216f0 | ||
|
|
07a2f36dd7 | ||
|
|
8e44718787 | ||
|
|
83572aa12a | ||
|
|
332a21cd99 | ||
|
|
128d1d12b0 | ||
|
|
f73b07cd3c | ||
|
|
a7d14e0e43 | ||
|
|
6622a990be | ||
|
|
3854931682 | ||
|
|
09534757a2 | ||
|
|
7b2ce0e350 | ||
|
|
19887332c6 | ||
|
|
a1858e8302 | ||
|
|
9fd006cfff | ||
|
|
75a7e144cd | ||
|
|
16cf23baf1 | ||
|
|
8236b02034 | ||
|
|
64242322a2 | ||
|
|
c1e03a45b0 | ||
|
|
f1466df56c | ||
|
|
3654c58469 | ||
|
|
b997c43bd5 | ||
|
|
a5d3c95145 | ||
|
|
003106348a | ||
|
|
c3a41272a3 | ||
|
|
0bff26fe47 | ||
|
|
9169410c1a | ||
|
|
9174e66134 | ||
|
|
04c3c57a59 | ||
|
|
f69b6bbf31 | ||
|
|
05aa1e7112 | ||
|
|
8aead0a00c | ||
|
|
db8ed2994a | ||
|
|
8571ccea78 | ||
|
|
4d273a6dce | ||
|
|
4aabc16096 | ||
|
|
94f9c3d118 | ||
|
|
a719364e40 | ||
|
|
308a439790 | ||
|
|
42a001b24e | ||
|
|
f48fc24b9e | ||
|
|
e3057ad039 | ||
|
|
32f41a3cc4 | ||
|
|
177d6f80d0 | ||
|
|
ecef55f1b3 | ||
|
|
dede689990 | ||
|
|
85edea713a | ||
|
|
7e3da7add7 | ||
|
|
dfeba1e4fb | ||
|
|
0442c8dd0f | ||
|
|
7960f1e82c | ||
|
|
f7f1602423 | ||
|
|
78d1451621 | ||
|
|
a9f005b2c4 | ||
|
|
b68214fc47 | ||
|
|
64c0c93c3e | ||
|
|
40a1f0c6e0 | ||
|
|
1bb2dd323a | ||
|
|
17238c982e | ||
|
|
cc1d63f91a | ||
|
|
ba6e931c59 | ||
|
|
885acf6fe6 | ||
|
|
477c30286d | ||
|
|
88e28b9819 | ||
|
|
e7ba3cb5f9 | ||
|
|
651fbd7cbb | ||
|
|
64fdd04cc7 | ||
|
|
c3e4b5b95f | ||
|
|
10313102ee | ||
|
|
868cbae66f | ||
|
|
2989968fa1 | ||
|
|
89a7fdbe34 | ||
|
|
432d1d8ccf | ||
|
|
5a74411ea2 | ||
|
|
705ecf41c2 | ||
|
|
1d4e9d6806 | ||
|
|
8e37ed7ba2 | ||
|
|
b870f25d1a | ||
|
|
7d3abb3f98 | ||
|
|
9baa6692dd | ||
|
|
dd7dfba1cf | ||
|
|
858aecce62 | ||
|
|
42fdc47d86 | ||
|
|
af8f913e62 | ||
|
|
ea086d6956 | ||
|
|
22b33d789c | ||
|
|
42e7bc1dbf | ||
|
|
13aa600852 | ||
|
|
c06f81ef76 | ||
|
|
c95db26437 | ||
|
|
94a85f8bbb | ||
|
|
05f8e6a07e | ||
|
|
dac78c7647 | ||
|
|
2d023de568 | ||
|
|
f2c88cfd23 | ||
|
|
910c675a49 | ||
|
|
882e91f6a7 | ||
|
|
06be65ec52 | ||
|
|
ea54736a39 | ||
|
|
da38f2ee76 | ||
|
|
83d56f1ff6 | ||
|
|
f94afb63c6 | ||
|
|
673f47983a | ||
|
|
b708e83cce |
No files matched your search
+3
-1
@@ -5,4 +5,6 @@
|
|||||||
mongo
|
mongo
|
||||||
node_modules/
|
node_modules/
|
||||||
docs/architecture
|
docs/architecture
|
||||||
docs/reviews
|
docs/reviews
|
||||||
|
docs/todo
|
||||||
|
docs/frontend
|
||||||
@@ -42,7 +42,7 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
|
|||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is uptimedemo@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
|
You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is demouser@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
|
||||||
|
|
||||||
## User's guide
|
## User's guide
|
||||||
|
|
||||||
|
|||||||
Generated
+640
-24
File diff suppressed because it is too large.
Load diff
@@ -25,6 +25,7 @@
|
|||||||
"@mui/x-charts": "7.29.1",
|
"@mui/x-charts": "7.29.1",
|
||||||
"@mui/x-date-pickers": "7.29.4",
|
"@mui/x-date-pickers": "7.29.4",
|
||||||
"@reduxjs/toolkit": "2.7.0",
|
"@reduxjs/toolkit": "2.7.0",
|
||||||
|
"@types/maplibre-gl": "^1.13.2",
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.4",
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
"flag-icons": "7.3.2",
|
"flag-icons": "7.3.2",
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
"i18next": "25.4.2",
|
"i18next": "25.4.2",
|
||||||
"joi": "17.13.3",
|
"joi": "17.13.3",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
|
"maplibre-gl": "^5.19.0",
|
||||||
"mui-color-input": "^7.0.0",
|
"mui-color-input": "^7.0.0",
|
||||||
"pretty-bytes": "^7.1.0",
|
"pretty-bytes": "^7.1.0",
|
||||||
"pretty-ms": "^9.3.0",
|
"pretty-ms": "^9.3.0",
|
||||||
@@ -41,6 +43,7 @@
|
|||||||
"react-hook-form": "^7.63.0",
|
"react-hook-form": "^7.63.0",
|
||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-icons": "5.5.0",
|
"react-icons": "5.5.0",
|
||||||
|
"react-map-gl": "^8.1.0",
|
||||||
"react-redux": "9.2.0",
|
"react-redux": "9.2.0",
|
||||||
"react-router": "^6.23.0",
|
"react-router": "^6.23.0",
|
||||||
"react-router-dom": "^6.23.1",
|
"react-router-dom": "^6.23.1",
|
||||||
|
|||||||
+36
-23
@@ -4,39 +4,52 @@ import "react-toastify/dist/ReactToastify.css";
|
|||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import { ThemeProvider } from "@emotion/react";
|
import { ThemeProvider } from "@emotion/react";
|
||||||
import { CssBaseline, GlobalStyles } from "@mui/material";
|
import { CssBaseline, GlobalStyles } from "@mui/material";
|
||||||
|
import { SWRConfig } from "swr";
|
||||||
import { Routes } from "./Routes";
|
import { Routes } from "./Routes";
|
||||||
import AppLayout from "@/Components/layout/AppLayout";
|
import AppLayout from "@/Components/layout/AppLayout";
|
||||||
import type { RootState } from "@/Types/state";
|
import type { RootState } from "@/Types/state";
|
||||||
import { lightTheme, darkTheme } from "@/Utils/Theme/Theme";
|
import { lightTheme, darkTheme } from "@/Utils/Theme/Theme";
|
||||||
|
|
||||||
|
const swrConfig = {
|
||||||
|
dedupingInterval: 5000,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateOnReconnect: true,
|
||||||
|
shouldRetryOnError: false,
|
||||||
|
errorRetryCount: 2,
|
||||||
|
errorRetryInterval: 1000,
|
||||||
|
focusThrottleInterval: 5000,
|
||||||
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const mode = useSelector((state: RootState) => state.ui.mode);
|
const mode = useSelector((state: RootState) => state.ui.mode);
|
||||||
const theme = mode === "light" ? lightTheme : darkTheme;
|
const theme = mode === "light" ? lightTheme : darkTheme;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<SWRConfig value={swrConfig}>
|
||||||
<CssBaseline />
|
<ThemeProvider theme={theme}>
|
||||||
<GlobalStyles
|
<CssBaseline />
|
||||||
styles={{
|
<GlobalStyles
|
||||||
body: {
|
styles={{
|
||||||
backgroundColor: theme.palette.background.default,
|
body: {
|
||||||
},
|
backgroundColor: theme.palette.background.default,
|
||||||
}}
|
},
|
||||||
/>
|
}}
|
||||||
<AppLayout>
|
/>
|
||||||
<Routes />
|
<AppLayout>
|
||||||
</AppLayout>
|
<Routes />
|
||||||
<ToastContainer
|
</AppLayout>
|
||||||
newestOnTop={true}
|
<ToastContainer
|
||||||
theme={mode}
|
newestOnTop={true}
|
||||||
style={
|
theme={mode}
|
||||||
{
|
style={
|
||||||
"--toastify-color-progress-light": "#7C8BA1",
|
{
|
||||||
"--toastify-color-progress-dark": "#7C8BA1",
|
"--toastify-color-progress-light": "#7C8BA1",
|
||||||
} as CSSProperties
|
"--toastify-color-progress-dark": "#7C8BA1",
|
||||||
}
|
} as CSSProperties
|
||||||
/>
|
}
|
||||||
</ThemeProvider>
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
|
</SWRConfig>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { BaseBox } from ".";
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||||
import type { ResponsiveStyleValue } from "@mui/system";
|
import type { ResponsiveStyleValue } from "@mui/system";
|
||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
@@ -26,7 +27,7 @@ export const BaseChart = ({
|
|||||||
return (
|
return (
|
||||||
<BaseBox
|
<BaseBox
|
||||||
sx={{
|
sx={{
|
||||||
padding: padding ?? theme.spacing(8),
|
padding: padding ?? theme.spacing(LAYOUT.MD),
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
width: width,
|
width: width,
|
||||||
@@ -34,13 +35,13 @@ export const BaseChart = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
gap={theme.spacing(8)}
|
gap={theme.spacing(LAYOUT.MD)}
|
||||||
flex={1}
|
flex={1}
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems={"center"}
|
alignItems={"center"}
|
||||||
gap={theme.spacing(4)}
|
gap={theme.spacing(LAYOUT.XS)}
|
||||||
>
|
>
|
||||||
{icon && (
|
{icon && (
|
||||||
<BaseBox
|
<BaseBox
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Stack from "@mui/material/Stack";
|
|||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
|
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||||
import {
|
import {
|
||||||
ErrorFallback,
|
ErrorFallback,
|
||||||
EmptyFallback,
|
EmptyFallback,
|
||||||
@@ -91,7 +92,7 @@ export const BasePage = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
spacing={theme.spacing(10)}
|
spacing={theme.spacing(LAYOUT.LG)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
|
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
|
||||||
@@ -220,7 +221,7 @@ export const BaseAuthPage = ({
|
|||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
breadcrumbOverride={[]}
|
breadcrumbOverride={[]}
|
||||||
gap={theme.spacing(8)}
|
gap={theme.spacing(LAYOUT.MD)}
|
||||||
alignItems={"center"}
|
alignItems={"center"}
|
||||||
justifyContent={"center"}
|
justifyContent={"center"}
|
||||||
minHeight="100vh"
|
minHeight="100vh"
|
||||||
@@ -229,7 +230,7 @@ export const BaseAuthPage = ({
|
|||||||
>
|
>
|
||||||
<HeaderAuthControls
|
<HeaderAuthControls
|
||||||
hideLogo
|
hideLogo
|
||||||
py={theme.spacing(4)}
|
py={theme.spacing(LAYOUT.XS)}
|
||||||
position={"absolute"}
|
position={"absolute"}
|
||||||
top={0}
|
top={0}
|
||||||
left={0}
|
left={0}
|
||||||
@@ -243,14 +244,14 @@ export const BaseAuthPage = ({
|
|||||||
<Stack alignItems={"center"}>
|
<Stack alignItems={"center"}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h1"
|
variant="h1"
|
||||||
mb={theme.spacing(2)}
|
mb={theme.spacing(SPACING.LG)}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h2">{subtitle}</Typography>
|
<Typography variant="h2">{subtitle}</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack
|
<Stack
|
||||||
gap={theme.spacing(8)}
|
gap={theme.spacing(LAYOUT.MD)}
|
||||||
width={{
|
width={{
|
||||||
xs: "80%",
|
xs: "80%",
|
||||||
md: "25%",
|
md: "25%",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Stack from "@mui/material/Stack";
|
|||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||||
|
|
||||||
export const BulletPointCheck = ({
|
export const BulletPointCheck = ({
|
||||||
text,
|
text,
|
||||||
@@ -21,7 +22,7 @@ export const BulletPointCheck = ({
|
|||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
gap={theme.spacing(6)}
|
gap={theme.spacing(LAYOUT.SM)}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Check
|
<Check
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Stack from "@mui/material/Stack";
|
|||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { BulletPointCheck, SkeletonCard } from "@/Components/design-elements";
|
import { BulletPointCheck, SkeletonCard } from "@/Components/design-elements";
|
||||||
import { Button } from "@/Components/inputs";
|
import { Button } from "@/Components/inputs";
|
||||||
|
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
||||||
|
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
@@ -27,7 +28,7 @@ export const BaseFallback = ({ children, ...props }: BaseFallbackProps) => {
|
|||||||
lg: "50%",
|
lg: "50%",
|
||||||
xl: "40%",
|
xl: "40%",
|
||||||
}}
|
}}
|
||||||
padding={{ xs: theme.spacing(8), md: theme.spacing(16) }}
|
padding={{ xs: theme.spacing(LAYOUT.MD), md: theme.spacing(LAYOUT.XXL) }}
|
||||||
bgcolor={theme.palette.background.paper}
|
bgcolor={theme.palette.background.paper}
|
||||||
border={1}
|
border={1}
|
||||||
borderColor={theme.palette.divider}
|
borderColor={theme.palette.divider}
|
||||||
@@ -55,7 +56,7 @@ export const ErrorFallback = ({
|
|||||||
<BaseFallback>
|
<BaseFallback>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h1"
|
variant="h1"
|
||||||
marginY={theme.spacing(4)}
|
marginY={theme.spacing(LAYOUT.XS)}
|
||||||
color={theme.palette.text.secondary}
|
color={theme.palette.text.secondary}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
@@ -81,7 +82,7 @@ export const EmptyFallback = ({
|
|||||||
return (
|
return (
|
||||||
<BaseFallback>
|
<BaseFallback>
|
||||||
<Stack
|
<Stack
|
||||||
gap={theme.spacing(10)}
|
gap={theme.spacing(LAYOUT.MD)}
|
||||||
zIndex={1}
|
zIndex={1}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
@@ -89,7 +90,7 @@ export const EmptyFallback = ({
|
|||||||
<Stack
|
<Stack
|
||||||
sx={{
|
sx={{
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
gap: theme.spacing(2),
|
gap: theme.spacing(SPACING.MD),
|
||||||
maxWidth: "1100px",
|
maxWidth: "1100px",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
@@ -134,7 +135,7 @@ export const EmptyMonitorFallback = ({
|
|||||||
return (
|
return (
|
||||||
<BaseFallback>
|
<BaseFallback>
|
||||||
<Stack
|
<Stack
|
||||||
gap={theme.spacing(10)}
|
gap={theme.spacing(LAYOUT.MD)}
|
||||||
zIndex={1}
|
zIndex={1}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
@@ -147,7 +148,7 @@ export const EmptyMonitorFallback = ({
|
|||||||
<Stack
|
<Stack
|
||||||
sx={{
|
sx={{
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
gap: theme.spacing(2),
|
gap: theme.spacing(SPACING.MD),
|
||||||
maxWidth: "1100px",
|
maxWidth: "1100px",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Box from "@mui/material/Box";
|
|||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||||
export const SplitBox = ({
|
export const SplitBox = ({
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
@@ -18,10 +19,10 @@ export const SplitBox = ({
|
|||||||
bgcolor={theme.palette.background.paper}
|
bgcolor={theme.palette.background.paper}
|
||||||
border={1}
|
border={1}
|
||||||
borderColor={theme.palette.divider}
|
borderColor={theme.palette.divider}
|
||||||
borderRadius={theme.spacing(2)}
|
borderRadius={theme.shape.borderRadius}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
padding={theme.spacing(15)}
|
padding={theme.spacing(LAYOUT.XXL)}
|
||||||
borderRight={isSmall ? 0 : 1}
|
borderRight={isSmall ? 0 : 1}
|
||||||
borderBottom={isSmall ? 1 : 0}
|
borderBottom={isSmall ? 1 : 0}
|
||||||
borderColor={theme.palette.divider}
|
borderColor={theme.palette.divider}
|
||||||
@@ -37,7 +38,7 @@ export const SplitBox = ({
|
|||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
flex={1}
|
flex={1}
|
||||||
padding={theme.spacing(15)}
|
padding={theme.spacing(LAYOUT.XXL)}
|
||||||
>
|
>
|
||||||
{right}
|
{right}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -60,7 +61,7 @@ export const ConfigBox = ({
|
|||||||
return (
|
return (
|
||||||
<SplitBox
|
<SplitBox
|
||||||
left={
|
left={
|
||||||
<Stack spacing={theme.spacing(4)}>
|
<Stack spacing={theme.spacing(LAYOUT.XS)}>
|
||||||
<Typography
|
<Typography
|
||||||
textTransform={"capitalize"}
|
textTransform={"capitalize"}
|
||||||
component="h2"
|
component="h2"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import type { TablePaginationOwnProps } from "@mui/material/TablePagination";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useState, Fragment, useEffect } from "react";
|
import { useState, Fragment, useEffect } from "react";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
||||||
import type { SxProps, Theme } from "@mui/material/styles";
|
import type { SxProps, Theme } from "@mui/material/styles";
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
|
||||||
@@ -90,20 +91,20 @@ export function DataTable<
|
|||||||
// Return stack of cards for small screens
|
// Return stack of cards for small screens
|
||||||
if (isSmall && cardsOnSmallScreens) {
|
if (isSmall && cardsOnSmallScreens) {
|
||||||
return (
|
return (
|
||||||
<Stack spacing={theme.spacing(4)}>
|
<Stack spacing={theme.spacing(LAYOUT.XS)}>
|
||||||
{data.map((row) => {
|
{data.map((row) => {
|
||||||
const key = row.id || row._id || Math.random();
|
const key = row.id || row._id || Math.random();
|
||||||
keys.push(key);
|
keys.push(key);
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
onClick={() => (onRowClick ? onRowClick(row) : null)}
|
onClick={() => (onRowClick ? onRowClick(row) : null)}
|
||||||
spacing={theme.spacing(4)}
|
spacing={theme.spacing(LAYOUT.XS)}
|
||||||
sx={{
|
sx={{
|
||||||
borderStyle: "solid",
|
borderStyle: "solid",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: theme.palette.divider,
|
borderColor: theme.palette.divider,
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
padding: theme.spacing(4),
|
padding: theme.spacing(LAYOUT.XS),
|
||||||
cursor: onRowClick ? "pointer" : "default",
|
cursor: onRowClick ? "pointer" : "default",
|
||||||
}}
|
}}
|
||||||
key={key}
|
key={key}
|
||||||
@@ -155,7 +156,7 @@ export function DataTable<
|
|||||||
<Table
|
<Table
|
||||||
sx={{
|
sx={{
|
||||||
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
|
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
|
||||||
paddingLeft: theme.spacing(8),
|
paddingLeft: theme.spacing(LAYOUT.MD),
|
||||||
},
|
},
|
||||||
"& .MuiTableCell-root": {
|
"& .MuiTableCell-root": {
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
@@ -168,13 +169,13 @@ export function DataTable<
|
|||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
padding: `${theme.spacing(2)} ${theme.spacing(8)}`,
|
padding: `${theme.spacing(SPACING.LG)} ${theme.spacing(LAYOUT.MD)}`,
|
||||||
fontSize: theme.typography.fontSize,
|
fontSize: theme.typography.fontSize,
|
||||||
},
|
},
|
||||||
"& :is(td)": {
|
"& :is(td)": {
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
padding: `${theme.spacing(7)} ${theme.spacing(8)}`,
|
padding: `${theme.spacing(LAYOUT.MD)} ${theme.spacing(LAYOUT.MD)}`,
|
||||||
fontSize: theme.typography.fontSize,
|
fontSize: theme.typography.fontSize,
|
||||||
overflowX: "hidden",
|
overflowX: "hidden",
|
||||||
},
|
},
|
||||||
@@ -514,7 +515,7 @@ export const Pagination = ({ ...props }: PaginationProps) => {
|
|||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "1fr 1fr",
|
gridTemplateColumns: "1fr 1fr",
|
||||||
gridAutoRows: "auto",
|
gridAutoRows: "auto",
|
||||||
rowGap: theme.spacing(1),
|
rowGap: theme.spacing(SPACING.SM),
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyItems: "center",
|
justifyItems: "center",
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
@@ -556,8 +557,8 @@ const EmptyView = ({ text, positive }: { text?: string; positive?: boolean }) =>
|
|||||||
alignItems={"center"}
|
alignItems={"center"}
|
||||||
justifyContent={"center"}
|
justifyContent={"center"}
|
||||||
sx={{
|
sx={{
|
||||||
py: theme.spacing(12),
|
py: theme.spacing(LAYOUT.XL),
|
||||||
px: theme.spacing(4),
|
px: theme.spacing(LAYOUT.XS),
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderStyle: "solid",
|
borderStyle: "solid",
|
||||||
borderColor: theme.palette.divider,
|
borderColor: theme.palette.divider,
|
||||||
@@ -574,7 +575,7 @@ const EmptyView = ({ text, positive }: { text?: string; positive?: boolean }) =>
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
mb: theme.spacing(2),
|
mb: theme.spacing(SPACING.LG),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -5,8 +5,17 @@ import useMediaQuery from "@mui/material/useMediaQuery";
|
|||||||
|
|
||||||
import type { MonitorType } from "@/Types/Monitor";
|
import type { MonitorType } from "@/Types/Monitor";
|
||||||
import { Typography, useTheme } from "@mui/material";
|
import { Typography, useTheme } from "@mui/material";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const types = ["http", "ping", "port", "docker", "game"];
|
const types = ["http", "ping", "port", "docker", "game", "grpc"];
|
||||||
|
const typeDisplayNames: Record<string, string> = {
|
||||||
|
http: "HTTP",
|
||||||
|
ping: "Ping",
|
||||||
|
port: "Port",
|
||||||
|
docker: "Docker",
|
||||||
|
game: "Game",
|
||||||
|
grpc: "gRPC",
|
||||||
|
};
|
||||||
const statuses = ["up", "down"];
|
const statuses = ["up", "down"];
|
||||||
const states = ["active", "paused"];
|
const states = ["active", "paused"];
|
||||||
|
|
||||||
@@ -30,6 +39,7 @@ export const ControlsFilter = ({
|
|||||||
onClearFilters: () => void;
|
onClearFilters: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
const isFilterActive =
|
const isFilterActive =
|
||||||
(selectedTypes?.length ?? 0) > 0 || selectedStatus !== "" || selectedState !== "";
|
(selectedTypes?.length ?? 0) > 0 || selectedStatus !== "" || selectedState !== "";
|
||||||
@@ -50,7 +60,7 @@ export const ControlsFilter = ({
|
|||||||
key={type}
|
key={type}
|
||||||
value={type}
|
value={type}
|
||||||
>
|
>
|
||||||
<Typography textTransform={"capitalize"}>{type}</Typography>
|
<Typography>{typeDisplayNames[type] ?? type}</Typography>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -88,7 +98,7 @@ export const ControlsFilter = ({
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={onClearFilters}
|
onClick={onClearFilters}
|
||||||
>
|
>
|
||||||
Clear Filters
|
{t("common.buttons.clearFilters")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import { useRef, useEffect, useState } from "react";
|
||||||
|
import { Box, Typography, Stack, useTheme, GlobalStyles } from "@mui/material";
|
||||||
|
import Map, { Marker, Popup } from "react-map-gl/maplibre";
|
||||||
|
import type { MapRef } from "react-map-gl/maplibre";
|
||||||
|
import type { FlatGeoCheck } from "@/Types/GeoCheck";
|
||||||
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
|
|
||||||
|
interface GeoChecksMapProps {
|
||||||
|
geoChecks: FlatGeoCheck[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
|
||||||
|
const mapRef = useRef<MapRef>(null);
|
||||||
|
const [selectedCheck, setSelectedCheck] = useState<FlatGeoCheck | null>(null);
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDarkMode = theme.palette.mode === "dark";
|
||||||
|
|
||||||
|
const mapPopupStyles = (
|
||||||
|
<GlobalStyles
|
||||||
|
styles={{
|
||||||
|
".maplibregl-popup-content": {
|
||||||
|
background: "transparent !important",
|
||||||
|
padding: "0 !important",
|
||||||
|
boxShadow: "none !important",
|
||||||
|
},
|
||||||
|
".maplibregl-popup-tip": {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
".maplibregl-ctrl-attrib": {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
".maplibregl-ctrl-logo": {
|
||||||
|
display: "none",
|
||||||
|
},
|
||||||
|
".maplibregl-popup-close-button": {
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
background: "transparent",
|
||||||
|
fontSize: "20px",
|
||||||
|
padding: "4px",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: theme.palette.action.hover,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (geoChecks.length === 0 || !mapRef.current) return;
|
||||||
|
|
||||||
|
const bounds = geoChecks.reduce(
|
||||||
|
(acc, check) => {
|
||||||
|
return {
|
||||||
|
minLng: Math.min(acc.minLng, check.location.longitude),
|
||||||
|
maxLng: Math.max(acc.maxLng, check.location.longitude),
|
||||||
|
minLat: Math.min(acc.minLat, check.location.latitude),
|
||||||
|
maxLat: Math.max(acc.maxLat, check.location.latitude),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
minLng: Infinity,
|
||||||
|
maxLng: -Infinity,
|
||||||
|
minLat: Infinity,
|
||||||
|
maxLat: -Infinity,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (bounds.minLng !== Infinity) {
|
||||||
|
mapRef.current.fitBounds(
|
||||||
|
[
|
||||||
|
[bounds.minLng, bounds.minLat],
|
||||||
|
[bounds.maxLng, bounds.maxLat],
|
||||||
|
],
|
||||||
|
{ padding: 50, duration: 1000 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [geoChecks]);
|
||||||
|
|
||||||
|
const getMarkerColor = (status: boolean): string => {
|
||||||
|
return status ? theme.palette.success.main : theme.palette.error.main;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatResponseTime = (timing: number): string => {
|
||||||
|
return `${timing.toFixed(0)}ms`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{mapPopupStyles}
|
||||||
|
<Box
|
||||||
|
height={500}
|
||||||
|
width={"100%"}
|
||||||
|
borderRadius={theme.shape.borderRadius}
|
||||||
|
overflow={"hidden"}
|
||||||
|
>
|
||||||
|
<Map
|
||||||
|
ref={mapRef}
|
||||||
|
initialViewState={{
|
||||||
|
longitude: 0,
|
||||||
|
latitude: 20,
|
||||||
|
zoom: 1.5,
|
||||||
|
}}
|
||||||
|
style={{ height: "100%", width: "100%" }}
|
||||||
|
mapStyle={
|
||||||
|
isDarkMode
|
||||||
|
? "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
|
||||||
|
: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{geoChecks.map((check, index) => (
|
||||||
|
<Marker
|
||||||
|
key={`${check.id}-${index}`}
|
||||||
|
longitude={check.location.longitude}
|
||||||
|
latitude={check.location.latitude}
|
||||||
|
anchor="bottom"
|
||||||
|
onClick={(e: any) => {
|
||||||
|
e.originalEvent.stopPropagation();
|
||||||
|
setSelectedCheck(check);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "10px",
|
||||||
|
height: "10px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: getMarkerColor(check.status),
|
||||||
|
boxShadow: "0 2px 4px rgba(0,0,0,0.3)",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Marker>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{selectedCheck && (
|
||||||
|
<Popup
|
||||||
|
longitude={selectedCheck.location.longitude}
|
||||||
|
latitude={selectedCheck.location.latitude}
|
||||||
|
anchor="top"
|
||||||
|
onClose={() => setSelectedCheck(null)}
|
||||||
|
closeOnClick={false}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
minWidth: 200,
|
||||||
|
p: theme.spacing(4),
|
||||||
|
bgcolor: theme.palette.background.paper,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: "solid",
|
||||||
|
borderColor: theme.palette.divider,
|
||||||
|
borderRadius: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
fontWeight="bold"
|
||||||
|
gutterBottom
|
||||||
|
>
|
||||||
|
{selectedCheck.location.city}, {selectedCheck.location.country}
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={0.5}>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
|
Status:
|
||||||
|
</Typography>{" "}
|
||||||
|
{selectedCheck.status ? "Up" : "Down"}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
|
Status Code:
|
||||||
|
</Typography>{" "}
|
||||||
|
{selectedCheck.statusCode}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
fontWeight="medium"
|
||||||
|
>
|
||||||
|
Response Time:
|
||||||
|
</Typography>{" "}
|
||||||
|
{formatResponseTime(selectedCheck.timings.total)}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ mt: 0.5 }}
|
||||||
|
>
|
||||||
|
{new Date(selectedCheck.createdAt).toLocaleString()}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Popup>
|
||||||
|
)}
|
||||||
|
</Map>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { Tabs, Tab } from "@/Components/design-elements";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import type { GeoContinent } from "@/Types/GeoCheck";
|
||||||
|
|
||||||
|
interface HeaderGeoTabsProps {
|
||||||
|
geoCheckEnabled: boolean;
|
||||||
|
locations: GeoContinent[] | undefined;
|
||||||
|
selectedLocation: GeoContinent;
|
||||||
|
onLocationChange: (location: GeoContinent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HeaderGeoTabs = ({
|
||||||
|
geoCheckEnabled,
|
||||||
|
locations,
|
||||||
|
selectedLocation,
|
||||||
|
onLocationChange,
|
||||||
|
}: HeaderGeoTabsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
if (!geoCheckEnabled || !locations || locations.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (_event: React.SyntheticEvent, newValue: GeoContinent) => {
|
||||||
|
onLocationChange(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
spacing={{ xs: theme.spacing(8), md: 0 }}
|
||||||
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
alignItems={"center"}
|
||||||
|
justifyContent={"flex-start"}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
value={selectedLocation}
|
||||||
|
onChange={handleChange}
|
||||||
|
>
|
||||||
|
{locations.map((location) => (
|
||||||
|
<Tab
|
||||||
|
key={location}
|
||||||
|
label={t(
|
||||||
|
`pages.createMonitor.form.geoChecks.option.locations.options.${location}`
|
||||||
|
)}
|
||||||
|
value={location}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { HistogramPageSpeedScoresTooltip } from "@/Components/monitors";
|
import { HistogramPageSpeedScoresTooltip } from "@/Components/monitors";
|
||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import type { CheckSnapshot } from "@/Types/Check";
|
import type { PageSpeedGroupedCheck } from "@/Types/Check";
|
||||||
import type { Palette } from "@mui/material/styles";
|
import type { Palette } from "@mui/material/styles";
|
||||||
type PaletteColorKey = Extract<
|
type PaletteColorKey = Extract<
|
||||||
keyof Palette,
|
keyof Palette,
|
||||||
@@ -52,7 +52,7 @@ export const HistogramPageSpeedDetails = ({
|
|||||||
checks,
|
checks,
|
||||||
range,
|
range,
|
||||||
}: {
|
}: {
|
||||||
checks: CheckSnapshot[];
|
checks: PageSpeedGroupedCheck[];
|
||||||
range: string;
|
range: string;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -73,7 +73,7 @@ export const HistogramPageSpeedDetails = ({
|
|||||||
>
|
>
|
||||||
<AreaChart data={checks}>
|
<AreaChart data={checks}>
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey={"createdAt"}
|
dataKey={"bucketDate"}
|
||||||
tick={(props) => (
|
tick={(props) => (
|
||||||
<XTick
|
<XTick
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Box from "@mui/material/Box";
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
||||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||||
import type { ConfigItem } from "@/Components/monitors";
|
import type { ConfigItem } from "@/Components/monitors";
|
||||||
import type { TooltipProps } from "recharts";
|
import type { TooltipProps } from "recharts";
|
||||||
@@ -30,8 +31,8 @@ export const HistogramPageSpeedScoresTooltip = ({
|
|||||||
border: 1,
|
border: 1,
|
||||||
borderColor: theme.palette.divider,
|
borderColor: theme.palette.divider,
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
py: theme.spacing(2),
|
py: theme.spacing(SPACING.LG),
|
||||||
px: theme.spacing(4),
|
px: theme.spacing(LAYOUT.XS),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
@@ -54,12 +55,12 @@ export const HistogramPageSpeedScoresTooltip = ({
|
|||||||
key={`${key}-tooltip`}
|
key={`${key}-tooltip`}
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
gap={theme.spacing(3)}
|
gap={theme.spacing(SPACING.XXL)}
|
||||||
mt={theme.spacing(1)}
|
mt={theme.spacing(SPACING.SM)}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
width={theme.spacing(4)}
|
width={theme.spacing(LAYOUT.XS)}
|
||||||
height={theme.spacing(4)}
|
height={theme.spacing(LAYOUT.XS)}
|
||||||
sx={{ borderRadius: "50%", backgroundColor: dotColor }}
|
sx={{ borderRadius: "50%", backgroundColor: dotColor }}
|
||||||
/>
|
/>
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
export * from "./ControlsFilter";
|
export * from "./ControlsFilter";
|
||||||
export * from "./MonitorStatBoxes";
|
export * from "./MonitorStatBoxes";
|
||||||
export * from "./HeaderMonitorControls";
|
export * from "./HeaderMonitorControls";
|
||||||
|
export * from "./HeaderGeoTabs";
|
||||||
|
export * from "./GeoChecksMap";
|
||||||
export * from "./charts/HistogramStatus";
|
export * from "./charts/HistogramStatus";
|
||||||
export * from "./charts/RadialAvgResponse";
|
export * from "./charts/RadialAvgResponse";
|
||||||
export * from "./charts/HistogramDetails";
|
export * from "./charts/HistogramDetails";
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import IconButton from "@mui/material/IconButton";
|
|||||||
import Menu from "@mui/material/Menu";
|
import Menu from "@mui/material/Menu";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from "@mui/material";
|
||||||
|
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -64,9 +65,9 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
const menuItemSx = {
|
const menuItemSx = {
|
||||||
gap: theme.spacing(2),
|
gap: theme.spacing(LAYOUT.MD),
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
pl: theme.spacing(4),
|
pl: theme.spacing(LAYOUT.XS),
|
||||||
"& svg": { stroke: theme.palette.text.secondary },
|
"& svg": { stroke: theme.palette.text.secondary },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,9 +75,9 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
py={theme.spacing(4)}
|
py={theme.spacing(LAYOUT.XS)}
|
||||||
px={theme.spacing(8)}
|
px={theme.spacing(LAYOUT.MD)}
|
||||||
gap={theme.spacing(4)}
|
gap={theme.spacing(LAYOUT.MD)}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
small
|
small
|
||||||
@@ -88,7 +89,7 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
gap={theme.spacing(2)}
|
gap={theme.spacing(SPACING.MD)}
|
||||||
minWidth={0}
|
minWidth={0}
|
||||||
flex={1}
|
flex={1}
|
||||||
>
|
>
|
||||||
@@ -132,10 +133,15 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
|
|||||||
disableScrollLock
|
disableScrollLock
|
||||||
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
paper: { sx: { mt: theme.spacing(-4), ml: collapsed ? theme.spacing(2) : 0 } },
|
paper: {
|
||||||
|
sx: {
|
||||||
|
mt: theme.spacing(-LAYOUT.XS),
|
||||||
|
ml: collapsed ? theme.spacing(SPACING.LG) : 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
MenuListProps={{ sx: { p: 2, "& li": { m: 0 } } }}
|
MenuListProps={{ sx: { p: 2, "& li": { m: 0 } } }}
|
||||||
sx={{ ml: theme.spacing(4) }}
|
sx={{ ml: theme.spacing(LAYOUT.XS) }}
|
||||||
>
|
>
|
||||||
{collapsed && (
|
{collapsed && (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Typography, IconButton, Stack, Box } from "@mui/material";
|
import { Typography, IconButton, Stack, Box } from "@mui/material";
|
||||||
import { Icon } from "@/Components/design-elements";
|
import { Icon } from "@/Components/design-elements";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
|
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
||||||
|
|
||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from "@mui/material";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
@@ -34,25 +35,22 @@ export const StarPrompt = ({
|
|||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
direction="column"
|
direction="column"
|
||||||
sx={{
|
gap={theme.spacing(LAYOUT.MD)}
|
||||||
width: "100%",
|
padding={theme.spacing(LAYOUT.SM)}
|
||||||
padding: `${theme.spacing(6)} ${theme.spacing(6)}`,
|
width={"100%"}
|
||||||
borderTop: `1px solid ${theme.palette.divider}`,
|
borderTop={`1px solid ${theme.palette.divider}`}
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
borderBottom={`1px solid ${theme.palette.divider}`}
|
||||||
borderRadius: 0,
|
|
||||||
gap: theme.spacing(1.5),
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
width="100%"
|
width="100%"
|
||||||
pl={theme.spacing(4)}
|
pl={theme.spacing(LAYOUT.XS)}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="subtitle2"
|
variant="subtitle2"
|
||||||
mt={theme.spacing(3)}
|
mt={theme.spacing(SPACING.XXL)}
|
||||||
>
|
>
|
||||||
{t("components.sidebar.starPrompt.title")}
|
{t("components.sidebar.starPrompt.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -78,7 +76,7 @@ export const StarPrompt = ({
|
|||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
mb: 1,
|
mb: 1,
|
||||||
px: theme.spacing(4),
|
px: theme.spacing(LAYOUT.XS),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("components.sidebar.starPrompt.description")}
|
{t("components.sidebar.starPrompt.description")}
|
||||||
@@ -96,7 +94,7 @@ export const StarPrompt = ({
|
|||||||
"&:hover": {
|
"&:hover": {
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
pl: theme.spacing(4),
|
pl: theme.spacing(LAYOUT.XS),
|
||||||
filter: mode === "dark" ? "invert(1)" : "none",
|
filter: mode === "dark" ? "invert(1)" : "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Backdrop from "@mui/material/Backdrop";
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import List from "@mui/material/List";
|
import List from "@mui/material/List";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
|
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||||
import { useSidebar } from "@/Hooks/useSidebar.js";
|
import { useSidebar } from "@/Hooks/useSidebar.js";
|
||||||
import { Logo } from "@/Components/sidebar/Logo";
|
import { Logo } from "@/Components/sidebar/Logo";
|
||||||
import { getMenu, getBottomMenu, getAccountMenu } from "@/Components/sidebar/Menu";
|
import { getMenu, getBottomMenu, getAccountMenu } from "@/Components/sidebar/Menu";
|
||||||
@@ -65,9 +66,9 @@ export const Sidebar = () => {
|
|||||||
left={0}
|
left={0}
|
||||||
minHeight={"100vh"}
|
minHeight={"100vh"}
|
||||||
maxHeight={"100vh"}
|
maxHeight={"100vh"}
|
||||||
paddingTop={theme.spacing(6)}
|
paddingTop={theme.spacing(LAYOUT.SM)}
|
||||||
paddingBottom={theme.spacing(6)}
|
paddingBottom={theme.spacing(LAYOUT.SM)}
|
||||||
gap={theme.spacing(6)}
|
gap={theme.spacing(LAYOUT.SM)}
|
||||||
borderRight={`1px solid ${theme.palette.divider}`}
|
borderRight={`1px solid ${theme.palette.divider}`}
|
||||||
width={width}
|
width={width}
|
||||||
sx={{
|
sx={{
|
||||||
@@ -81,13 +82,13 @@ export const Sidebar = () => {
|
|||||||
component="nav"
|
component="nav"
|
||||||
disablePadding
|
disablePadding
|
||||||
sx={{
|
sx={{
|
||||||
px: theme.spacing(6),
|
px: theme.spacing(LAYOUT.SM),
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Logo
|
<Logo
|
||||||
pt={theme.spacing(8)}
|
pt={theme.spacing(LAYOUT.MD)}
|
||||||
pb={theme.spacing(10)}
|
pb={theme.spacing(LAYOUT.MD)}
|
||||||
/>
|
/>
|
||||||
{menu.map((item) => {
|
{menu.map((item) => {
|
||||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||||
@@ -105,7 +106,7 @@ export const Sidebar = () => {
|
|||||||
<List
|
<List
|
||||||
component="nav"
|
component="nav"
|
||||||
disablePadding
|
disablePadding
|
||||||
sx={{ px: theme.spacing(6) }}
|
sx={{ px: theme.spacing(LAYOUT.SM) }}
|
||||||
>
|
>
|
||||||
{bottomMenu.map((item) => {
|
{bottomMenu.map((item) => {
|
||||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { AxiosRequestConfig } from "axios";
|
|||||||
import { get, patch, post, put, deleteOp } from "@/Utils/ApiClient";
|
import { get, patch, post, put, deleteOp } from "@/Utils/ApiClient";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useToast } from "@/Hooks/UseToast";
|
import { useToast } from "@/Hooks/UseToast";
|
||||||
|
import { logger } from "@/Utils/logger";
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
interface ApiResponse<T> {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -17,7 +18,7 @@ const fetcher = async <T>(url: string, config?: AxiosRequestConfig) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useGet = <T>(
|
export const useGet = <T>(
|
||||||
url: string | null,
|
url: string | null | undefined,
|
||||||
axiosConfig?: AxiosRequestConfig,
|
axiosConfig?: AxiosRequestConfig,
|
||||||
swrConfig?: SWRConfiguration
|
swrConfig?: SWRConfiguration
|
||||||
) => {
|
) => {
|
||||||
@@ -61,7 +62,7 @@ export const usePost = <B = any, R = any>() => {
|
|||||||
return res.data;
|
return res.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
||||||
console.error(err);
|
logger.error("POST request failed", err, { endpoint, body });
|
||||||
toastError(errMsg);
|
toastError(errMsg);
|
||||||
setError(errMsg);
|
setError(errMsg);
|
||||||
return null;
|
return null;
|
||||||
@@ -96,7 +97,7 @@ export const usePatch = <B = any, R = any>() => {
|
|||||||
toastSuccess(res.data?.msg || "Operation successful");
|
toastSuccess(res.data?.msg || "Operation successful");
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
logger.error("PATCH request failed", err, { endpoint, body });
|
||||||
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
||||||
toastError(errMsg);
|
toastError(errMsg);
|
||||||
setError(errMsg);
|
setError(errMsg);
|
||||||
@@ -132,7 +133,7 @@ export const usePut = <B = any, R = any>() => {
|
|||||||
toastSuccess(res.data?.msg || "Operation successful");
|
toastSuccess(res.data?.msg || "Operation successful");
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
logger.error("PUT request failed", err, { endpoint, body });
|
||||||
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
||||||
toastError(errMsg);
|
toastError(errMsg);
|
||||||
setError(errMsg);
|
setError(errMsg);
|
||||||
@@ -167,7 +168,7 @@ export const useDelete = <R = any>() => {
|
|||||||
toastSuccess(res.data?.msg || "Operation successful");
|
toastSuccess(res.data?.msg || "Operation successful");
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
logger.error("DELETE request failed", err, { endpoint });
|
||||||
const errMsg = err?.response?.data?.message || err.message || "An error occurred";
|
const errMsg = err?.response?.data?.message || err.message || "An error occurred";
|
||||||
toastError(errMsg);
|
toastError(errMsg);
|
||||||
setError(errMsg);
|
setError(errMsg);
|
||||||
@@ -201,7 +202,7 @@ export const useLazyGet = <R = any>() => {
|
|||||||
});
|
});
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
logger.error("GET request failed", err, { endpoint });
|
||||||
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
||||||
toastError(errMsg);
|
toastError(errMsg);
|
||||||
setError(errMsg);
|
setError(errMsg);
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ const getBaseDefaults = (data?: Monitor | null) => ({
|
|||||||
notifications: data?.notifications || [],
|
notifications: data?.notifications || [],
|
||||||
statusWindowSize: data?.statusWindowSize || 5,
|
statusWindowSize: data?.statusWindowSize || 5,
|
||||||
statusWindowThreshold: data?.statusWindowThreshold || 60,
|
statusWindowThreshold: data?.statusWindowThreshold || 60,
|
||||||
|
geoCheckEnabled: data?.geoCheckEnabled ?? false,
|
||||||
|
geoCheckLocations: data?.geoCheckLocations || [],
|
||||||
|
geoCheckInterval: data?.geoCheckInterval || 300000,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useMonitorForm = ({
|
export const useMonitorForm = ({
|
||||||
@@ -37,6 +40,8 @@ export const useMonitorForm = ({
|
|||||||
matchMethod: data?.matchMethod || "",
|
matchMethod: data?.matchMethod || "",
|
||||||
expectedValue: data?.expectedValue || "",
|
expectedValue: data?.expectedValue || "",
|
||||||
jsonPath: data?.jsonPath || "",
|
jsonPath: data?.jsonPath || "",
|
||||||
|
basicAuthUser: data?.basicAuthUser || "",
|
||||||
|
basicAuthPassword: data?.basicAuthPassword || "",
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case "ping":
|
case "ping":
|
||||||
@@ -70,6 +75,16 @@ export const useMonitorForm = ({
|
|||||||
gameId: data?.gameId || "",
|
gameId: data?.gameId || "",
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case "grpc":
|
||||||
|
defaults = {
|
||||||
|
...base,
|
||||||
|
type: "grpc",
|
||||||
|
url: data?.url || "",
|
||||||
|
port: data?.port || 50051,
|
||||||
|
grpcServiceName: data?.grpcServiceName || "",
|
||||||
|
ignoreTlsErrors: data?.ignoreTlsErrors || false,
|
||||||
|
};
|
||||||
|
break;
|
||||||
case "pagespeed":
|
case "pagespeed":
|
||||||
defaults = {
|
defaults = {
|
||||||
...base,
|
...base,
|
||||||
@@ -100,6 +115,8 @@ export const useMonitorForm = ({
|
|||||||
matchMethod: "",
|
matchMethod: "",
|
||||||
expectedValue: "",
|
expectedValue: "",
|
||||||
jsonPath: "",
|
jsonPath: "",
|
||||||
|
basicAuthUser: "",
|
||||||
|
basicAuthPassword: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,22 @@ export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) =>
|
|||||||
systemEmailTLSServername: data?.systemEmailTLSServername || "",
|
systemEmailTLSServername: data?.systemEmailTLSServername || "",
|
||||||
systemEmailPort: data?.systemEmailPort,
|
systemEmailPort: data?.systemEmailPort,
|
||||||
globalThresholds: {
|
globalThresholds: {
|
||||||
cpu: data?.globalThresholds?.cpu ?? 0,
|
cpu:
|
||||||
memory: data?.globalThresholds?.memory ?? 0,
|
data?.globalThresholds?.cpu && data.globalThresholds.cpu >= 1
|
||||||
disk: data?.globalThresholds?.disk ?? 0,
|
? data.globalThresholds.cpu
|
||||||
temperature: data?.globalThresholds?.temperature ?? 0,
|
: 80,
|
||||||
|
memory:
|
||||||
|
data?.globalThresholds?.memory && data.globalThresholds.memory >= 1
|
||||||
|
? data.globalThresholds.memory
|
||||||
|
: 80,
|
||||||
|
disk:
|
||||||
|
data?.globalThresholds?.disk && data.globalThresholds.disk >= 1
|
||||||
|
? data.globalThresholds.disk
|
||||||
|
: 80,
|
||||||
|
temperature:
|
||||||
|
data?.globalThresholds?.temperature && data.globalThresholds.temperature >= 1
|
||||||
|
? data.globalThresholds.temperature
|
||||||
|
: 80,
|
||||||
},
|
},
|
||||||
checkTTL: data?.checkTTL ?? 30,
|
checkTTL: data?.checkTTL ?? 30,
|
||||||
pagespeedApiKey: "",
|
pagespeedApiKey: "",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type { Monitor } from "@/Types/Monitor";
|
|||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
import type { Check } from "@/Types/Check";
|
import type { Check } from "@/Types/Check";
|
||||||
import type { RootState } from "@/Types/state";
|
import type { RootState } from "@/Types/state";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
@@ -29,7 +28,6 @@ export const ChecksTable = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const getHeaders = (t: Function, uiTimezone: string) => {
|
const getHeaders = (t: Function, uiTimezone: string) => {
|
||||||
const headers: Header<Check>[] = [
|
const headers: Header<Check>[] = [
|
||||||
@@ -109,9 +107,6 @@ export const ChecksTable = ({
|
|||||||
<Table
|
<Table
|
||||||
headers={headers}
|
headers={headers}
|
||||||
data={checks}
|
data={checks}
|
||||||
onRowClick={(row) => {
|
|
||||||
navigate(`/checks/${row.id}`);
|
|
||||||
}}
|
|
||||||
emptyViewText={t("pages.checks.table.empty")}
|
emptyViewText={t("pages.checks.table.empty")}
|
||||||
/>
|
/>
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { logger } from "@/Utils/logger";
|
||||||
import { useParams, useLocation, useNavigate } from "react-router";
|
import { useParams, useLocation, useNavigate } from "react-router";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -15,6 +16,7 @@ import Divider from "@mui/material/Divider";
|
|||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import { Trash2 } from "lucide-react";
|
import { Trash2 } from "lucide-react";
|
||||||
import { HeaderDeleteControls } from "@/Components/monitors";
|
import { HeaderDeleteControls } from "@/Components/monitors";
|
||||||
|
import { GeoContinents } from "@/Types/GeoCheck";
|
||||||
|
|
||||||
import { BasePage, ConfigBox } from "@/Components/design-elements";
|
import { BasePage, ConfigBox } from "@/Components/design-elements";
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +29,7 @@ import {
|
|||||||
SliderWithLabel,
|
SliderWithLabel,
|
||||||
Dialog,
|
Dialog,
|
||||||
} from "@/Components/inputs";
|
} from "@/Components/inputs";
|
||||||
|
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
||||||
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
|
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
|
||||||
import { useMonitorForm } from "@/Hooks/useMonitorForm";
|
import { useMonitorForm } from "@/Hooks/useMonitorForm";
|
||||||
import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
|
import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
|
||||||
@@ -41,6 +44,8 @@ interface GeneralSettingsConfig {
|
|||||||
showPort: boolean;
|
showPort: boolean;
|
||||||
showGameSelect: boolean;
|
showGameSelect: boolean;
|
||||||
showSecret: boolean;
|
showSecret: boolean;
|
||||||
|
showGrpcServiceName: boolean;
|
||||||
|
showIgnoreTls: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getGeneralSettingsConfig = (
|
const getGeneralSettingsConfig = (
|
||||||
@@ -56,6 +61,8 @@ const getGeneralSettingsConfig = (
|
|||||||
showPort: false,
|
showPort: false,
|
||||||
showGameSelect: false,
|
showGameSelect: false,
|
||||||
showSecret: false,
|
showSecret: false,
|
||||||
|
showGrpcServiceName: false,
|
||||||
|
showIgnoreTls: false,
|
||||||
},
|
},
|
||||||
ping: {
|
ping: {
|
||||||
urlLabel: t("pages.createMonitor.form.general.option.host.label"),
|
urlLabel: t("pages.createMonitor.form.general.option.host.label"),
|
||||||
@@ -65,6 +72,8 @@ const getGeneralSettingsConfig = (
|
|||||||
showPort: false,
|
showPort: false,
|
||||||
showGameSelect: false,
|
showGameSelect: false,
|
||||||
showSecret: false,
|
showSecret: false,
|
||||||
|
showGrpcServiceName: false,
|
||||||
|
showIgnoreTls: false,
|
||||||
},
|
},
|
||||||
docker: {
|
docker: {
|
||||||
urlLabel: t("pages.createMonitor.form.general.option.container.label"),
|
urlLabel: t("pages.createMonitor.form.general.option.container.label"),
|
||||||
@@ -74,6 +83,8 @@ const getGeneralSettingsConfig = (
|
|||||||
showPort: false,
|
showPort: false,
|
||||||
showGameSelect: false,
|
showGameSelect: false,
|
||||||
showSecret: false,
|
showSecret: false,
|
||||||
|
showGrpcServiceName: false,
|
||||||
|
showIgnoreTls: false,
|
||||||
},
|
},
|
||||||
port: {
|
port: {
|
||||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||||
@@ -83,6 +94,8 @@ const getGeneralSettingsConfig = (
|
|||||||
showPort: true,
|
showPort: true,
|
||||||
showGameSelect: false,
|
showGameSelect: false,
|
||||||
showSecret: false,
|
showSecret: false,
|
||||||
|
showGrpcServiceName: false,
|
||||||
|
showIgnoreTls: false,
|
||||||
},
|
},
|
||||||
game: {
|
game: {
|
||||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||||
@@ -92,6 +105,19 @@ const getGeneralSettingsConfig = (
|
|||||||
showPort: true,
|
showPort: true,
|
||||||
showGameSelect: true,
|
showGameSelect: true,
|
||||||
showSecret: false,
|
showSecret: false,
|
||||||
|
showGrpcServiceName: false,
|
||||||
|
showIgnoreTls: false,
|
||||||
|
},
|
||||||
|
grpc: {
|
||||||
|
urlLabel: t("pages.createMonitor.form.general.option.host.label"),
|
||||||
|
urlPlaceholder: t("pages.createMonitor.form.general.option.host.placeholder"),
|
||||||
|
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
|
||||||
|
showUrl: true,
|
||||||
|
showPort: true,
|
||||||
|
showGameSelect: false,
|
||||||
|
showSecret: false,
|
||||||
|
showGrpcServiceName: true,
|
||||||
|
showIgnoreTls: true,
|
||||||
},
|
},
|
||||||
pagespeed: {
|
pagespeed: {
|
||||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||||
@@ -101,6 +127,8 @@ const getGeneralSettingsConfig = (
|
|||||||
showPort: false,
|
showPort: false,
|
||||||
showGameSelect: false,
|
showGameSelect: false,
|
||||||
showSecret: false,
|
showSecret: false,
|
||||||
|
showGrpcServiceName: false,
|
||||||
|
showIgnoreTls: false,
|
||||||
},
|
},
|
||||||
hardware: {
|
hardware: {
|
||||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||||
@@ -110,6 +138,8 @@ const getGeneralSettingsConfig = (
|
|||||||
showPort: false,
|
showPort: false,
|
||||||
showGameSelect: false,
|
showGameSelect: false,
|
||||||
showSecret: true,
|
showSecret: true,
|
||||||
|
showGrpcServiceName: false,
|
||||||
|
showIgnoreTls: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return configs[type] || configs.http;
|
return configs[type] || configs.http;
|
||||||
@@ -165,6 +195,7 @@ const CreateMonitorPage = () => {
|
|||||||
const watchedType = watch("type") as MonitorType;
|
const watchedType = watch("type") as MonitorType;
|
||||||
|
|
||||||
const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean;
|
const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean;
|
||||||
|
const watchGeoCheckEnabled = watch("geoCheckEnabled") as boolean;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearErrors();
|
clearErrors();
|
||||||
@@ -224,7 +255,7 @@ const CreateMonitorPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onError = (errors: unknown) => {
|
const onError = (errors: unknown) => {
|
||||||
console.log(errors);
|
logger.debug("Monitor creation validation errors", errors);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -251,7 +282,7 @@ const CreateMonitorPage = () => {
|
|||||||
<FormControl error={!!fieldState.error}>
|
<FormControl error={!!fieldState.error}>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
{...field}
|
{...field}
|
||||||
sx={{ gap: theme.spacing(6) }}
|
sx={{ gap: theme.spacing(LAYOUT.MD) }}
|
||||||
>
|
>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="http"
|
value="http"
|
||||||
@@ -288,6 +319,13 @@ const CreateMonitorPage = () => {
|
|||||||
"pages.createMonitor.form.type.optionGameDescription"
|
"pages.createMonitor.form.type.optionGameDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<RadioWithDescription
|
||||||
|
value="grpc"
|
||||||
|
label={t("pages.createMonitor.form.type.optionGrpc")}
|
||||||
|
description={t(
|
||||||
|
"pages.createMonitor.form.type.optionGrpcDescription"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@@ -300,7 +338,7 @@ const CreateMonitorPage = () => {
|
|||||||
title={t("pages.createMonitor.form.general.title")}
|
title={t("pages.createMonitor.form.general.title")}
|
||||||
subtitle={t(`pages.createMonitor.form.general.description.${watchedType}`)}
|
subtitle={t(`pages.createMonitor.form.general.description.${watchedType}`)}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(8)}>
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
{/* URL/Host/Container field - not shown for hardware */}
|
{/* URL/Host/Container field - not shown for hardware */}
|
||||||
{generalSettingsConfig.showUrl && (
|
{generalSettingsConfig.showUrl && (
|
||||||
<Controller
|
<Controller
|
||||||
@@ -376,6 +414,30 @@ const CreateMonitorPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* gRPC Service Name field - only for grpc type */}
|
||||||
|
{generalSettingsConfig.showGrpcServiceName && (
|
||||||
|
<Controller
|
||||||
|
name="grpcServiceName"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
type="text"
|
||||||
|
fieldLabel={t(
|
||||||
|
"pages.createMonitor.form.general.option.grpcServiceName.label"
|
||||||
|
)}
|
||||||
|
placeholder={t(
|
||||||
|
"pages.createMonitor.form.general.option.grpcServiceName.placeholder"
|
||||||
|
)}
|
||||||
|
fullWidth
|
||||||
|
error={!!fieldState.error}
|
||||||
|
helperText={fieldState.error?.message ?? ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Secret field - only for hardware type */}
|
{/* Secret field - only for hardware type */}
|
||||||
{generalSettingsConfig.showSecret && (
|
{generalSettingsConfig.showSecret && (
|
||||||
<Controller
|
<Controller
|
||||||
@@ -495,7 +557,7 @@ const CreateMonitorPage = () => {
|
|||||||
title={t("pages.createMonitor.form.thresholds.title")}
|
title={t("pages.createMonitor.form.thresholds.title")}
|
||||||
subtitle={t("pages.createMonitor.form.thresholds.description")}
|
subtitle={t("pages.createMonitor.form.thresholds.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(8)}>
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="cpuAlertThreshold"
|
name="cpuAlertThreshold"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -577,7 +639,7 @@ const CreateMonitorPage = () => {
|
|||||||
title={t("pages.createMonitor.form.incidents.title")}
|
title={t("pages.createMonitor.form.incidents.title")}
|
||||||
subtitle={t("pages.createMonitor.form.incidents.description")}
|
subtitle={t("pages.createMonitor.form.incidents.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(12)}>
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="statusWindowSize"
|
name="statusWindowSize"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -629,7 +691,7 @@ const CreateMonitorPage = () => {
|
|||||||
(field.value ?? []).includes(n.id)
|
(field.value ?? []).includes(n.id)
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Stack spacing={theme.spacing(4)}>
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
multiple
|
multiple
|
||||||
options={notificationOptions}
|
options={notificationOptions}
|
||||||
@@ -680,7 +742,7 @@ const CreateMonitorPage = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{watchedType === "http" && (
|
{(watchedType === "http" || watchedType === "grpc") && (
|
||||||
<ConfigBox
|
<ConfigBox
|
||||||
title={t("pages.createMonitor.form.ignoreTls.title")}
|
title={t("pages.createMonitor.form.ignoreTls.title")}
|
||||||
subtitle={t("pages.createMonitor.form.ignoreTls.description")}
|
subtitle={t("pages.createMonitor.form.ignoreTls.description")}
|
||||||
@@ -692,7 +754,7 @@ const CreateMonitorPage = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
spacing={theme.spacing(2)}
|
spacing={theme.spacing(SPACING.LG)}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value ?? false}
|
checked={field.value ?? false}
|
||||||
@@ -708,12 +770,56 @@ const CreateMonitorPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{watchedType === "http" && (
|
||||||
|
<ConfigBox
|
||||||
|
title={t("pages.createMonitor.form.basicAuth.title")}
|
||||||
|
subtitle={t("pages.createMonitor.form.basicAuth.description")}
|
||||||
|
rightContent={
|
||||||
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
|
<Controller
|
||||||
|
name="basicAuthUser"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
fieldLabel={t(
|
||||||
|
"pages.createMonitor.form.basicAuth.option.username.label"
|
||||||
|
)}
|
||||||
|
fullWidth
|
||||||
|
error={!!fieldState.error}
|
||||||
|
helperText={fieldState.error?.message ?? ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="basicAuthPassword"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
type="password"
|
||||||
|
fieldLabel={t(
|
||||||
|
"pages.createMonitor.form.basicAuth.option.password.label"
|
||||||
|
)}
|
||||||
|
fullWidth
|
||||||
|
error={!!fieldState.error}
|
||||||
|
helperText={fieldState.error?.message ?? ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{watchedType === "http" && (
|
{watchedType === "http" && (
|
||||||
<ConfigBox
|
<ConfigBox
|
||||||
title={t("pages.createMonitor.form.advanced.title")}
|
title={t("pages.createMonitor.form.advanced.title")}
|
||||||
subtitle={t("pages.createMonitor.form.advanced.description")}
|
subtitle={t("pages.createMonitor.form.advanced.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(8)}>
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="useAdvancedMatching"
|
name="useAdvancedMatching"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -721,7 +827,7 @@ const CreateMonitorPage = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
spacing={theme.spacing(2)}
|
spacing={theme.spacing(SPACING.LG)}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value ?? false}
|
checked={field.value ?? false}
|
||||||
@@ -736,7 +842,7 @@ const CreateMonitorPage = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{watchedUseAdvancedMatching && (
|
{watchedUseAdvancedMatching && (
|
||||||
<Stack spacing={theme.spacing(8)}>
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="matchMethod"
|
name="matchMethod"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -823,6 +929,140 @@ const CreateMonitorPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{watchedType === "http" && (
|
||||||
|
<ConfigBox
|
||||||
|
title={t("pages.createMonitor.form.geoChecks.title")}
|
||||||
|
subtitle={t("pages.createMonitor.form.geoChecks.description")}
|
||||||
|
rightContent={
|
||||||
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
|
<Controller
|
||||||
|
name="geoCheckEnabled"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={theme.spacing(SPACING.LG)}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
checked={field.value ?? false}
|
||||||
|
onChange={(e) => field.onChange(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<Typography>
|
||||||
|
{t("pages.createMonitor.form.geoChecks.option.enabled.label")}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{watchGeoCheckEnabled && (
|
||||||
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
|
<Controller
|
||||||
|
name="geoCheckLocations"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
// Map continents to have 'name' property for Autocomplete
|
||||||
|
const locationOptions = GeoContinents.map((continent) => ({
|
||||||
|
id: continent,
|
||||||
|
name: t(
|
||||||
|
`pages.createMonitor.form.geoChecks.option.locations.options.${continent}`
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
const selectedLocations = locationOptions.filter((loc) =>
|
||||||
|
(field.value ?? []).includes(loc.id)
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
|
<Autocomplete
|
||||||
|
multiple
|
||||||
|
options={locationOptions}
|
||||||
|
value={selectedLocations}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
onChange={(_: unknown, newValue: typeof locationOptions) => {
|
||||||
|
field.onChange(newValue.map((loc) => loc.id));
|
||||||
|
}}
|
||||||
|
isOptionEqualToValue={(option, value) =>
|
||||||
|
option.id === value.id
|
||||||
|
}
|
||||||
|
fieldLabel={t(
|
||||||
|
"pages.createMonitor.form.geoChecks.option.locations.label"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{selectedLocations.length > 0 && (
|
||||||
|
<Stack
|
||||||
|
flex={1}
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
{selectedLocations.map((location, index) => (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
key={location.id}
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<Typography flexGrow={1}>{location.name}</Typography>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
field.onChange(
|
||||||
|
(field.value ?? []).filter(
|
||||||
|
(id: string) => id !== location.id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
aria-label="Remove location"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</IconButton>
|
||||||
|
{index < selectedLocations.length - 1 && <Divider />}
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="geoCheckInterval"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? 300000}
|
||||||
|
fieldLabel={t(
|
||||||
|
"pages.createMonitor.form.geoChecks.option.interval.label"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MenuItem value={300000}>
|
||||||
|
{t(
|
||||||
|
"pages.createMonitor.form.geoChecks.option.interval.value.fiveMinutes"
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={600000}>
|
||||||
|
{t(
|
||||||
|
"pages.createMonitor.form.geoChecks.option.interval.value.tenMinutes"
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={900000}>
|
||||||
|
{t(
|
||||||
|
"pages.createMonitor.form.geoChecks.option.interval.value.fifteenMinutes"
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={1800000}>
|
||||||
|
{t(
|
||||||
|
"pages.createMonitor.form.geoChecks.option.interval.value.thirtyMinutes"
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
justifyContent="flex-end"
|
justifyContent="flex-end"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Grid from "@mui/material/Grid";
|
|||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { BaseBox, ValueLabel } from "@/Components/design-elements";
|
import { BaseBox, ValueLabel } from "@/Components/design-elements";
|
||||||
|
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { Incident } from "@/Types/Incident";
|
import type { Incident } from "@/Types/Incident";
|
||||||
@@ -29,14 +30,14 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
gap={theme.spacing(4)}
|
gap={theme.spacing(LAYOUT.MD)}
|
||||||
sx={sx}
|
sx={sx}
|
||||||
>
|
>
|
||||||
<Typography textTransform={"uppercase"}>
|
<Typography textTransform={"uppercase"}>
|
||||||
{t("pages.incidents.dialog.details.title")}
|
{t("pages.incidents.dialog.details.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<BaseBox padding={8}>
|
<BaseBox padding={LAYOUT.MD}>
|
||||||
<Stack gap={theme.spacing(4)}>
|
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||||
<Typography textTransform={"uppercase"}>
|
<Typography textTransform={"uppercase"}>
|
||||||
{t("pages.incidents.dialog.details.overview")}
|
{t("pages.incidents.dialog.details.overview")}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -44,7 +45,7 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
|||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
spacing={theme.spacing(4)}
|
spacing={theme.spacing(LAYOUT.MD)}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Grid size={2}>{t("pages.incidents.dialog.details.status")}</Grid>
|
<Grid size={2}>{t("pages.incidents.dialog.details.status")}</Grid>
|
||||||
@@ -75,15 +76,15 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Stack>
|
</Stack>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
<BaseBox padding={8}>
|
<BaseBox padding={LAYOUT.MD}>
|
||||||
<Stack gap={theme.spacing(4)}>
|
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||||
<Typography textTransform={"uppercase"}>
|
<Typography textTransform={"uppercase"}>
|
||||||
{t("pages.incidents.dialog.details.analysis")}
|
{t("pages.incidents.dialog.details.analysis")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
spacing={theme.spacing(2)}
|
spacing={theme.spacing(LAYOUT.MD)}
|
||||||
>
|
>
|
||||||
<Grid size={6}>
|
<Grid size={6}>
|
||||||
<Typography>{t("pages.incidents.dialog.details.timeline")}</Typography>
|
<Typography>{t("pages.incidents.dialog.details.timeline")}</Typography>
|
||||||
@@ -127,15 +128,15 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
{!incident.status && (
|
{!incident.status && (
|
||||||
<BaseBox padding={8}>
|
<BaseBox padding={LAYOUT.MD}>
|
||||||
<Stack gap={theme.spacing(4)}>
|
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
||||||
<Typography textTransform={"uppercase"}>
|
<Typography textTransform={"uppercase"}>
|
||||||
{t("pages.incidents.dialog.details.resolutionDetails")}
|
{t("pages.incidents.dialog.details.resolutionDetails")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
spacing={theme.spacing(2)}
|
spacing={theme.spacing(LAYOUT.MD)}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Grid size={2}>
|
<Grid size={2}>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
const LogsPage = () => {
|
const LogsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [activeTab, setActiveTab] = useState<number>(2);
|
const [activeTab, setActiveTab] = useState<number>(1);
|
||||||
return (
|
return (
|
||||||
<BasePage>
|
<BasePage>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import { logger } from "@/Utils/logger";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
||||||
import { BasePage, ConfigBox } from "@/Components/design-elements";
|
import { BasePage, ConfigBox } from "@/Components/design-elements";
|
||||||
import {
|
import {
|
||||||
TextField,
|
TextField,
|
||||||
@@ -108,7 +110,7 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
const isLoading = isPosting || isPatching;
|
const isLoading = isPosting || isPatching;
|
||||||
|
|
||||||
const onError = (errors: any) => {
|
const onError = (errors: any) => {
|
||||||
console.error("Form submission errors:", errors);
|
logger.error("Maintenance form submission failed", undefined, { errors });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -120,7 +122,7 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
title={t("pages.maintenanceWindow.form.general.title")}
|
title={t("pages.maintenanceWindow.form.general.title")}
|
||||||
subtitle={t("pages.maintenanceWindow.form.general.description")}
|
subtitle={t("pages.maintenanceWindow.form.general.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(8)}>
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -170,7 +172,7 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
title={t("pages.maintenanceWindow.form.startDate.title")}
|
title={t("pages.maintenanceWindow.form.startDate.title")}
|
||||||
subtitle={t("pages.maintenanceWindow.form.startDate.description")}
|
subtitle={t("pages.maintenanceWindow.form.startDate.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(8)}>
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="startDate"
|
name="startDate"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -197,7 +199,7 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
title={t("pages.maintenanceWindow.form.startTime.title")}
|
title={t("pages.maintenanceWindow.form.startTime.title")}
|
||||||
subtitle={t("pages.maintenanceWindow.form.startTime.description")}
|
subtitle={t("pages.maintenanceWindow.form.startTime.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(8)}>
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="startTime"
|
name="startTime"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -218,7 +220,7 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="flex-start"
|
alignItems="flex-start"
|
||||||
spacing={theme.spacing(4)}
|
spacing={theme.spacing(LAYOUT.MD)}
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="duration"
|
name="duration"
|
||||||
@@ -283,7 +285,7 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
.filter((m): m is Monitor => m !== undefined);
|
.filter((m): m is Monitor => m !== undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={theme.spacing(4)}>
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
multiple
|
multiple
|
||||||
options={monitorsList}
|
options={monitorsList}
|
||||||
@@ -317,9 +319,8 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
key={monitor.id}
|
key={monitor.id}
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
spacing={theme.spacing(4)}
|
padding={theme.spacing(LAYOUT.XS)}
|
||||||
padding={theme.spacing(4)}
|
marginTop={theme.spacing(SPACING.LG)}
|
||||||
marginTop={theme.spacing(2)}
|
|
||||||
borderRadius={1}
|
borderRadius={1}
|
||||||
sx={{
|
sx={{
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
@@ -350,7 +351,7 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
justifyContent="flex-end"
|
justifyContent="flex-end"
|
||||||
spacing={theme.spacing(2)}
|
spacing={theme.spacing(SPACING.LG)}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BasePage } from "@/Components/design-elements";
|
import { BasePage } from "@/Components/design-elements";
|
||||||
|
import { HeaderTimeRange } from "@/Components/common";
|
||||||
import type { PageSpeedDetailsResponse } from "@/Types/Monitor";
|
import type { PageSpeedDetailsResponse } from "@/Types/Monitor";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import {
|
import {
|
||||||
@@ -13,23 +14,36 @@ import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
|||||||
import { useGet } from "@/Hooks/UseApi";
|
import { useGet } from "@/Hooks/UseApi";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from "@mui/material";
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
|
||||||
const PageSpeedDetails = () => {
|
const PageSpeedDetails = () => {
|
||||||
const { monitorId } = useParams();
|
const { monitorId } = useParams();
|
||||||
const isAdmin = useIsAdmin();
|
const isAdmin = useIsAdmin();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const [dateRange, setDateRange] = useState<string>("day");
|
||||||
|
|
||||||
|
const monitorDetailsUrl = useMemo(() => {
|
||||||
|
if (!monitorId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("dateRange", dateRange);
|
||||||
|
return `/monitors/pagespeed/details/${monitorId}?${params.toString()}`;
|
||||||
|
}, [monitorId, dateRange]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: monitorData,
|
data: monitorData,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
refetch,
|
refetch,
|
||||||
} = useGet<PageSpeedDetailsResponse>(
|
} = useGet<PageSpeedDetailsResponse>(
|
||||||
monitorId ? `/monitors/pagespeed/details/${monitorId}?dateRange=day` : null,
|
monitorDetailsUrl,
|
||||||
{},
|
{},
|
||||||
{ keepPreviousData: true, refreshInterval: 30000 }
|
{ keepPreviousData: true, refreshInterval: 30000 }
|
||||||
);
|
);
|
||||||
|
|
||||||
const monitor = monitorData?.monitor;
|
const monitor = monitorData?.monitorData?.monitor;
|
||||||
|
const groupedChecks = monitorData?.monitorData?.groupedChecks || [];
|
||||||
const monitorStats = monitorData?.monitorStats || null;
|
const monitorStats = monitorData?.monitorStats || null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,9 +61,15 @@ const PageSpeedDetails = () => {
|
|||||||
monitor={monitor}
|
monitor={monitor}
|
||||||
monitorStats={monitorStats}
|
monitorStats={monitorStats}
|
||||||
/>
|
/>
|
||||||
|
<HeaderTimeRange
|
||||||
|
isLoading={isLoading}
|
||||||
|
hasDateRange={true}
|
||||||
|
dateRange={dateRange}
|
||||||
|
setDateRange={setDateRange}
|
||||||
|
/>
|
||||||
<HistogramPageSpeedDetails
|
<HistogramPageSpeedDetails
|
||||||
checks={monitor?.recentChecks || []}
|
checks={groupedChecks}
|
||||||
range="day"
|
range={dateRange}
|
||||||
/>
|
/>
|
||||||
<Stack
|
<Stack
|
||||||
direction={{ xs: "column", md: "row" }}
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { PageSpeedMonitorsTable } from "@/Pages/PageSpeed/Monitors/Components/Pa
|
|||||||
import type { Monitor } from "@/Types/Monitor";
|
import type { Monitor } from "@/Types/Monitor";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||||
import { useGet, useDelete } from "@/Hooks/UseApi";
|
import { useGet, useDelete } from "@/Hooks/UseApi";
|
||||||
import type { MonitorsWithChecksResponse } from "@/Types/Monitor";
|
import type { MonitorsWithChecksResponse } from "@/Types/Monitor";
|
||||||
@@ -20,14 +20,6 @@ const PageSpeedMonitorsPage = () => {
|
|||||||
const isAdmin = useIsAdmin();
|
const isAdmin = useIsAdmin();
|
||||||
const { deleteFn, loading: isDeleting } = useDelete();
|
const { deleteFn, loading: isDeleting } = useDelete();
|
||||||
|
|
||||||
const monitorsUrl = "/monitors/team/with-checks?type=pagespeed&limit=10";
|
|
||||||
const {
|
|
||||||
data: monitorsData,
|
|
||||||
isLoading: monitorsIsLoading,
|
|
||||||
error: monitorsError,
|
|
||||||
refetch,
|
|
||||||
} = useGet<MonitorsWithChecksResponse>(monitorsUrl, {}, { refreshInterval: 30000 });
|
|
||||||
|
|
||||||
const settingsUrl = "/settings";
|
const settingsUrl = "/settings";
|
||||||
const {
|
const {
|
||||||
data: settingsData,
|
data: settingsData,
|
||||||
@@ -42,6 +34,28 @@ const PageSpeedMonitorsPage = () => {
|
|||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||||
|
|
||||||
|
const monitorsWithChecksUrl = useMemo(() => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("type", "pagespeed");
|
||||||
|
params.append("limit", "25");
|
||||||
|
if (page !== undefined) params.append("page", String(page));
|
||||||
|
if (rowsPerPage) params.append("rowsPerPage", String(rowsPerPage));
|
||||||
|
if (sortField) params.append("field", sortField);
|
||||||
|
if (sortOrder) params.append("order", sortOrder);
|
||||||
|
return `/monitors/team/with-checks?${params.toString()}`;
|
||||||
|
}, [page, rowsPerPage, sortField, sortOrder]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: monitorsData,
|
||||||
|
isLoading: monitorsIsLoading,
|
||||||
|
error: monitorsError,
|
||||||
|
refetch,
|
||||||
|
} = useGet<MonitorsWithChecksResponse>(
|
||||||
|
monitorsWithChecksUrl,
|
||||||
|
{},
|
||||||
|
{ refreshInterval: 30000 }
|
||||||
|
);
|
||||||
|
|
||||||
const monitors = monitorsData?.monitors;
|
const monitors = monitorsData?.monitors;
|
||||||
const monitorsCount = monitorsData?.count ?? 0;
|
const monitorsCount = monitorsData?.count ?? 0;
|
||||||
const summary = monitorsData?.summary ?? null;
|
const summary = monitorsData?.summary ?? null;
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { BasePage, ConfigBox, TextLink } from "@/Components/design-elements";
|
import { BasePage, ConfigBox, TextLink } from "@/Components/design-elements";
|
||||||
import { Autocomplete, Select, Dialog, SwitchComponent } from "@/Components/inputs";
|
import { Autocomplete, Select, Dialog, SwitchComponent } from "@/Components/inputs";
|
||||||
import { Stack, useTheme, MenuItem, Link, type SelectChangeEvent } from "@mui/material";
|
import { logger } from "@/Utils/logger";
|
||||||
|
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
useTheme,
|
||||||
|
MenuItem,
|
||||||
|
Link,
|
||||||
|
Alert,
|
||||||
|
type SelectChangeEvent,
|
||||||
|
} from "@mui/material";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -8,12 +17,13 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import DummyChart from "@/Pages/Settings/DummyChart";
|
import DummyChart from "@/Pages/Settings/DummyChart";
|
||||||
import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi";
|
import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi";
|
||||||
|
import { useToast } from "@/Hooks/UseToast";
|
||||||
import { useSettingsForm } from "@/Hooks/useSettingsForm";
|
import { useSettingsForm } from "@/Hooks/useSettingsForm";
|
||||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||||
import type { SettingsFormData } from "@/Validation/settings";
|
import type { SettingsFormData } from "@/Validation/settings";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
import { TextField, Button, FieldLabel } from "@/Components/inputs";
|
import { TextField, Button, FieldLabel, SliderWithLabel } from "@/Components/inputs";
|
||||||
import { Box, Typography } from "@mui/material";
|
import { Box, Typography } from "@mui/material";
|
||||||
import { useDelete } from "@/Hooks/UseApi";
|
import { useDelete } from "@/Hooks/UseApi";
|
||||||
|
|
||||||
@@ -44,10 +54,13 @@ export const SettingsPage = () => {
|
|||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const isAdmin = useIsAdmin();
|
const isAdmin = useIsAdmin();
|
||||||
|
const { toastError } = useToast();
|
||||||
// Local state for demo monitors dialog
|
// Local state for demo monitors dialog
|
||||||
const [isDemoMonitorsDialogOpen, setIsDemoMonitorsDialogOpen] = useState(false);
|
const [isDemoMonitorsDialogOpen, setIsDemoMonitorsDialogOpen] = useState(false);
|
||||||
const { post: postDemoMonitors, loading: isPostingDemoMonitors } = usePost();
|
const { post: postDemoMonitors, loading: isPostingDemoMonitors } = usePost();
|
||||||
const { deleteFn: deleteAllMonitors, loading: isDeletingAllMonitors } = useDelete();
|
const { deleteFn: deleteAllMonitors, loading: isDeletingAllMonitors } = useDelete();
|
||||||
|
// Import monitors functionality
|
||||||
|
const { post: importMonitors, loading: isImportingMonitors } = usePost();
|
||||||
|
|
||||||
// Fetch settings data from API
|
// Fetch settings data from API
|
||||||
const { data: fetchedSettings } = useGet<SettingsResponse>("/settings");
|
const { data: fetchedSettings } = useGet<SettingsResponse>("/settings");
|
||||||
@@ -202,6 +215,36 @@ export const SettingsPage = () => {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.type !== "application/json") {
|
||||||
|
toastError("Please select a valid JSON file");
|
||||||
|
event.target.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = await file.text();
|
||||||
|
const monitors = JSON.parse(text);
|
||||||
|
|
||||||
|
if (!Array.isArray(monitors)) {
|
||||||
|
toastError("Invalid file format: expected an array of monitors");
|
||||||
|
event.target.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await importMonitors("/monitors/import/json", { monitors });
|
||||||
|
|
||||||
|
event.target.value = "";
|
||||||
|
} catch (error) {
|
||||||
|
toastError("Error parsing JSON file. Please check the file format.");
|
||||||
|
event.target.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: SettingsFormData) => {
|
const onSubmit = async (data: SettingsFormData) => {
|
||||||
// Don't send pagespeedApiKey if it's already set and user hasn't clicked reset
|
// Don't send pagespeedApiKey if it's already set and user hasn't clicked reset
|
||||||
const dataToSend = { ...data };
|
const dataToSend = { ...data };
|
||||||
@@ -223,8 +266,7 @@ export const SettingsPage = () => {
|
|||||||
(acc as any)[typedKey] = Object.entries(value).reduce(
|
(acc as any)[typedKey] = Object.entries(value).reduce(
|
||||||
(nested, [nestedKey, nestedValue]) => ({
|
(nested, [nestedKey, nestedValue]) => ({
|
||||||
...nested,
|
...nested,
|
||||||
[nestedKey]:
|
[nestedKey]: nestedValue === undefined ? "" : nestedValue,
|
||||||
nestedValue === undefined || nestedValue === 0 ? "" : nestedValue,
|
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
@@ -248,7 +290,7 @@ export const SettingsPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onError = (errors: unknown) => {
|
const onError = (errors: unknown) => {
|
||||||
console.log("Form validation errors:", errors);
|
logger.debug("Form validation errors", errors);
|
||||||
};
|
};
|
||||||
|
|
||||||
const languages = Object.keys(i18n.options.resources || {});
|
const languages = Object.keys(i18n.options.resources || {});
|
||||||
@@ -258,7 +300,7 @@ export const SettingsPage = () => {
|
|||||||
component="form"
|
component="form"
|
||||||
onSubmit={form.handleSubmit(onSubmit, onError)}
|
onSubmit={form.handleSubmit(onSubmit, onError)}
|
||||||
>
|
>
|
||||||
<Stack gap={theme.spacing(8)}>
|
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||||
<ConfigBox
|
<ConfigBox
|
||||||
title={t("pages.settings.form.timezone.title")}
|
title={t("pages.settings.form.timezone.title")}
|
||||||
subtitle={t("pages.settings.form.timezone.description")}
|
subtitle={t("pages.settings.form.timezone.description")}
|
||||||
@@ -281,7 +323,7 @@ export const SettingsPage = () => {
|
|||||||
title={t("pages.settings.form.ui.title")}
|
title={t("pages.settings.form.ui.title")}
|
||||||
subtitle={t("pages.settings.form.ui.description")}
|
subtitle={t("pages.settings.form.ui.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack gap={theme.spacing(8)}>
|
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||||
<Select
|
<Select
|
||||||
value={mode}
|
value={mode}
|
||||||
onChange={handleModeChange}
|
onChange={handleModeChange}
|
||||||
@@ -431,24 +473,16 @@ export const SettingsPage = () => {
|
|||||||
name="globalThresholds.cpu"
|
name="globalThresholds.cpu"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
defaultValue={defaults.globalThresholds?.cpu}
|
defaultValue={defaults.globalThresholds?.cpu}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field }) => (
|
||||||
<TextField
|
<SliderWithLabel
|
||||||
{...field}
|
{...field}
|
||||||
value={
|
|
||||||
field.value === undefined || field.value === 0 ? "" : field.value
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
|
||||||
field.onChange(val === "" ? 0 : Number(val));
|
|
||||||
}}
|
|
||||||
fieldLabel={t("pages.settings.form.thresholds.option.cpu.label")}
|
fieldLabel={t("pages.settings.form.thresholds.option.cpu.label")}
|
||||||
type="number"
|
min={1}
|
||||||
inputProps={{ min: 0 }}
|
max={100}
|
||||||
placeholder={t(
|
sliderMaxWidth={{ xs: "100%", md: "50%" }}
|
||||||
"pages.settings.form.thresholds.option.cpu.placeholder"
|
value={field.value || 1}
|
||||||
)}
|
onChange={(_, value) => field.onChange(value)}
|
||||||
error={!!fieldState.error}
|
valueLabelDisplay="auto"
|
||||||
helperText={fieldState.error?.message}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -456,24 +490,16 @@ export const SettingsPage = () => {
|
|||||||
name="globalThresholds.memory"
|
name="globalThresholds.memory"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
defaultValue={defaults.globalThresholds?.memory}
|
defaultValue={defaults.globalThresholds?.memory}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field }) => (
|
||||||
<TextField
|
<SliderWithLabel
|
||||||
{...field}
|
{...field}
|
||||||
value={
|
|
||||||
field.value === undefined || field.value === 0 ? "" : field.value
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
|
||||||
field.onChange(val === "" ? 0 : Number(val));
|
|
||||||
}}
|
|
||||||
fieldLabel={t("pages.settings.form.thresholds.option.memory.label")}
|
fieldLabel={t("pages.settings.form.thresholds.option.memory.label")}
|
||||||
type="number"
|
min={1}
|
||||||
inputProps={{ min: 0 }}
|
max={100}
|
||||||
placeholder={t(
|
sliderMaxWidth={{ xs: "100%", md: "50%" }}
|
||||||
"pages.settings.form.thresholds.option.memory.placeholder"
|
value={field.value || 1}
|
||||||
)}
|
onChange={(_, value) => field.onChange(value)}
|
||||||
error={!!fieldState.error}
|
valueLabelDisplay="auto"
|
||||||
helperText={fieldState.error?.message}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -481,24 +507,16 @@ export const SettingsPage = () => {
|
|||||||
name="globalThresholds.disk"
|
name="globalThresholds.disk"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
defaultValue={defaults.globalThresholds?.disk}
|
defaultValue={defaults.globalThresholds?.disk}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field }) => (
|
||||||
<TextField
|
<SliderWithLabel
|
||||||
{...field}
|
{...field}
|
||||||
value={
|
|
||||||
field.value === undefined || field.value === 0 ? "" : field.value
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
|
||||||
field.onChange(val === "" ? 0 : Number(val));
|
|
||||||
}}
|
|
||||||
fieldLabel={t("pages.settings.form.thresholds.option.disk.label")}
|
fieldLabel={t("pages.settings.form.thresholds.option.disk.label")}
|
||||||
type="number"
|
min={1}
|
||||||
inputProps={{ min: 0 }}
|
max={100}
|
||||||
placeholder={t(
|
sliderMaxWidth={{ xs: "100%", md: "50%" }}
|
||||||
"pages.settings.form.thresholds.option.disk.placeholder"
|
value={field.value || 1}
|
||||||
)}
|
onChange={(_, value) => field.onChange(value)}
|
||||||
error={!!fieldState.error}
|
valueLabelDisplay="auto"
|
||||||
helperText={fieldState.error?.message}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -506,26 +524,18 @@ export const SettingsPage = () => {
|
|||||||
name="globalThresholds.temperature"
|
name="globalThresholds.temperature"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
defaultValue={defaults.globalThresholds?.temperature}
|
defaultValue={defaults.globalThresholds?.temperature}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field }) => (
|
||||||
<TextField
|
<SliderWithLabel
|
||||||
{...field}
|
{...field}
|
||||||
value={
|
|
||||||
field.value === undefined || field.value === 0 ? "" : field.value
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
const val = e.target.value;
|
|
||||||
field.onChange(val === "" ? 0 : Number(val));
|
|
||||||
}}
|
|
||||||
fieldLabel={t(
|
fieldLabel={t(
|
||||||
"pages.settings.form.thresholds.option.temperature.label"
|
"pages.settings.form.thresholds.option.temperature.label"
|
||||||
)}
|
)}
|
||||||
type="number"
|
min={1}
|
||||||
inputProps={{ min: 0 }}
|
max={150}
|
||||||
placeholder={t(
|
sliderMaxWidth={{ xs: "100%", md: "50%" }}
|
||||||
"pages.settings.form.thresholds.option.temperature.placeholder"
|
value={field.value || 1}
|
||||||
)}
|
onChange={(_, value) => field.onChange(value)}
|
||||||
error={!!fieldState.error}
|
valueLabelDisplay="auto"
|
||||||
helperText={fieldState.error?.message}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -541,7 +551,7 @@ export const SettingsPage = () => {
|
|||||||
title={t("pages.settings.form.email.title")}
|
title={t("pages.settings.form.email.title")}
|
||||||
subtitle={t("pages.settings.form.email.description")}
|
subtitle={t("pages.settings.form.email.description")}
|
||||||
leftContent={
|
leftContent={
|
||||||
<Stack gap={theme.spacing(8)}>
|
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||||
<TextLink
|
<TextLink
|
||||||
text={t("pages.settings.form.email.descriptionTransport")}
|
text={t("pages.settings.form.email.descriptionTransport")}
|
||||||
linkText={t("pages.settings.form.email.descriptionTransportLink")}
|
linkText={t("pages.settings.form.email.descriptionTransportLink")}
|
||||||
@@ -589,7 +599,7 @@ export const SettingsPage = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack gap={theme.spacing(10)}>
|
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||||
{/* Email Host */}
|
{/* Email Host */}
|
||||||
<Controller
|
<Controller
|
||||||
name="systemEmailHost"
|
name="systemEmailHost"
|
||||||
@@ -676,7 +686,7 @@ export const SettingsPage = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
gap={theme.spacing(4)}
|
gap={theme.spacing(LAYOUT.XS)}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -751,7 +761,7 @@ export const SettingsPage = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: theme.spacing(4),
|
gap: theme.spacing(LAYOUT.XS),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[
|
{[
|
||||||
@@ -866,17 +876,34 @@ export const SettingsPage = () => {
|
|||||||
{/* Export Monitors - Admin Only */}
|
{/* Export Monitors - Admin Only */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<ConfigBox
|
<ConfigBox
|
||||||
title={t("pages.settings.form.exportMonitors.title")}
|
title={t("pages.settings.form.importExportMonitors.title")}
|
||||||
subtitle={t("pages.settings.form.exportMonitors.description")}
|
subtitle={t("pages.settings.form.importExportMonitors.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Box>
|
<Stack
|
||||||
|
gap={theme.spacing(LAYOUT.MD)}
|
||||||
|
direction={"row"}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="monitor-import-input"
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => document.getElementById("monitor-import-input")?.click()}
|
||||||
|
disabled={isImportingMonitors}
|
||||||
|
>
|
||||||
|
{t("common.buttons.importFromJSON")}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleExportMonitors}
|
onClick={handleExportMonitors}
|
||||||
>
|
>
|
||||||
{t("common.buttons.exportToJSON")}
|
{t("common.buttons.exportToJSON")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Stack>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -939,13 +966,46 @@ export const SettingsPage = () => {
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
borderTop: `1px solid ${theme.palette.divider}`,
|
borderTop: `1px solid ${theme.palette.divider}`,
|
||||||
padding: theme.spacing(8),
|
padding: theme.spacing(LAYOUT.MD),
|
||||||
marginLeft: theme.spacing(-8),
|
marginLeft: theme.spacing(-LAYOUT.MD),
|
||||||
marginRight: theme.spacing(-8),
|
marginRight: theme.spacing(-LAYOUT.MD),
|
||||||
marginBottom: theme.spacing(-8),
|
marginBottom: theme.spacing(-LAYOUT.MD),
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Validation Error Display */}
|
||||||
|
{Object.keys(form.formState.errors).length > 0 && (
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
sx={{ mb: 2, flexGrow: 1, mr: 2 }}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{ fontWeight: 600, mb: 1 }}
|
||||||
|
>
|
||||||
|
{t("pages.settings.form.validation.errorMessage")}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
component="ul"
|
||||||
|
sx={{ m: 0, pl: 2 }}
|
||||||
|
>
|
||||||
|
{Object.entries(form.formState.errors).map(([field, error]) => {
|
||||||
|
const message =
|
||||||
|
typeof error === "object" && error?.message
|
||||||
|
? error.message
|
||||||
|
: "Invalid value";
|
||||||
|
return (
|
||||||
|
<li key={field}>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<strong>{field}:</strong> {message}
|
||||||
|
</Typography>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
loading={isSaving}
|
loading={isSaving}
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { BasePage, ConfigBox } from "@/Components/design-elements";
|
import { BasePage, ConfigBox } from "@/Components/design-elements";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
|
import { logger } from "@/Utils/logger";
|
||||||
|
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
@@ -80,7 +82,7 @@ const CreateStatusPage = () => {
|
|||||||
}, [defaults, reset]);
|
}, [defaults, reset]);
|
||||||
|
|
||||||
const onError = (errors: any) => {
|
const onError = (errors: any) => {
|
||||||
console.log(errors);
|
logger.debug("Status page validation errors", errors);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteClick = () => {
|
const handleDeleteClick = () => {
|
||||||
@@ -128,7 +130,7 @@ const CreateStatusPage = () => {
|
|||||||
fd.append("logo", imageResult.data);
|
fd.append("logo", imageResult.data);
|
||||||
URL.revokeObjectURL(data.logo.data);
|
URL.revokeObjectURL(data.logo.data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error fetching logo blob:", e);
|
logger.error("Failed to fetch logo blob", e instanceof Error ? e : undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +167,7 @@ const CreateStatusPage = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
spacing={theme.spacing(2)}
|
spacing={theme.spacing(SPACING.MD)}
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="isPublished"
|
name="isPublished"
|
||||||
@@ -187,7 +189,7 @@ const CreateStatusPage = () => {
|
|||||||
title={t("pages.statusPages.form.basicInfo.title")}
|
title={t("pages.statusPages.form.basicInfo.title")}
|
||||||
subtitle={t("pages.statusPages.form.basicInfo.description")}
|
subtitle={t("pages.statusPages.form.basicInfo.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(6)}>
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="companyName"
|
name="companyName"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -242,7 +244,7 @@ const CreateStatusPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={theme.spacing(4)}>
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
multiple
|
multiple
|
||||||
options={monitors}
|
options={monitors}
|
||||||
@@ -290,9 +292,9 @@ const CreateStatusPage = () => {
|
|||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
spacing={theme.spacing(4)}
|
spacing={theme.spacing(LAYOUT.XS)}
|
||||||
padding={theme.spacing(4)}
|
padding={theme.spacing(LAYOUT.XS)}
|
||||||
marginTop={theme.spacing(2)}
|
marginTop={theme.spacing(SPACING.LG)}
|
||||||
borderRadius={1}
|
borderRadius={1}
|
||||||
sx={{
|
sx={{
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
@@ -366,7 +368,7 @@ const CreateStatusPage = () => {
|
|||||||
title={t("pages.statusPages.form.appearance.title")}
|
title={t("pages.statusPages.form.appearance.title")}
|
||||||
subtitle={t("pages.statusPages.form.appearance.description")}
|
subtitle={t("pages.statusPages.form.appearance.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(8)}>
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
<Stack alignItems={"center"}>
|
<Stack alignItems={"center"}>
|
||||||
<Controller
|
<Controller
|
||||||
name="logo"
|
name="logo"
|
||||||
@@ -407,7 +409,7 @@ const CreateStatusPage = () => {
|
|||||||
title={t("pages.statusPages.form.features.title")}
|
title={t("pages.statusPages.form.features.title")}
|
||||||
subtitle={t("pages.statusPages.form.features.description")}
|
subtitle={t("pages.statusPages.form.features.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(2)}>
|
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="showCharts"
|
name="showCharts"
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const getHeaders = (t: Function, uiTimezone: string) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "date",
|
id: "date",
|
||||||
content: t("pages.checks.table.headers.dateTime"),
|
content: t("common.table.headers.dateTime"),
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
return formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone);
|
return formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { Table, Pagination, StatusLabel } from "@/Components/design-elements";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import type { Header } from "@/Components/design-elements";
|
||||||
|
import type { FlatGeoCheck } from "@/Types/GeoCheck";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||||
|
import type { RootState } from "@/Types/state";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import prettyMilliseconds from "pretty-ms";
|
||||||
|
|
||||||
|
const getHeaders = (t: Function, uiTimezone: string) => {
|
||||||
|
const headers: Header<FlatGeoCheck>[] = [
|
||||||
|
{
|
||||||
|
id: "status",
|
||||||
|
content: t("common.table.headers.status"),
|
||||||
|
render: (row) => {
|
||||||
|
const status = row.status ? "up" : "down";
|
||||||
|
return <StatusLabel status={status} />;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "date",
|
||||||
|
content: t("common.table.headers.dateTime"),
|
||||||
|
render: (row) => {
|
||||||
|
return formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "statusCode",
|
||||||
|
content: t("pages.checks.table.headers.statusCode"),
|
||||||
|
render: (row) => {
|
||||||
|
return row.statusCode || "N/A";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "location",
|
||||||
|
content: t("pages.checks.table.headers.location"),
|
||||||
|
render: (row) => {
|
||||||
|
const location = row.location;
|
||||||
|
if (!location) return "N/A";
|
||||||
|
const { continent, country, city } = location;
|
||||||
|
return `${continent} - ${country}, ${city}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "responseTime",
|
||||||
|
content: t("common.table.headers.responseTime"),
|
||||||
|
render: (row) => {
|
||||||
|
if (!row.timings?.total) return "N/A";
|
||||||
|
return prettyMilliseconds(row.timings.total, { compact: true });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GeoChecksTable = ({
|
||||||
|
geoChecks,
|
||||||
|
count,
|
||||||
|
page,
|
||||||
|
setPage,
|
||||||
|
rowsPerPage,
|
||||||
|
setRowsPerPage,
|
||||||
|
}: {
|
||||||
|
geoChecks: FlatGeoCheck[];
|
||||||
|
count: number;
|
||||||
|
page: number;
|
||||||
|
setPage: (page: number) => void;
|
||||||
|
rowsPerPage: number;
|
||||||
|
setRowsPerPage: (rowsPerPage: number) => void;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
||||||
|
const headers = getHeaders(t, uiTimezone);
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Table
|
||||||
|
headers={headers}
|
||||||
|
data={geoChecks}
|
||||||
|
/>
|
||||||
|
<Pagination
|
||||||
|
component="div"
|
||||||
|
count={count}
|
||||||
|
page={page}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onRowsPerPageChange={handleRowsPerPageChange}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,9 +6,12 @@ import {
|
|||||||
RadialAvgResponse,
|
RadialAvgResponse,
|
||||||
HistogramDetails,
|
HistogramDetails,
|
||||||
HeaderMonitorControls,
|
HeaderMonitorControls,
|
||||||
|
HeaderGeoTabs,
|
||||||
|
GeoChecksMap,
|
||||||
} from "@/Components/monitors";
|
} from "@/Components/monitors";
|
||||||
import { TrendingUp, AlertTriangle } from "lucide-react";
|
import { TrendingUp, AlertTriangle } from "lucide-react";
|
||||||
import { ChecksTable } from "@/Pages/Uptime/Details/Components/ChecksTable";
|
import { ChecksTable } from "@/Pages/Uptime/Details/Components/ChecksTable";
|
||||||
|
import { GeoChecksTable } from "@/Pages/Uptime/Details/Components/GeoChecksTable";
|
||||||
import { MonitorStatBoxes } from "@/Components/monitors";
|
import { MonitorStatBoxes } from "@/Components/monitors";
|
||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
@@ -19,9 +22,15 @@ import { useSelector } from "react-redux";
|
|||||||
import { useGet } from "@/Hooks/UseApi";
|
import { useGet } from "@/Hooks/UseApi";
|
||||||
import type { MonitorDetailsResponse } from "@/Types/Monitor";
|
import type { MonitorDetailsResponse } from "@/Types/Monitor";
|
||||||
import type { ChecksResponse } from "@/Types/Check";
|
import type { ChecksResponse } from "@/Types/Check";
|
||||||
|
import type {
|
||||||
|
GeoChecksResult,
|
||||||
|
FlatGeoChecksResponse,
|
||||||
|
GeoContinent,
|
||||||
|
} from "@/Types/GeoCheck";
|
||||||
import type { RootState } from "@/Types/state";
|
import type { RootState } from "@/Types/state";
|
||||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
import { Typography } from "@mui/material";
|
||||||
|
|
||||||
const certificateDateFormat = "MMM D, YYYY h A";
|
const certificateDateFormat = "MMM D, YYYY h A";
|
||||||
|
|
||||||
@@ -37,7 +46,10 @@ const UptimeDetailsPage = () => {
|
|||||||
|
|
||||||
const [page, setPage] = useState<number>(0);
|
const [page, setPage] = useState<number>(0);
|
||||||
const [rowsPerPage, setRowsPerPage] = useState<number>(5);
|
const [rowsPerPage, setRowsPerPage] = useState<number>(5);
|
||||||
|
const [geoPage, setGeoPage] = useState<number>(0);
|
||||||
|
const [geoRowsPerPage, setGeoRowsPerPage] = useState<number>(5);
|
||||||
const [dateRange, setDateRange] = useState<string>("recent");
|
const [dateRange, setDateRange] = useState<string>("recent");
|
||||||
|
const [selectedLocation, setSelectedLocation] = useState<GeoContinent>("NA");
|
||||||
|
|
||||||
const monitorDetailsUrl = useMemo(() => {
|
const monitorDetailsUrl = useMemo(() => {
|
||||||
if (!monitorId) {
|
if (!monitorId) {
|
||||||
@@ -56,7 +68,7 @@ const UptimeDetailsPage = () => {
|
|||||||
} = useGet<MonitorDetailsResponse>(
|
} = useGet<MonitorDetailsResponse>(
|
||||||
monitorDetailsUrl,
|
monitorDetailsUrl,
|
||||||
{},
|
{},
|
||||||
{ refreshInterval: 10000, keepPreviousData: true }
|
{ refreshInterval: 10000, keepPreviousData: true, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const monitorData = monitorDetailsData?.monitorData;
|
const monitorData = monitorDetailsData?.monitorData;
|
||||||
@@ -71,7 +83,11 @@ const UptimeDetailsPage = () => {
|
|||||||
return `/monitors/certificate/${monitorId}`;
|
return `/monitors/certificate/${monitorId}`;
|
||||||
}, [monitorId, monitor?.type]);
|
}, [monitorId, monitor?.type]);
|
||||||
|
|
||||||
const { data: certificateData } = useGet<CertificateResponse>(certificateUrl);
|
const { data: certificateData } = useGet<CertificateResponse>(
|
||||||
|
certificateUrl,
|
||||||
|
{},
|
||||||
|
{ revalidateOnFocus: false }
|
||||||
|
);
|
||||||
|
|
||||||
const certificateExpiry = useMemo(() => {
|
const certificateExpiry = useMemo(() => {
|
||||||
if (!certificateData?.certificateDate) {
|
if (!certificateData?.certificateDate) {
|
||||||
@@ -102,9 +118,58 @@ const UptimeDetailsPage = () => {
|
|||||||
const { data: checksData, isLoading: checksIsLoading } = useGet<ChecksResponse>(
|
const { data: checksData, isLoading: checksIsLoading } = useGet<ChecksResponse>(
|
||||||
checksUrl,
|
checksUrl,
|
||||||
{},
|
{},
|
||||||
{ keepPreviousData: true }
|
{ keepPreviousData: true, revalidateOnFocus: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const geoChecksUrl = useMemo(() => {
|
||||||
|
if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("dateRange", dateRange);
|
||||||
|
params.append("continent", selectedLocation);
|
||||||
|
return `/monitors/${monitorId}/geo-checks?${params.toString()}`;
|
||||||
|
}, [monitorId, monitor?.type, monitor?.geoCheckEnabled, dateRange, selectedLocation]);
|
||||||
|
|
||||||
|
const { data: geoGroupedData } = useGet<GeoChecksResult>(
|
||||||
|
geoChecksUrl,
|
||||||
|
{},
|
||||||
|
{ keepPreviousData: true, revalidateOnFocus: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const geoGroupedChecks = geoGroupedData?.groupedGeoChecks ?? [];
|
||||||
|
|
||||||
|
// Fetch paginated geo checks for the table
|
||||||
|
const geoChecksTableUrl = useMemo(() => {
|
||||||
|
if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("sortOrder", "desc");
|
||||||
|
params.append("dateRange", dateRange);
|
||||||
|
params.append("page", String(geoPage));
|
||||||
|
params.append("rowsPerPage", String(geoRowsPerPage));
|
||||||
|
return `/geo-checks/${monitorId}?${params.toString()}`;
|
||||||
|
}, [
|
||||||
|
monitorId,
|
||||||
|
monitor?.type,
|
||||||
|
monitor?.geoCheckEnabled,
|
||||||
|
dateRange,
|
||||||
|
geoPage,
|
||||||
|
geoRowsPerPage,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { data: geoChecksTableData } = useGet<FlatGeoChecksResponse>(
|
||||||
|
geoChecksTableUrl,
|
||||||
|
{},
|
||||||
|
{ keepPreviousData: true, revalidateOnFocus: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const geoChecksForTable = geoChecksTableData?.geoChecks ?? [];
|
||||||
|
const geoChecksCount = geoChecksTableData?.geoChecksCount ?? 0;
|
||||||
|
|
||||||
|
const geoLocations = monitor?.geoCheckLocations;
|
||||||
|
|
||||||
const checks = checksData?.checks ?? [];
|
const checks = checksData?.checks ?? [];
|
||||||
const checksCount = checksData?.checksCount ?? 0;
|
const checksCount = checksData?.checksCount ?? 0;
|
||||||
|
|
||||||
@@ -127,6 +192,7 @@ const UptimeDetailsPage = () => {
|
|||||||
dateRange={dateRange}
|
dateRange={dateRange}
|
||||||
setDateRange={setDateRange}
|
setDateRange={setDateRange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
direction={{ xs: "column", md: "row" }}
|
direction={{ xs: "column", md: "row" }}
|
||||||
gap={theme.spacing(8)}
|
gap={theme.spacing(8)}
|
||||||
@@ -160,6 +226,31 @@ const UptimeDetailsPage = () => {
|
|||||||
rowsPerPage={rowsPerPage}
|
rowsPerPage={rowsPerPage}
|
||||||
setRowsPerPage={setRowsPerPage}
|
setRowsPerPage={setRowsPerPage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{monitor?.geoCheckEnabled && (
|
||||||
|
<>
|
||||||
|
<Typography variant="h1">Location breakdown</Typography>
|
||||||
|
<HeaderGeoTabs
|
||||||
|
geoCheckEnabled={monitor?.geoCheckEnabled ?? false}
|
||||||
|
locations={geoLocations}
|
||||||
|
selectedLocation={selectedLocation}
|
||||||
|
onLocationChange={setSelectedLocation}
|
||||||
|
/>
|
||||||
|
<HistogramDetails
|
||||||
|
checks={geoGroupedChecks}
|
||||||
|
range={dateRange}
|
||||||
|
/>
|
||||||
|
<GeoChecksTable
|
||||||
|
geoChecks={geoChecksForTable}
|
||||||
|
count={geoChecksCount}
|
||||||
|
page={geoPage}
|
||||||
|
setPage={setGeoPage}
|
||||||
|
rowsPerPage={geoRowsPerPage}
|
||||||
|
setRowsPerPage={setGeoRowsPerPage}
|
||||||
|
/>
|
||||||
|
<GeoChecksMap geoChecks={geoChecksForTable} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</BasePage>
|
</BasePage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ export const MonitorTable = ({
|
|||||||
onClick={(e) => handleSort(e, "status")}
|
onClick={(e) => handleSort(e, "status")}
|
||||||
sx={{ cursor: "pointer" }}
|
sx={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
|
<Box width={theme.spacing(8)} />
|
||||||
{t("common.table.headers.status")}
|
{t("common.table.headers.status")}
|
||||||
{renderSortIcon(sortField === "status")}
|
{renderSortIcon(sortField === "status")}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -211,6 +212,7 @@ export const MonitorTable = ({
|
|||||||
onClick={(e) => handleSort(e, "type")}
|
onClick={(e) => handleSort(e, "type")}
|
||||||
sx={{ cursor: "pointer" }}
|
sx={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
|
<Box width={theme.spacing(8)} />
|
||||||
{t("common.table.headers.type")}
|
{t("common.table.headers.type")}
|
||||||
{renderSortIcon(sortField === "type")}
|
{renderSortIcon(sortField === "type")}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ const UptimeMonitorsPage = () => {
|
|||||||
const debouncedSearch = useDebounce<string>(search, 300);
|
const debouncedSearch = useDebounce<string>(search, 300);
|
||||||
|
|
||||||
// Convert filter selections to API filter values
|
// Convert filter selections to API filter values
|
||||||
// Status: "up" -> true, "down" -> false
|
// Status: pass "up"/"down" directly to the API
|
||||||
// State: "active" -> true, "paused" -> false
|
// State: "active" -> true, "paused" -> false
|
||||||
const toFilterStatus = useMemo(() => {
|
const toFilterStatus = useMemo(() => {
|
||||||
if (selectedStatus === "up") return "true";
|
if (selectedStatus === "up") return "up";
|
||||||
if (selectedStatus === "down") return "false";
|
if (selectedStatus === "down") return "down";
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [selectedStatus]);
|
}, [selectedStatus]);
|
||||||
|
|
||||||
@@ -68,7 +68,9 @@ const UptimeMonitorsPage = () => {
|
|||||||
|
|
||||||
// Default to all types when none selected
|
// Default to all types when none selected
|
||||||
const effectiveTypes =
|
const effectiveTypes =
|
||||||
selectedTypes.length > 0 ? selectedTypes : ["http", "ping", "docker", "port", "game"];
|
selectedTypes.length > 0
|
||||||
|
? selectedTypes
|
||||||
|
: ["http", "ping", "docker", "port", "game", "grpc"];
|
||||||
|
|
||||||
// Build URL for monitors with checks
|
// Build URL for monitors with checks
|
||||||
const monitorsWithChecksUrl = useMemo(() => {
|
const monitorsWithChecksUrl = useMemo(() => {
|
||||||
|
|||||||
@@ -151,6 +151,15 @@ export interface GroupedCheck {
|
|||||||
totalChecks: number;
|
totalChecks: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PageSpeedGroupedCheck {
|
||||||
|
bucketDate: string;
|
||||||
|
performance: number;
|
||||||
|
accessibility: number;
|
||||||
|
bestPractices: number;
|
||||||
|
seo: number;
|
||||||
|
totalChecks: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LatestCheck {
|
export interface LatestCheck {
|
||||||
status: boolean;
|
status: boolean;
|
||||||
responseTime: number;
|
responseTime: number;
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
export const GeoContinents = ["EU", "NA", "AS", "SA", "AF", "OC"] as const;
|
||||||
|
export type GeoContinent = (typeof GeoContinents)[number];
|
||||||
|
|
||||||
|
export interface GeoCheckMetadata {
|
||||||
|
monitorId: string;
|
||||||
|
teamId: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeoCheckTimings {
|
||||||
|
total: number;
|
||||||
|
dns: number;
|
||||||
|
tcp: number;
|
||||||
|
tls: number;
|
||||||
|
firstByte: number;
|
||||||
|
download: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeoCheckLocation {
|
||||||
|
continent: GeoContinent;
|
||||||
|
region: string;
|
||||||
|
country: string;
|
||||||
|
state: string;
|
||||||
|
city: string;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeoCheckResult {
|
||||||
|
location: GeoCheckLocation;
|
||||||
|
status: boolean;
|
||||||
|
statusCode: number;
|
||||||
|
timings: GeoCheckTimings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeoCheck {
|
||||||
|
id: string;
|
||||||
|
metadata: GeoCheckMetadata;
|
||||||
|
results: GeoCheckResult[];
|
||||||
|
expiry: string;
|
||||||
|
__v: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlatGeoCheck {
|
||||||
|
id: string;
|
||||||
|
monitorId: string;
|
||||||
|
teamId: string;
|
||||||
|
type: string;
|
||||||
|
location: GeoCheckLocation;
|
||||||
|
status: boolean;
|
||||||
|
statusCode: number;
|
||||||
|
timings: GeoCheckTimings;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupedGeoCheck {
|
||||||
|
bucketDate: string;
|
||||||
|
continent: GeoContinent;
|
||||||
|
avgResponseTime: number;
|
||||||
|
totalChecks: number;
|
||||||
|
uptimePercentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeoChecksResult {
|
||||||
|
monitorType: string;
|
||||||
|
groupedGeoChecks: GroupedGeoCheck[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeoChecksResponse {
|
||||||
|
geoChecks: GeoCheck[];
|
||||||
|
geoChecksCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlatGeoChecksResponse {
|
||||||
|
geoChecks: FlatGeoCheck[];
|
||||||
|
geoChecksCount: number;
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import type { GroupedCheck, CheckSnapshot } from "@/Types/Check";
|
import type { GroupedCheck, CheckSnapshot } from "@/Types/Check";
|
||||||
|
import type { PageSpeedGroupedCheck } from "@/Types/Check";
|
||||||
|
import type { GeoContinent } from "@/Types/GeoCheck";
|
||||||
|
export type { GeoContinent } from "@/Types/GeoCheck";
|
||||||
|
|
||||||
export const MonitorTypes = [
|
export const MonitorTypes = [
|
||||||
"http",
|
"http",
|
||||||
@@ -8,6 +11,7 @@ export const MonitorTypes = [
|
|||||||
"docker",
|
"docker",
|
||||||
"port",
|
"port",
|
||||||
"game",
|
"game",
|
||||||
|
"grpc",
|
||||||
"unknown",
|
"unknown",
|
||||||
] as const;
|
] as const;
|
||||||
export type MonitorType = (typeof MonitorTypes)[number];
|
export type MonitorType = (typeof MonitorTypes)[number];
|
||||||
@@ -40,6 +44,8 @@ export interface Monitor {
|
|||||||
jsonPath?: string;
|
jsonPath?: string;
|
||||||
expectedValue?: string;
|
expectedValue?: string;
|
||||||
matchMethod?: MonitorMatchMethod;
|
matchMethod?: MonitorMatchMethod;
|
||||||
|
basicAuthUser?: string;
|
||||||
|
basicAuthPassword?: string;
|
||||||
url: string;
|
url: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -57,7 +63,11 @@ export interface Monitor {
|
|||||||
tempAlertCounter: number;
|
tempAlertCounter: number;
|
||||||
selectedDisks: string[];
|
selectedDisks: string[];
|
||||||
gameId?: string;
|
gameId?: string;
|
||||||
|
grpcServiceName?: string;
|
||||||
group: string | null;
|
group: string | null;
|
||||||
|
geoCheckEnabled?: boolean;
|
||||||
|
geoCheckLocations?: GeoContinent[];
|
||||||
|
geoCheckInterval?: number;
|
||||||
recentChecks: CheckSnapshot[];
|
recentChecks: CheckSnapshot[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -112,7 +122,10 @@ export interface MonitorDetailsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PageSpeedDetailsResponse {
|
export interface PageSpeedDetailsResponse {
|
||||||
monitor: MonitorWithChecks;
|
monitorData: {
|
||||||
|
monitor: Monitor;
|
||||||
|
groupedChecks: PageSpeedGroupedCheck[];
|
||||||
|
};
|
||||||
monitorStats: MonitorStats | null;
|
monitorStats: MonitorStats | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+6
@@ -4,6 +4,7 @@ declare const __APP_VERSION__: string;
|
|||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_APP_API_V2_BASE_URL?: string;
|
readonly VITE_APP_API_V2_BASE_URL?: string;
|
||||||
|
readonly VITE_APP_LOG_LEVEL?: "debug" | "info" | "warn" | "error";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
@@ -17,3 +18,8 @@ declare module "*.svg?react" {
|
|||||||
>;
|
>;
|
||||||
export default ReactComponent;
|
export default ReactComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "*.css" {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ export const getMonitorPath = (type: MonitorType): string => {
|
|||||||
port: "uptime",
|
port: "uptime",
|
||||||
ping: "uptime",
|
ping: "uptime",
|
||||||
game: "uptime",
|
game: "uptime",
|
||||||
|
grpc: "uptime",
|
||||||
unknown: "uptime",
|
unknown: "uptime",
|
||||||
docker: "uptime",
|
docker: "uptime",
|
||||||
hardware: "infrastructure",
|
hardware: "infrastructure",
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export const SPACING = {
|
||||||
|
XS: 0.5,
|
||||||
|
SM: 1,
|
||||||
|
MD: 1.5,
|
||||||
|
LG: 2,
|
||||||
|
XL: 2.5,
|
||||||
|
XXL: 3,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const LAYOUT = {
|
||||||
|
XS: 4,
|
||||||
|
SM: 6,
|
||||||
|
MD: 8,
|
||||||
|
LG: 10,
|
||||||
|
XL: 12,
|
||||||
|
XXL: 16,
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import type { LogLevel } from "@/Types/Log";
|
||||||
|
|
||||||
|
interface LogContext {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
||||||
|
debug: 0,
|
||||||
|
info: 1,
|
||||||
|
warn: 2,
|
||||||
|
error: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ErrorLogData {
|
||||||
|
timestamp: string;
|
||||||
|
level: LogLevel;
|
||||||
|
message: string;
|
||||||
|
error?: {
|
||||||
|
message: string;
|
||||||
|
name: string;
|
||||||
|
stack?: string;
|
||||||
|
};
|
||||||
|
context?: LogContext;
|
||||||
|
url: string;
|
||||||
|
userAgent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuredLevel: LogLevel = import.meta.env.VITE_APP_LOG_LEVEL || "error";
|
||||||
|
const configuredPriority = LOG_LEVEL_PRIORITY[configuredLevel];
|
||||||
|
|
||||||
|
const shouldLog = (level: LogLevel): boolean => {
|
||||||
|
return LOG_LEVEL_PRIORITY[level] >= configuredPriority;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatError = (error?: Error) => {
|
||||||
|
if (!error) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: error.message,
|
||||||
|
name: error.name,
|
||||||
|
stack: error.stack,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createLogData = (
|
||||||
|
level: LogLevel,
|
||||||
|
message: string,
|
||||||
|
error?: Error,
|
||||||
|
context?: LogContext
|
||||||
|
): ErrorLogData => {
|
||||||
|
return {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
error: formatError(error),
|
||||||
|
context,
|
||||||
|
url: window.location.href,
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = (message: string, error?: Error, context?: LogContext): void => {
|
||||||
|
if (!shouldLog("error")) return;
|
||||||
|
|
||||||
|
const logData = createLogData("error", message, error, context);
|
||||||
|
|
||||||
|
if (configuredLevel === "debug") {
|
||||||
|
console.group(`ERROR: ${message}`);
|
||||||
|
if (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
}
|
||||||
|
if (context && Object.keys(context).length > 0) {
|
||||||
|
console.log("Context:", context);
|
||||||
|
}
|
||||||
|
console.log("URL:", logData.url);
|
||||||
|
console.log("Timestamp:", logData.timestamp);
|
||||||
|
console.groupEnd();
|
||||||
|
} else {
|
||||||
|
console.error(JSON.stringify(logData));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const warn = (message: string, context?: LogContext): void => {
|
||||||
|
if (!shouldLog("warn")) return;
|
||||||
|
|
||||||
|
const logData = createLogData("warn", message, undefined, context);
|
||||||
|
|
||||||
|
if (configuredLevel === "debug") {
|
||||||
|
console.group(`WARN: ${message}`);
|
||||||
|
if (context && Object.keys(context).length > 0) {
|
||||||
|
console.log("Context:", context);
|
||||||
|
}
|
||||||
|
console.groupEnd();
|
||||||
|
} else {
|
||||||
|
console.warn(JSON.stringify(logData));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const debug = (message: string, data?: any): void => {
|
||||||
|
if (!shouldLog("debug")) return;
|
||||||
|
|
||||||
|
console.group(`DEBUG: ${message}`);
|
||||||
|
if (data !== undefined) {
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
|
console.groupEnd();
|
||||||
|
};
|
||||||
|
|
||||||
|
const info = (message: string, data?: any): void => {
|
||||||
|
if (!shouldLog("info")) return;
|
||||||
|
|
||||||
|
if (configuredLevel === "debug") {
|
||||||
|
console.log(`INFO: ${message}`, data);
|
||||||
|
} else {
|
||||||
|
const logData = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level: "info" as LogLevel,
|
||||||
|
message,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
console.log(JSON.stringify(logData));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ILogger {
|
||||||
|
error(message: string, error?: Error, context?: LogContext): void;
|
||||||
|
warn(message: string, context?: LogContext): void;
|
||||||
|
debug(message: string, data?: any): void;
|
||||||
|
info(message: string, data?: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logger: ILogger = {
|
||||||
|
error,
|
||||||
|
warn,
|
||||||
|
debug,
|
||||||
|
info,
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { GeoContinents } from "@/Types/GeoCheck";
|
||||||
|
|
||||||
// URL schema with custom error message
|
// URL schema with custom error message
|
||||||
const urlSchema = z.url({ message: "Please enter a valid URL" });
|
const urlSchema = z.url({ message: "Please enter a valid URL" });
|
||||||
@@ -20,6 +21,12 @@ const baseSchema = z.object({
|
|||||||
.number({ message: "Threshold percentage is required" })
|
.number({ message: "Threshold percentage is required" })
|
||||||
.min(1, "Incident percentage must be at least 1")
|
.min(1, "Incident percentage must be at least 1")
|
||||||
.max(100, "Incident percentage must be at most 100"),
|
.max(100, "Incident percentage must be at most 100"),
|
||||||
|
geoCheckEnabled: z.boolean().optional(),
|
||||||
|
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
|
||||||
|
geoCheckInterval: z
|
||||||
|
.number()
|
||||||
|
.min(300000, "Interval must be at least 5 minutes")
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// HTTP monitor schema
|
// HTTP monitor schema
|
||||||
@@ -31,6 +38,8 @@ const httpSchema = baseSchema.extend({
|
|||||||
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
|
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
|
||||||
expectedValue: z.string().optional(),
|
expectedValue: z.string().optional(),
|
||||||
jsonPath: z.string().optional(),
|
jsonPath: z.string().optional(),
|
||||||
|
basicAuthUser: z.string().optional(),
|
||||||
|
basicAuthPassword: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ping monitor schema
|
// Ping monitor schema
|
||||||
@@ -66,6 +75,18 @@ const gameSchema = baseSchema.extend({
|
|||||||
gameId: z.string().min(1, "Game type is required"),
|
gameId: z.string().min(1, "Game type is required"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// gRPC monitor schema
|
||||||
|
const grpcSchema = baseSchema.extend({
|
||||||
|
type: z.literal("grpc"),
|
||||||
|
url: z.string().min(1, "Host is required"),
|
||||||
|
port: z
|
||||||
|
.number()
|
||||||
|
.min(1, "Port must be at least 1")
|
||||||
|
.max(65535, "Port must be at most 65535"),
|
||||||
|
grpcServiceName: z.string().optional(),
|
||||||
|
ignoreTlsErrors: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
// PageSpeed monitor schema
|
// PageSpeed monitor schema
|
||||||
const pagespeedSchema = baseSchema.extend({
|
const pagespeedSchema = baseSchema.extend({
|
||||||
type: z.literal("pagespeed"),
|
type: z.literal("pagespeed"),
|
||||||
@@ -103,6 +124,7 @@ export const monitorSchema = z.discriminatedUnion("type", [
|
|||||||
portSchema,
|
portSchema,
|
||||||
dockerSchema,
|
dockerSchema,
|
||||||
gameSchema,
|
gameSchema,
|
||||||
|
grpcSchema,
|
||||||
pagespeedSchema,
|
pagespeedSchema,
|
||||||
hardwareSchema,
|
hardwareSchema,
|
||||||
]);
|
]);
|
||||||
@@ -116,6 +138,7 @@ export {
|
|||||||
portSchema,
|
portSchema,
|
||||||
dockerSchema,
|
dockerSchema,
|
||||||
gameSchema,
|
gameSchema,
|
||||||
|
grpcSchema,
|
||||||
pagespeedSchema,
|
pagespeedSchema,
|
||||||
hardwareSchema,
|
hardwareSchema,
|
||||||
};
|
};
|
||||||
@@ -40,34 +40,12 @@ export const settingsSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
globalThresholds: z
|
globalThresholds: z.object({
|
||||||
.object({
|
cpu: z.number().int().min(1).max(100),
|
||||||
cpu: z
|
memory: z.number().int().min(1).max(100),
|
||||||
.number()
|
disk: z.number().int().min(1).max(100),
|
||||||
.int()
|
temperature: z.number().int().min(1).max(150),
|
||||||
.min(1, "Please enter a valid value")
|
}),
|
||||||
.max(100, "Maximum value is 100")
|
|
||||||
.optional(),
|
|
||||||
memory: z
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(1, "Please enter a valid value")
|
|
||||||
.max(100, "Maximum value is 100")
|
|
||||||
.optional(),
|
|
||||||
disk: z
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(1, "Please enter a valid value")
|
|
||||||
.max(100, "Maximum value is 100")
|
|
||||||
.optional(),
|
|
||||||
temperature: z
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(1, "Please enter a valid value")
|
|
||||||
.max(150, "Maximum value is 150")
|
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SettingsFormData = z.infer<typeof settingsSchema>;
|
export type SettingsFormData = z.infer<typeof settingsSchema>;
|
||||||
@@ -44,7 +44,9 @@
|
|||||||
"addDemo": "Add demo monitors",
|
"addDemo": "Add demo monitors",
|
||||||
"removeMonitors": "Remove monitors",
|
"removeMonitors": "Remove monitors",
|
||||||
"sendTestEmail": "Send test email",
|
"sendTestEmail": "Send test email",
|
||||||
"exportToJSON": "Export to JSON"
|
"exportToJSON": "Export to JSON",
|
||||||
|
"importFromJSON": "Import from JSON",
|
||||||
|
"clearFilters": "Clear filters"
|
||||||
},
|
},
|
||||||
"charts": {
|
"charts": {
|
||||||
"labels": {
|
"labels": {
|
||||||
@@ -114,7 +116,8 @@
|
|||||||
"type": "Type",
|
"type": "Type",
|
||||||
"url": "Url",
|
"url": "Url",
|
||||||
"interval": "Interval",
|
"interval": "Interval",
|
||||||
"active": "Active"
|
"active": "Active",
|
||||||
|
"responseTime": "Response time"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -371,8 +374,8 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"empty": "No down checks in this time range",
|
"empty": "No down checks in this time range",
|
||||||
"headers": {
|
"headers": {
|
||||||
"dateTime": "Date & time",
|
"statusCode": "Status code",
|
||||||
"statusCode": "Status code"
|
"location": "Location"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -480,6 +483,10 @@
|
|||||||
"port": {
|
"port": {
|
||||||
"label": "Port to monitor",
|
"label": "Port to monitor",
|
||||||
"placeholder": 80
|
"placeholder": 80
|
||||||
|
},
|
||||||
|
"grpcServiceName": {
|
||||||
|
"label": "Service name",
|
||||||
|
"placeholder": "e.g. my.service.v1 (leave empty for overall health)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "General settings",
|
"title": "General settings",
|
||||||
@@ -490,9 +497,22 @@
|
|||||||
"docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).",
|
"docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).",
|
||||||
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.",
|
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.",
|
||||||
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
|
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
|
||||||
|
"grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.",
|
||||||
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure."
|
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"basicAuth": {
|
||||||
|
"description": "Configure HTTP basic authentication credentials for this monitor.",
|
||||||
|
"option": {
|
||||||
|
"username": {
|
||||||
|
"label": "Username"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"label": "Password"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Basic authentication"
|
||||||
|
},
|
||||||
"ignoreTls": {
|
"ignoreTls": {
|
||||||
"description": "Configure TLS/SSL certificate validation for HTTPS connections.",
|
"description": "Configure TLS/SSL certificate validation for HTTPS connections.",
|
||||||
"option": {
|
"option": {
|
||||||
@@ -524,6 +544,8 @@
|
|||||||
"optionDockerDescription": "Use Docker to monitor if a container is running.",
|
"optionDockerDescription": "Use Docker to monitor if a container is running.",
|
||||||
"optionGame": "Game",
|
"optionGame": "Game",
|
||||||
"optionGameDescription": "Monitor if a specific game server is online.",
|
"optionGameDescription": "Monitor if a specific game server is online.",
|
||||||
|
"optionGrpc": "gRPC",
|
||||||
|
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
||||||
"optionHttp": "HTTP(S)",
|
"optionHttp": "HTTP(S)",
|
||||||
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
||||||
"optionPing": "Ping",
|
"optionPing": "Ping",
|
||||||
@@ -550,6 +572,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"geoChecks": {
|
||||||
|
"title": "Geo-Distributed Checks",
|
||||||
|
"description": "Run checks from multiple geographic locations to monitor global availability and performance.",
|
||||||
|
"option": {
|
||||||
|
"enabled": {
|
||||||
|
"label": "Enable geo-distributed checks"
|
||||||
|
},
|
||||||
|
"locations": {
|
||||||
|
"label": "Locations",
|
||||||
|
"placeholder": "Select locations",
|
||||||
|
"options": {
|
||||||
|
"EU": "Europe",
|
||||||
|
"NA": "North America",
|
||||||
|
"AS": "Asia",
|
||||||
|
"SA": "South America",
|
||||||
|
"AF": "Africa",
|
||||||
|
"OC": "Oceania"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interval": {
|
||||||
|
"label": "Check interval",
|
||||||
|
"value": {
|
||||||
|
"fiveMinutes": "5 minutes",
|
||||||
|
"tenMinutes": "10 minutes",
|
||||||
|
"fifteenMinutes": "15 minutes",
|
||||||
|
"thirtyMinutes": "30 minutes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"title": "Monitor IP/URL on Status Page",
|
"title": "Monitor IP/URL on Status Page",
|
||||||
"description": "Display the IP address or URL of monitor on the public Status page. If it's disabled, only the monitor name will be shown to protect sensitive information.",
|
"description": "Display the IP address or URL of monitor on the public Status page. If it's disabled, only the monitor name will be shown to protect sensitive information.",
|
||||||
@@ -619,7 +671,6 @@
|
|||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"allMonitors": "All monitors",
|
"allMonitors": "All monitors",
|
||||||
"clearFilters": "Clear filters",
|
|
||||||
"monitor": "Monitor",
|
"monitor": "Monitor",
|
||||||
"resolutionType": "Resolution type",
|
"resolutionType": "Resolution type",
|
||||||
"resolutionTypes": {
|
"resolutionTypes": {
|
||||||
@@ -1086,12 +1137,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exportMonitors": {
|
"exportMonitors": {
|
||||||
"title": "Export monitors",
|
"title": "Export monitors"
|
||||||
"description": "Export your monitors data as a JSON file for backup or transfer."
|
},
|
||||||
|
"importExportMonitors": {
|
||||||
|
"title": "Import / Export monitors",
|
||||||
|
"description": "Import or export your monitors data as a JSON file for backup or transfer."
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"title": "About",
|
"title": "About",
|
||||||
"developedBy": "Developed by Bluewave Labs"
|
"developedBy": "Developed by Bluewave Labs"
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"errorMessage": "Please fix the following validation errors:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
export default defineConfig(({}) => {
|
export default defineConfig(({}) => {
|
||||||
let version = "3.4.0";
|
let version = "3.5.0";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base: "/",
|
base: "/",
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# ==============================================
|
||||||
|
# Checkmate Server Configuration
|
||||||
|
# ==============================================
|
||||||
|
# Copy this file to .env and fill in your values
|
||||||
|
|
||||||
|
# Server Configuration
|
||||||
|
# --------------------
|
||||||
|
NODE_ENV=development
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# --------
|
||||||
|
DB_CONNECTION_STRING=mongodb://localhost:27017/uptime_db
|
||||||
|
|
||||||
|
# JWT Authentication
|
||||||
|
# ------------------
|
||||||
|
JWT_SECRET=my_secret
|
||||||
|
TOKEN_TTL=99d
|
||||||
|
|
||||||
|
# Client Configuration
|
||||||
|
# --------------------
|
||||||
|
CLIENT_HOST=http://localhost:5173
|
||||||
|
|
||||||
|
# Optional Configuration
|
||||||
|
# ----------------------
|
||||||
|
ORIGIN=localhost
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import pluginJs from "@eslint/js";
|
import pluginJs from "@eslint/js";
|
||||||
import mochaPlugin from "eslint-plugin-mocha";
|
import mochaPlugin from "eslint-plugin-mocha";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Please do not forget to look at the latest eslint configurations and rules.
|
Please do not forget to look at the latest eslint configurations and rules.
|
||||||
@@ -10,6 +11,9 @@ ESlint v9 configuration is different than v8.
|
|||||||
|
|
||||||
/** @type {import('eslint').Linter.Config[]} */
|
/** @type {import('eslint').Linter.Config[]} */
|
||||||
export default [
|
export default [
|
||||||
|
{
|
||||||
|
ignores: ["dist/**", "node_modules/**", "coverage/**", "test/**"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
@@ -21,9 +25,19 @@ export default [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
pluginJs.configs.recommended, // Core JS rules
|
pluginJs.configs.recommended, // Core JS rules
|
||||||
|
...tseslint.configs.recommended, // TypeScript recommended rules
|
||||||
mochaPlugin.configs.flat.recommended, // Mocha rules
|
mochaPlugin.configs.flat.recommended, // Mocha rules
|
||||||
{
|
{
|
||||||
|
files: ["**/*.ts", "**/*.tsx"],
|
||||||
|
languageOptions: {
|
||||||
|
parser: tseslint.parser,
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn", // Warn on 'any' types
|
||||||
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
|
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,46 +432,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/monitors": {
|
"/monitors": {
|
||||||
"get": {
|
|
||||||
"tags": ["monitors"],
|
|
||||||
"summary": "Get all monitors",
|
|
||||||
"description": "Retrieve all monitors for the authenticated user",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Monitors retrieved successfully",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/SuccessResponse"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/Monitor"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"$ref": "#/components/responses/InternalServerError"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearerAuth": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"post": {
|
"post": {
|
||||||
"tags": ["monitors"],
|
"tags": ["monitors"],
|
||||||
"summary": "Create monitor",
|
"summary": "Create monitor",
|
||||||
|
|||||||
Generated
+482
-144
@@ -9,6 +9,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@grpc/grpc-js": "^1.14.3",
|
||||||
|
"@grpc/proto-loader": "^0.8.0",
|
||||||
"@pulsecron/pulse": "1.6.8",
|
"@pulsecron/pulse": "1.6.8",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"bcryptjs": "3.0.2",
|
"bcryptjs": "3.0.2",
|
||||||
@@ -28,7 +30,6 @@
|
|||||||
"ioredis": "^5.4.2",
|
"ioredis": "^5.4.2",
|
||||||
"isomorphic-dompurify": "^2.26.0",
|
"isomorphic-dompurify": "^2.26.0",
|
||||||
"jmespath": "^0.16.0",
|
"jmespath": "^0.16.0",
|
||||||
"joi": "^17.13.1",
|
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"jsonwebtoken": "9.0.2",
|
"jsonwebtoken": "9.0.2",
|
||||||
"mailersend": "^2.2.0",
|
"mailersend": "^2.2.0",
|
||||||
@@ -41,7 +42,8 @@
|
|||||||
"ssl-checker": "2.0.10",
|
"ssl-checker": "2.0.10",
|
||||||
"super-simple-scheduler": "1.4.5",
|
"super-simple-scheduler": "1.4.5",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"winston": "^3.13.0"
|
"winston": "^3.13.0",
|
||||||
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
@@ -61,6 +63,8 @@
|
|||||||
"@types/papaparse": "^5.5.2",
|
"@types/papaparse": "^5.5.2",
|
||||||
"@types/ping": "0.4.4",
|
"@types/ping": "0.4.4",
|
||||||
"@types/swagger-ui-express": "4.1.8",
|
"@types/swagger-ui-express": "4.1.8",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||||
|
"@typescript-eslint/parser": "^8.56.1",
|
||||||
"c8": "10.1.3",
|
"c8": "10.1.3",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
"eslint-plugin-mocha": "^10.5.0",
|
"eslint-plugin-mocha": "^10.5.0",
|
||||||
@@ -74,7 +78,8 @@
|
|||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.20.5",
|
"tsx": "4.20.5",
|
||||||
"typescript": "5.9.2"
|
"typescript": "5.9.2",
|
||||||
|
"typescript-eslint": "^8.56.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
@@ -2041,9 +2046,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||||
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
|
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2198,9 +2203,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@grpc/grpc-js": {
|
"node_modules/@grpc/grpc-js": {
|
||||||
"version": "1.14.0",
|
"version": "1.14.3",
|
||||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
|
||||||
"integrity": "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==",
|
"integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/proto-loader": "^0.8.0",
|
"@grpc/proto-loader": "^0.8.0",
|
||||||
@@ -2210,7 +2215,7 @@
|
|||||||
"node": ">=12.10.0"
|
"node": ">=12.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": {
|
"node_modules/@grpc/proto-loader": {
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
|
||||||
"integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
|
"integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
|
||||||
@@ -2228,39 +2233,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@grpc/proto-loader": {
|
|
||||||
"version": "0.7.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
|
|
||||||
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"lodash.camelcase": "^4.3.0",
|
|
||||||
"long": "^5.0.0",
|
|
||||||
"protobufjs": "^7.2.5",
|
|
||||||
"yargs": "^17.7.2"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@hapi/hoek": {
|
|
||||||
"version": "9.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
|
||||||
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/@hapi/topo": {
|
|
||||||
"version": "5.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
|
|
||||||
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"@hapi/hoek": "^9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -3475,27 +3447,6 @@
|
|||||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@sideway/address": {
|
|
||||||
"version": "4.1.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
|
||||||
"integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"@hapi/hoek": "^9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@sideway/formula": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/@sideway/pinpoint": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/@sinclair/typebox": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.34.47",
|
"version": "0.34.47",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.47.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.47.tgz",
|
||||||
@@ -4223,16 +4174,6 @@
|
|||||||
"node": ">=14.16"
|
"node": ">=14.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@trysound/sax": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
|
|
||||||
"license": "ISC",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.13.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.12",
|
"version": "1.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||||
@@ -4749,6 +4690,360 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
|
"version": "8.56.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz",
|
||||||
|
"integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@eslint-community/regexpp": "^4.12.2",
|
||||||
|
"@typescript-eslint/scope-manager": "8.56.1",
|
||||||
|
"@typescript-eslint/type-utils": "8.56.1",
|
||||||
|
"@typescript-eslint/utils": "8.56.1",
|
||||||
|
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||||
|
"ignore": "^7.0.5",
|
||||||
|
"natural-compare": "^1.4.0",
|
||||||
|
"ts-api-utils": "^2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@typescript-eslint/parser": "^8.56.1",
|
||||||
|
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||||
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||||
|
"version": "7.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||||
|
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/parser": {
|
||||||
|
"version": "8.56.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz",
|
||||||
|
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/scope-manager": "8.56.1",
|
||||||
|
"@typescript-eslint/types": "8.56.1",
|
||||||
|
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||||
|
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||||
|
"debug": "^4.4.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||||
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/parser/node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
|
"version": "8.56.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz",
|
||||||
|
"integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/tsconfig-utils": "^8.56.1",
|
||||||
|
"@typescript-eslint/types": "^8.56.1",
|
||||||
|
"debug": "^4.4.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/project-service/node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
|
"version": "8.56.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz",
|
||||||
|
"integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/types": "8.56.1",
|
||||||
|
"@typescript-eslint/visitor-keys": "8.56.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
|
"version": "8.56.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz",
|
||||||
|
"integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
|
"version": "8.56.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz",
|
||||||
|
"integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/types": "8.56.1",
|
||||||
|
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||||
|
"@typescript-eslint/utils": "8.56.1",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"ts-api-utils": "^2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||||
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/type-utils/node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/types": {
|
||||||
|
"version": "8.56.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz",
|
||||||
|
"integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
|
"version": "8.56.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz",
|
||||||
|
"integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/project-service": "8.56.1",
|
||||||
|
"@typescript-eslint/tsconfig-utils": "8.56.1",
|
||||||
|
"@typescript-eslint/types": "8.56.1",
|
||||||
|
"@typescript-eslint/visitor-keys": "8.56.1",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"minimatch": "^10.2.2",
|
||||||
|
"semver": "^7.7.3",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
|
"ts-api-utils": "^2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||||
|
"version": "10.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
|
||||||
|
"integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^5.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/utils": {
|
||||||
|
"version": "8.56.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz",
|
||||||
|
"integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@eslint-community/eslint-utils": "^4.9.1",
|
||||||
|
"@typescript-eslint/scope-manager": "8.56.1",
|
||||||
|
"@typescript-eslint/types": "8.56.1",
|
||||||
|
"@typescript-eslint/typescript-estree": "8.56.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||||
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
|
"version": "8.56.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz",
|
||||||
|
"integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/types": "8.56.1",
|
||||||
|
"eslint-visitor-keys": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@ungap/structured-clone": {
|
"node_modules/@ungap/structured-clone": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||||
@@ -6948,6 +7243,24 @@
|
|||||||
"node": ">= 8.0"
|
"node": ">= 8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dockerode/node_modules/@grpc/proto-loader": {
|
||||||
|
"version": "0.7.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
|
||||||
|
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.camelcase": "^4.3.0",
|
||||||
|
"long": "^5.0.0",
|
||||||
|
"protobufjs": "^7.2.5",
|
||||||
|
"yargs": "^17.7.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dockerode/node_modules/uuid": {
|
"node_modules/dockerode/node_modules/uuid": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
@@ -9938,19 +10251,6 @@
|
|||||||
"node": ">= 0.6.0"
|
"node": ">= 0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/joi": {
|
|
||||||
"version": "17.13.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
|
|
||||||
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"@hapi/hoek": "^9.3.0",
|
|
||||||
"@hapi/topo": "^5.1.0",
|
|
||||||
"@sideway/address": "^4.1.5",
|
|
||||||
"@sideway/formula": "^3.0.1",
|
|
||||||
"@sideway/pinpoint": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -14173,63 +14473,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svgo": {
|
|
||||||
"version": "3.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
|
|
||||||
"integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@trysound/sax": "0.2.0",
|
|
||||||
"commander": "^7.2.0",
|
|
||||||
"css-select": "^5.1.0",
|
|
||||||
"css-tree": "^2.3.1",
|
|
||||||
"css-what": "^6.1.0",
|
|
||||||
"csso": "^5.0.5",
|
|
||||||
"picocolors": "^1.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"svgo": "bin/svgo"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/svgo"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/svgo/node_modules/commander": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
|
||||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/svgo/node_modules/css-tree": {
|
|
||||||
"version": "2.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
|
||||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"mdn-data": "2.0.30",
|
|
||||||
"source-map-js": "^1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/svgo/node_modules/mdn-data": {
|
|
||||||
"version": "2.0.30",
|
|
||||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
|
||||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
|
||||||
"license": "CC0-1.0",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"node_modules/swagger-ui-dist": {
|
"node_modules/swagger-ui-dist": {
|
||||||
"version": "5.30.0",
|
"version": "5.30.0",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.0.tgz",
|
||||||
@@ -14365,6 +14608,55 @@
|
|||||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyglobby": {
|
||||||
|
"version": "0.2.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fdir": "^6.5.0",
|
||||||
|
"picomatch": "^4.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyglobby/node_modules/fdir": {
|
||||||
|
"version": "6.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"picomatch": "^3 || ^4"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"picomatch": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tldts": {
|
"node_modules/tldts": {
|
||||||
"version": "6.1.86",
|
"version": "6.1.86",
|
||||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||||
@@ -14454,6 +14746,19 @@
|
|||||||
"node": ">= 14.0.0"
|
"node": ">= 14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-api-utils": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.8.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ts-jest": {
|
"node_modules/ts-jest": {
|
||||||
"version": "29.4.6",
|
"version": "29.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz",
|
||||||
@@ -14685,6 +14990,30 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typescript-eslint": {
|
||||||
|
"version": "8.56.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz",
|
||||||
|
"integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@typescript-eslint/eslint-plugin": "8.56.1",
|
||||||
|
"@typescript-eslint/parser": "8.56.1",
|
||||||
|
"@typescript-eslint/typescript-estree": "8.56.1",
|
||||||
|
"@typescript-eslint/utils": "8.56.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||||
|
"typescript": ">=4.8.4 <6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/uglify-js": {
|
"node_modules/uglify-js": {
|
||||||
"version": "3.19.3",
|
"version": "3.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
||||||
@@ -15481,6 +15810,15 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "4.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||||
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+8
-3
@@ -24,6 +24,8 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@grpc/grpc-js": "^1.14.3",
|
||||||
|
"@grpc/proto-loader": "^0.8.0",
|
||||||
"@pulsecron/pulse": "1.6.8",
|
"@pulsecron/pulse": "1.6.8",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"bcryptjs": "3.0.2",
|
"bcryptjs": "3.0.2",
|
||||||
@@ -43,7 +45,6 @@
|
|||||||
"ioredis": "^5.4.2",
|
"ioredis": "^5.4.2",
|
||||||
"isomorphic-dompurify": "^2.26.0",
|
"isomorphic-dompurify": "^2.26.0",
|
||||||
"jmespath": "^0.16.0",
|
"jmespath": "^0.16.0",
|
||||||
"joi": "^17.13.1",
|
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"jsonwebtoken": "9.0.2",
|
"jsonwebtoken": "9.0.2",
|
||||||
"mailersend": "^2.2.0",
|
"mailersend": "^2.2.0",
|
||||||
@@ -56,7 +57,8 @@
|
|||||||
"ssl-checker": "2.0.10",
|
"ssl-checker": "2.0.10",
|
||||||
"super-simple-scheduler": "1.4.5",
|
"super-simple-scheduler": "1.4.5",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"winston": "^3.13.0"
|
"winston": "^3.13.0",
|
||||||
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
@@ -76,6 +78,8 @@
|
|||||||
"@types/papaparse": "^5.5.2",
|
"@types/papaparse": "^5.5.2",
|
||||||
"@types/ping": "0.4.4",
|
"@types/ping": "0.4.4",
|
||||||
"@types/swagger-ui-express": "4.1.8",
|
"@types/swagger-ui-express": "4.1.8",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||||
|
"@typescript-eslint/parser": "^8.56.1",
|
||||||
"c8": "10.1.3",
|
"c8": "10.1.3",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
"eslint-plugin-mocha": "^10.5.0",
|
"eslint-plugin-mocha": "^10.5.0",
|
||||||
@@ -89,6 +93,7 @@
|
|||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.20.5",
|
"tsx": "4.20.5",
|
||||||
"typescript": "5.9.2"
|
"typescript": "5.9.2",
|
||||||
|
"typescript-eslint": "^8.56.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,6 @@ import { MonitorModel } from "../dist/db/models/Monitor.js";
|
|||||||
import { CheckModel } from "../dist/db/models/Check.js";
|
import { CheckModel } from "../dist/db/models/Check.js";
|
||||||
|
|
||||||
const DEFAULT_MONITOR_ID = "000000000000000000000001";
|
const DEFAULT_MONITOR_ID = "000000000000000000000001";
|
||||||
const DEFAULT_TEAM_ID = "0000000000000000000000aa";
|
|
||||||
const DEFAULT_USER_ID = "0000000000000000000000bb";
|
|
||||||
const DEFAULT_MONITOR_TYPE = "http";
|
const DEFAULT_MONITOR_TYPE = "http";
|
||||||
const DEFAULT_TOTAL = 1_000_000;
|
const DEFAULT_TOTAL = 1_000_000;
|
||||||
const DEFAULT_BATCH_SIZE = 5_000;
|
const DEFAULT_BATCH_SIZE = 5_000;
|
||||||
@@ -12,7 +10,7 @@ const DEFAULT_BATCH_SIZE = 5_000;
|
|||||||
const parseObjectId = (value, fallback) => {
|
const parseObjectId = (value, fallback) => {
|
||||||
try {
|
try {
|
||||||
return new mongoose.Types.ObjectId(value || fallback);
|
return new mongoose.Types.ObjectId(value || fallback);
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.warn(`Invalid ObjectId '${value}', falling back to '${fallback}'.`);
|
console.warn(`Invalid ObjectId '${value}', falling back to '${fallback}'.`);
|
||||||
return new mongoose.Types.ObjectId(fallback);
|
return new mongoose.Types.ObjectId(fallback);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import MonitorController from "../controllers/monitorController.js";
|
|||||||
import AuthController from "../controllers/authController.js";
|
import AuthController from "../controllers/authController.js";
|
||||||
import SettingsController from "../controllers/settingsController.js";
|
import SettingsController from "../controllers/settingsController.js";
|
||||||
import CheckController from "../controllers/checkController.js";
|
import CheckController from "../controllers/checkController.js";
|
||||||
|
import GeoCheckController from "../controllers/geoCheckController.js";
|
||||||
import InviteController from "../controllers/inviteController.js";
|
import InviteController from "../controllers/inviteController.js";
|
||||||
import MaintenanceWindowController from "../controllers/maintenanceWindowController.js";
|
import MaintenanceWindowController from "../controllers/maintenanceWindowController.js";
|
||||||
import QueueController from "../controllers/queueController.js";
|
import QueueController from "../controllers/queueController.js";
|
||||||
@@ -17,6 +18,7 @@ export interface InitializedControllers {
|
|||||||
monitorController: MonitorController;
|
monitorController: MonitorController;
|
||||||
settingsController: SettingsController;
|
settingsController: SettingsController;
|
||||||
checkController: CheckController;
|
checkController: CheckController;
|
||||||
|
geoCheckController: GeoCheckController;
|
||||||
inviteController: InviteController;
|
inviteController: InviteController;
|
||||||
maintenanceWindowController: MaintenanceWindowController;
|
maintenanceWindowController: MaintenanceWindowController;
|
||||||
queueController: QueueController;
|
queueController: QueueController;
|
||||||
@@ -32,6 +34,7 @@ export const initializeControllers = (services: InitializedServices): Initialize
|
|||||||
monitorController: new MonitorController(services.monitorService),
|
monitorController: new MonitorController(services.monitorService),
|
||||||
settingsController: new SettingsController(services.settingsService, services.emailService),
|
settingsController: new SettingsController(services.settingsService, services.emailService),
|
||||||
checkController: new CheckController(services.checkService),
|
checkController: new CheckController(services.checkService),
|
||||||
|
geoCheckController: new GeoCheckController(services.geoChecksService),
|
||||||
inviteController: new InviteController(services.inviteService),
|
inviteController: new InviteController(services.inviteService),
|
||||||
maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService),
|
maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService),
|
||||||
queueController: new QueueController(services.jobQueue),
|
queueController: new QueueController(services.jobQueue),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import AuthRoutes from "../routes/authRoute.js";
|
|||||||
import InviteRoutes from "../routes/inviteRoute.js";
|
import InviteRoutes from "../routes/inviteRoute.js";
|
||||||
import MonitorRoutes from "../routes/monitorRoute.js";
|
import MonitorRoutes from "../routes/monitorRoute.js";
|
||||||
import CheckRoutes from "../routes/checkRoute.js";
|
import CheckRoutes from "../routes/checkRoute.js";
|
||||||
|
import GeoCheckRoutes from "../routes/geoCheckRoutes.js";
|
||||||
import SettingsRoutes from "../routes/settingsRoute.js";
|
import SettingsRoutes from "../routes/settingsRoute.js";
|
||||||
import MaintenanceWindowRoutes from "../routes/maintenanceWindowRoute.js";
|
import MaintenanceWindowRoutes from "../routes/maintenanceWindowRoute.js";
|
||||||
import StatusPageRoutes from "../routes/statusPageRoute.js";
|
import StatusPageRoutes from "../routes/statusPageRoute.js";
|
||||||
@@ -22,6 +23,7 @@ export const setupRoutes = (app: any, controllers: Record<string, any>, services
|
|||||||
const monitorRoutes = new MonitorRoutes(controllers.monitorController);
|
const monitorRoutes = new MonitorRoutes(controllers.monitorController);
|
||||||
const settingsRoutes = new SettingsRoutes(controllers.settingsController);
|
const settingsRoutes = new SettingsRoutes(controllers.settingsController);
|
||||||
const checkRoutes = new CheckRoutes(controllers.checkController);
|
const checkRoutes = new CheckRoutes(controllers.checkController);
|
||||||
|
const geoCheckRoutes = new GeoCheckRoutes(controllers.geoCheckController);
|
||||||
const inviteRoutes = new InviteRoutes(controllers.inviteController, verifyJWT);
|
const inviteRoutes = new InviteRoutes(controllers.inviteController, verifyJWT);
|
||||||
const maintenanceWindowRoutes = new MaintenanceWindowRoutes(controllers.maintenanceWindowController);
|
const maintenanceWindowRoutes = new MaintenanceWindowRoutes(controllers.maintenanceWindowController);
|
||||||
const queueRoutes = new QueueRoutes(controllers.queueController);
|
const queueRoutes = new QueueRoutes(controllers.queueController);
|
||||||
@@ -36,6 +38,7 @@ export const setupRoutes = (app: any, controllers: Record<string, any>, services
|
|||||||
app.use("/api/v1/monitors", verifyJWT, monitorRoutes.getRouter());
|
app.use("/api/v1/monitors", verifyJWT, monitorRoutes.getRouter());
|
||||||
app.use("/api/v1/settings", verifyJWT, settingsRoutes.getRouter());
|
app.use("/api/v1/settings", verifyJWT, settingsRoutes.getRouter());
|
||||||
app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter());
|
app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter());
|
||||||
|
app.use("/api/v1/geo-checks", verifyJWT, geoCheckRoutes.getRouter());
|
||||||
app.use("/api/v1/invite", inviteRoutes.getRouter());
|
app.use("/api/v1/invite", inviteRoutes.getRouter());
|
||||||
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter());
|
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter());
|
||||||
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
|
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/S
|
|||||||
import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
||||||
import UserService from "../service/business/userService.js";
|
import UserService from "../service/business/userService.js";
|
||||||
import CheckService from "../service/business/checkService.js";
|
import CheckService from "../service/business/checkService.js";
|
||||||
|
import GeoChecksService from "../service/business/geoChecksService.js";
|
||||||
|
import GlobalPingService from "../service/infrastructure/globalPingService.js";
|
||||||
import DiagnosticService from "../service/business/diagnosticService.js";
|
import DiagnosticService from "../service/business/diagnosticService.js";
|
||||||
import InviteService from "../service/business/inviteService.js";
|
import InviteService from "../service/business/inviteService.js";
|
||||||
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
|
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
|
||||||
@@ -27,7 +29,6 @@ import { NotificationMessageBuilder, INotificationMessageBuilder } from "../serv
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import got from "got";
|
import got from "got";
|
||||||
import ping from "ping";
|
import ping from "ping";
|
||||||
import http from "http";
|
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import Docker from "dockerode";
|
import Docker from "dockerode";
|
||||||
import net from "net";
|
import net from "net";
|
||||||
@@ -41,11 +42,14 @@ import jwt from "jsonwebtoken";
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { games, GameDig } from "gamedig";
|
import { games, GameDig } from "gamedig";
|
||||||
import jmespath from "jmespath";
|
import jmespath from "jmespath";
|
||||||
|
import * as grpc from "@grpc/grpc-js";
|
||||||
|
import * as protoLoader from "@grpc/proto-loader";
|
||||||
|
|
||||||
// repositories
|
// repositories
|
||||||
import {
|
import {
|
||||||
MongoMonitorsRepository,
|
MongoMonitorsRepository,
|
||||||
MongoChecksRepository,
|
MongoChecksRepository,
|
||||||
|
MongoGeoChecksRepository,
|
||||||
MongoMonitorStatsRepository,
|
MongoMonitorStatsRepository,
|
||||||
MongoStatusPagesRepository,
|
MongoStatusPagesRepository,
|
||||||
MongoUsersRepository,
|
MongoUsersRepository,
|
||||||
@@ -57,6 +61,7 @@ import {
|
|||||||
MongoMaintenanceWindowsRepository,
|
MongoMaintenanceWindowsRepository,
|
||||||
IMonitorsRepository,
|
IMonitorsRepository,
|
||||||
IChecksRepository,
|
IChecksRepository,
|
||||||
|
IGeoChecksRepository,
|
||||||
IMonitorStatsRepository,
|
IMonitorStatsRepository,
|
||||||
IStatusPagesRepository,
|
IStatusPagesRepository,
|
||||||
IUsersRepository,
|
IUsersRepository,
|
||||||
@@ -81,12 +86,13 @@ export type InitializedServices = {
|
|||||||
jobQueue: any;
|
jobQueue: any;
|
||||||
userService: any;
|
userService: any;
|
||||||
checkService: any;
|
checkService: any;
|
||||||
|
geoChecksService: any;
|
||||||
diagnosticService: any;
|
diagnosticService: any;
|
||||||
inviteService: any;
|
inviteService: any;
|
||||||
maintenanceWindowService: any;
|
maintenanceWindowService: any;
|
||||||
monitorService: any;
|
monitorService: any;
|
||||||
incidentService: any;
|
incidentService: any;
|
||||||
logger: any;
|
logger: ILogger;
|
||||||
notificationsService: INotificationsService;
|
notificationsService: INotificationsService;
|
||||||
statusPageService: IStatusPageService;
|
statusPageService: IStatusPageService;
|
||||||
notificationMessageBuilder: INotificationMessageBuilder;
|
notificationMessageBuilder: INotificationMessageBuilder;
|
||||||
@@ -94,6 +100,7 @@ export type InitializedServices = {
|
|||||||
// Repositories
|
// Repositories
|
||||||
monitorsRepository: IMonitorsRepository;
|
monitorsRepository: IMonitorsRepository;
|
||||||
checksRepository: IChecksRepository;
|
checksRepository: IChecksRepository;
|
||||||
|
geoChecksRepository: IGeoChecksRepository;
|
||||||
monitorStatsRepository: IMonitorStatsRepository;
|
monitorStatsRepository: IMonitorStatsRepository;
|
||||||
statusPagesRepository: IStatusPagesRepository;
|
statusPagesRepository: IStatusPagesRepository;
|
||||||
usersRepository: IUsersRepository;
|
usersRepository: IUsersRepository;
|
||||||
@@ -126,6 +133,7 @@ export const initializeServices = async ({
|
|||||||
// Repositories
|
// Repositories
|
||||||
const monitorsRepository = new MongoMonitorsRepository();
|
const monitorsRepository = new MongoMonitorsRepository();
|
||||||
const checksRepository = new MongoChecksRepository(logger);
|
const checksRepository = new MongoChecksRepository(logger);
|
||||||
|
const geoChecksRepository = new MongoGeoChecksRepository(logger);
|
||||||
const monitorStatsRepository = new MongoMonitorStatsRepository();
|
const monitorStatsRepository = new MongoMonitorStatsRepository();
|
||||||
const statusPagesRepository = new MongoStatusPagesRepository();
|
const statusPagesRepository = new MongoStatusPagesRepository();
|
||||||
const usersRepository = new MongoUsersRepository();
|
const usersRepository = new MongoUsersRepository();
|
||||||
@@ -136,38 +144,40 @@ export const initializeServices = async ({
|
|||||||
const teamsRepository = new MongoTeamsRepository();
|
const teamsRepository = new MongoTeamsRepository();
|
||||||
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
||||||
|
|
||||||
const networkService = new NetworkService({
|
const networkService = new NetworkService(
|
||||||
axios,
|
axios,
|
||||||
got,
|
got,
|
||||||
https,
|
https,
|
||||||
jmespath,
|
jmespath,
|
||||||
GameDig,
|
GameDig as unknown as {
|
||||||
|
query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }>;
|
||||||
|
},
|
||||||
ping,
|
ping,
|
||||||
logger,
|
logger,
|
||||||
http,
|
|
||||||
Docker,
|
Docker,
|
||||||
net,
|
net,
|
||||||
settingsService,
|
settingsService,
|
||||||
});
|
grpc,
|
||||||
|
protoLoader
|
||||||
|
);
|
||||||
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
|
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
|
||||||
|
|
||||||
const notificationMessageBuilder = new NotificationMessageBuilder();
|
const notificationMessageBuilder = new NotificationMessageBuilder();
|
||||||
|
|
||||||
const incidentService = new IncidentService({
|
const incidentService = new IncidentService(logger, incidentsRepository, monitorsRepository, usersRepository, notificationMessageBuilder);
|
||||||
|
|
||||||
|
const checkService = new CheckService(monitorsRepository, logger, checksRepository);
|
||||||
|
|
||||||
|
const globalPingService = new GlobalPingService(logger);
|
||||||
|
|
||||||
|
const geoChecksService = new GeoChecksService({
|
||||||
logger,
|
logger,
|
||||||
incidentsRepository,
|
geoChecksRepository,
|
||||||
|
globalPingService,
|
||||||
monitorsRepository,
|
monitorsRepository,
|
||||||
usersRepository,
|
|
||||||
notificationMessageBuilder,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkService = new CheckService({
|
const bufferService = new BufferService(logger, checkService, geoChecksService, settingsService);
|
||||||
monitorsRepository,
|
|
||||||
logger,
|
|
||||||
checksRepository,
|
|
||||||
});
|
|
||||||
|
|
||||||
const bufferService = new BufferService({ logger, checkService, settingsService });
|
|
||||||
|
|
||||||
const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository);
|
const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository);
|
||||||
|
|
||||||
@@ -192,13 +202,13 @@ export const initializeServices = async ({
|
|||||||
notificationMessageBuilder
|
notificationMessageBuilder
|
||||||
);
|
);
|
||||||
|
|
||||||
const superSimpleQueueHelper = new SuperSimpleQueueHelper({
|
const superSimpleQueueHelper = new SuperSimpleQueueHelper(
|
||||||
logger,
|
logger,
|
||||||
networkService,
|
networkService,
|
||||||
statusService,
|
statusService,
|
||||||
notificationsService,
|
notificationsService,
|
||||||
checkService,
|
checkService,
|
||||||
buffer: bufferService,
|
bufferService,
|
||||||
incidentService,
|
incidentService,
|
||||||
maintenanceWindowsRepository,
|
maintenanceWindowsRepository,
|
||||||
monitorsRepository,
|
monitorsRepository,
|
||||||
@@ -206,13 +216,11 @@ export const initializeServices = async ({
|
|||||||
monitorStatsRepository,
|
monitorStatsRepository,
|
||||||
checksRepository,
|
checksRepository,
|
||||||
incidentsRepository,
|
incidentsRepository,
|
||||||
});
|
geoChecksService,
|
||||||
|
geoChecksRepository
|
||||||
|
);
|
||||||
|
|
||||||
const superSimpleQueue = await SuperSimpleQueue.create({
|
const superSimpleQueue = await SuperSimpleQueue.create(logger, superSimpleQueueHelper, monitorsRepository);
|
||||||
logger,
|
|
||||||
helper: superSimpleQueueHelper,
|
|
||||||
monitorsRepository,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Business services
|
// Business services
|
||||||
const userService = new UserService({
|
const userService = new UserService({
|
||||||
@@ -247,6 +255,7 @@ export const initializeServices = async ({
|
|||||||
games,
|
games,
|
||||||
monitorsRepository,
|
monitorsRepository,
|
||||||
checksRepository,
|
checksRepository,
|
||||||
|
geoChecksRepository,
|
||||||
monitorStatsRepository,
|
monitorStatsRepository,
|
||||||
statusPagesRepository,
|
statusPagesRepository,
|
||||||
incidentsRepository,
|
incidentsRepository,
|
||||||
@@ -265,6 +274,7 @@ export const initializeServices = async ({
|
|||||||
jobQueue: superSimpleQueue,
|
jobQueue: superSimpleQueue,
|
||||||
userService,
|
userService,
|
||||||
checkService,
|
checkService,
|
||||||
|
geoChecksService,
|
||||||
diagnosticService,
|
diagnosticService,
|
||||||
inviteService,
|
inviteService,
|
||||||
maintenanceWindowService,
|
maintenanceWindowService,
|
||||||
@@ -278,6 +288,7 @@ export const initializeServices = async ({
|
|||||||
// Repositories
|
// Repositories
|
||||||
monitorsRepository,
|
monitorsRepository,
|
||||||
checksRepository,
|
checksRepository,
|
||||||
|
geoChecksRepository,
|
||||||
monitorStatsRepository,
|
monitorStatsRepository,
|
||||||
statusPagesRepository,
|
statusPagesRepository,
|
||||||
usersRepository,
|
usersRepository,
|
||||||
|
|||||||
@@ -6,17 +6,20 @@ import type { UserRole } from "@/types/user.js";
|
|||||||
import {
|
import {
|
||||||
registrationBodyValidation,
|
registrationBodyValidation,
|
||||||
loginValidation,
|
loginValidation,
|
||||||
editUserBodyValidation,
|
|
||||||
createUserBodyValidation,
|
|
||||||
recoveryValidation,
|
recoveryValidation,
|
||||||
recoveryTokenBodyValidation,
|
recoveryTokenBodyValidation,
|
||||||
newPasswordValidation,
|
newPasswordValidation,
|
||||||
|
} from "@/validation/authValidation.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
editUserBodyValidation,
|
||||||
|
createUserBodyValidation,
|
||||||
getUserByIdParamValidation,
|
getUserByIdParamValidation,
|
||||||
editUserByIdParamValidation,
|
editUserByIdParamValidation,
|
||||||
editUserByIdBodyValidation,
|
editUserByIdBodyValidation,
|
||||||
editSuperadminUserByIdBodyValidation,
|
editSuperadminUserByIdBodyValidation,
|
||||||
editUserPasswordByIdBodyValidation,
|
editUserPasswordByIdBodyValidation,
|
||||||
} from "@/validation/joi.js";
|
} from "@/validation/userValidation.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "authController";
|
const SERVICE_NAME = "authController";
|
||||||
|
|
||||||
@@ -40,7 +43,7 @@ class AuthController {
|
|||||||
if (newUser?.email) {
|
if (newUser?.email) {
|
||||||
newUser.email = newUser.email.toLowerCase();
|
newUser.email = newUser.email.toLowerCase();
|
||||||
}
|
}
|
||||||
await registrationBodyValidation.validateAsync(newUser);
|
registrationBodyValidation.parse(newUser);
|
||||||
|
|
||||||
const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file);
|
const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file);
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
@@ -59,7 +62,7 @@ class AuthController {
|
|||||||
if (userData?.email) {
|
if (userData?.email) {
|
||||||
userData.email = userData.email.toLowerCase();
|
userData.email = userData.email.toLowerCase();
|
||||||
}
|
}
|
||||||
await createUserBodyValidation.validateAsync(userData);
|
createUserBodyValidation.parse(userData);
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
const actorRoles = requireUserRoles(req.user?.role) as UserRole[];
|
const actorRoles = requireUserRoles(req.user?.role) as UserRole[];
|
||||||
@@ -79,7 +82,7 @@ class AuthController {
|
|||||||
if (req.body?.email) {
|
if (req.body?.email) {
|
||||||
req.body.email = req.body.email?.toLowerCase();
|
req.body.email = req.body.email?.toLowerCase();
|
||||||
}
|
}
|
||||||
await loginValidation.validateAsync(req.body);
|
loginValidation.parse(req.body);
|
||||||
const { user, token } = await this.userService.loginUser(req.body.email, req.body.password);
|
const { user, token } = await this.userService.loginUser(req.body.email, req.body.password);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -97,7 +100,7 @@ class AuthController {
|
|||||||
|
|
||||||
editUser = async (req: Request, res: Response, next: NextFunction) => {
|
editUser = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await editUserBodyValidation.validateAsync(req.body);
|
editUserBodyValidation.parse(req.body);
|
||||||
const updatedUser = await this.userService.editUser(req.body, req.file, req.user);
|
const updatedUser = await this.userService.editUser(req.body, req.file, req.user);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
@@ -125,7 +128,7 @@ class AuthController {
|
|||||||
|
|
||||||
requestRecovery = async (req: Request, res: Response, next: NextFunction) => {
|
requestRecovery = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await recoveryValidation.validateAsync(req.body);
|
recoveryValidation.parse(req.body);
|
||||||
const email = req?.body?.email;
|
const email = req?.body?.email;
|
||||||
const msgId = await this.userService.requestRecovery(email);
|
const msgId = await this.userService.requestRecovery(email);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -140,7 +143,7 @@ class AuthController {
|
|||||||
|
|
||||||
validateRecovery = async (req: Request, res: Response, next: NextFunction) => {
|
validateRecovery = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await recoveryTokenBodyValidation.validateAsync(req.body);
|
recoveryTokenBodyValidation.parse(req.body);
|
||||||
await this.userService.validateRecovery(req.body.recoveryToken);
|
await this.userService.validateRecovery(req.body.recoveryToken);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -153,7 +156,7 @@ class AuthController {
|
|||||||
|
|
||||||
resetPassword = async (req: Request, res: Response, next: NextFunction) => {
|
resetPassword = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await newPasswordValidation.validateAsync(req.body);
|
newPasswordValidation.parse(req.body);
|
||||||
const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken);
|
const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -192,7 +195,7 @@ class AuthController {
|
|||||||
|
|
||||||
getUserById = async (req: Request, res: Response, next: NextFunction) => {
|
getUserById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getUserByIdParamValidation.validateAsync(req.params);
|
getUserByIdParamValidation.parse(req.params);
|
||||||
const userId = req?.params?.userId;
|
const userId = req?.params?.userId;
|
||||||
const roles = req?.user?.role;
|
const roles = req?.user?.role;
|
||||||
|
|
||||||
@@ -223,12 +226,12 @@ class AuthController {
|
|||||||
const userId = req.params.userId as string;
|
const userId = req.params.userId as string;
|
||||||
const user = { ...req.body };
|
const user = { ...req.body };
|
||||||
|
|
||||||
await editUserByIdParamValidation.validateAsync(req.params);
|
editUserByIdParamValidation.parse(req.params);
|
||||||
// If this is superadmin self edit, allow "superadmin" role
|
// If this is superadmin self edit, allow "superadmin" role
|
||||||
if (userId === req.user?.id) {
|
if (userId === req.user?.id) {
|
||||||
await editSuperadminUserByIdBodyValidation.validateAsync(req.body);
|
editSuperadminUserByIdBodyValidation.parse(req.body);
|
||||||
} else {
|
} else {
|
||||||
await editUserByIdBodyValidation.validateAsync(req.body);
|
editUserByIdBodyValidation.parse(req.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.userService.editUserById(userId, user);
|
await this.userService.editUserById(userId, user);
|
||||||
@@ -246,8 +249,8 @@ class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userId = req.params.userId as string;
|
const userId = req.params.userId as string;
|
||||||
await editUserByIdParamValidation.validateAsync(req.params);
|
editUserByIdParamValidation.parse(req.params);
|
||||||
await editUserPasswordByIdBodyValidation.validateAsync(req.body);
|
editUserPasswordByIdBodyValidation.parse(req.body);
|
||||||
const updatedPassword = req.body.password;
|
const updatedPassword = req.body.password;
|
||||||
await this.userService.setPasswordByUserId(userId, updatedPassword);
|
await this.userService.setPasswordByUserId(userId, updatedPassword);
|
||||||
return res.status(200).json({ success: true, msg: "Password reset successfully" });
|
return res.status(200).json({ success: true, msg: "Password reset successfully" });
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import {
|
|||||||
deleteChecksParamValidation,
|
deleteChecksParamValidation,
|
||||||
deleteChecksByTeamIdParamValidation,
|
deleteChecksByTeamIdParamValidation,
|
||||||
updateChecksTTLBodyValidation,
|
updateChecksTTLBodyValidation,
|
||||||
ackCheckBodyValidation,
|
} from "@/validation/checkValidation.js";
|
||||||
ackAllChecksParamValidation,
|
|
||||||
ackAllChecksBodyValidation,
|
|
||||||
} from "@/validation/joi.js";
|
|
||||||
|
|
||||||
const SERVICE_NAME = "checkController";
|
const SERVICE_NAME = "checkController";
|
||||||
|
|
||||||
@@ -28,8 +25,8 @@ class CheckController {
|
|||||||
|
|
||||||
getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getChecksParamValidation.validateAsync(req.params);
|
getChecksParamValidation.parse(req.params);
|
||||||
await getChecksQueryValidation.validateAsync(req.query);
|
getChecksQueryValidation.parse(req.query);
|
||||||
|
|
||||||
const result = await this.checkService.getChecksByMonitor({
|
const result = await this.checkService.getChecksByMonitor({
|
||||||
monitorId: req?.params?.monitorId,
|
monitorId: req?.params?.monitorId,
|
||||||
@@ -49,7 +46,7 @@ class CheckController {
|
|||||||
|
|
||||||
getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => {
|
getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getTeamChecksQueryValidation.validateAsync(req.query);
|
getTeamChecksQueryValidation.parse(req.query);
|
||||||
const checkData = await this.checkService.getChecksByTeam({
|
const checkData = await this.checkService.getChecksByTeam({
|
||||||
teamId: req?.user?.teamId,
|
teamId: req?.user?.teamId,
|
||||||
query: req?.query,
|
query: req?.query,
|
||||||
@@ -80,7 +77,7 @@ class CheckController {
|
|||||||
|
|
||||||
deleteChecks = async (req: Request, res: Response, next: NextFunction) => {
|
deleteChecks = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await deleteChecksParamValidation.validateAsync(req.params);
|
deleteChecksParamValidation.parse(req.params);
|
||||||
|
|
||||||
const deletedCount = await this.checkService.deleteChecks({
|
const deletedCount = await this.checkService.deleteChecks({
|
||||||
monitorId: req.params.monitorId as string,
|
monitorId: req.params.monitorId as string,
|
||||||
@@ -99,7 +96,7 @@ class CheckController {
|
|||||||
|
|
||||||
deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await deleteChecksByTeamIdParamValidation.validateAsync(req.params);
|
deleteChecksByTeamIdParamValidation.parse(req.params);
|
||||||
|
|
||||||
const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId });
|
const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId });
|
||||||
|
|
||||||
@@ -115,7 +112,7 @@ class CheckController {
|
|||||||
|
|
||||||
updateChecksTTL = async (req: Request, res: Response, next: NextFunction) => {
|
updateChecksTTL = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await updateChecksTTLBodyValidation.validateAsync(req.body);
|
updateChecksTTLBodyValidation.parse(req.body);
|
||||||
|
|
||||||
await this.checkService.updateChecksTTL({
|
await this.checkService.updateChecksTTL({
|
||||||
teamId: req?.user?.teamId,
|
teamId: req?.user?.teamId,
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js";
|
||||||
|
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
|
||||||
|
|
||||||
|
const SERVICE_NAME = "geoCheckController";
|
||||||
|
|
||||||
|
class GeoCheckController {
|
||||||
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
|
private geoChecksService: IGeoChecksService;
|
||||||
|
constructor(geoChecksService: IGeoChecksService) {
|
||||||
|
this.geoChecksService = geoChecksService;
|
||||||
|
}
|
||||||
|
|
||||||
|
get serviceName() {
|
||||||
|
return GeoCheckController.SERVICE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
getChecksParamValidation.parse(req.params);
|
||||||
|
getChecksQueryValidation.parse(req.query);
|
||||||
|
|
||||||
|
const result = await this.geoChecksService.getGeoChecksByMonitor({
|
||||||
|
monitorId: req?.params?.monitorId as string,
|
||||||
|
query: req?.query,
|
||||||
|
teamId: req?.user?.teamId as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
msg: "Geo checks retrieved successfully",
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GeoCheckController;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/joi.js";
|
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js";
|
||||||
import { requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
import { requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
||||||
const SERVICE_NAME = "inviteController";
|
const SERVICE_NAME = "inviteController";
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ class InviteController {
|
|||||||
const userRoles = requireUserRoles(req.user?.role);
|
const userRoles = requireUserRoles(req.user?.role);
|
||||||
const invite = req.body;
|
const invite = req.body;
|
||||||
invite.teamId = teamId;
|
invite.teamId = teamId;
|
||||||
await inviteBodyValidation.validateAsync(invite);
|
inviteBodyValidation.parse(invite);
|
||||||
const inviteToken = await this.inviteService.getInviteToken({ invite, teamId, userRoles });
|
const inviteToken = await this.inviteService.getInviteToken({ invite, teamId, userRoles });
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -39,7 +39,7 @@ class InviteController {
|
|||||||
|
|
||||||
const inviteRequest = req.body;
|
const inviteRequest = req.body;
|
||||||
inviteRequest.teamId = teamId;
|
inviteRequest.teamId = teamId;
|
||||||
await inviteBodyValidation.validateAsync(inviteRequest);
|
inviteBodyValidation.parse(inviteRequest);
|
||||||
|
|
||||||
const inviteToken = await this.inviteService.sendInviteEmail({
|
const inviteToken = await this.inviteService.sendInviteEmail({
|
||||||
invite: inviteRequest,
|
invite: inviteRequest,
|
||||||
@@ -58,7 +58,7 @@ class InviteController {
|
|||||||
|
|
||||||
verifyInviteToken = async (req: Request, res: Response, next: NextFunction) => {
|
verifyInviteToken = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await inviteVerificationBodyValidation.validateAsync(req.body);
|
inviteVerificationBodyValidation.parse(req.body);
|
||||||
const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token });
|
const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token });
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
getMaintenanceWindowsByMonitorIdParamValidation,
|
getMaintenanceWindowsByMonitorIdParamValidation,
|
||||||
getMaintenanceWindowsByTeamIdQueryValidation,
|
getMaintenanceWindowsByTeamIdQueryValidation,
|
||||||
deleteMaintenanceWindowByIdParamValidation,
|
deleteMaintenanceWindowByIdParamValidation,
|
||||||
} from "@/validation/joi.js";
|
} from "@/validation/maintenanceWindowValidation.js";
|
||||||
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "maintenanceWindowController";
|
const SERVICE_NAME = "maintenanceWindowController";
|
||||||
@@ -25,7 +25,7 @@ class MaintenanceWindowController {
|
|||||||
|
|
||||||
createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => {
|
createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await createMaintenanceWindowBodyValidation.validateAsync(req.body);
|
createMaintenanceWindowBodyValidation.parse(req.body);
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body });
|
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body });
|
||||||
@@ -40,7 +40,7 @@ class MaintenanceWindowController {
|
|||||||
};
|
};
|
||||||
getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => {
|
getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
|
getMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class MaintenanceWindowController {
|
|||||||
|
|
||||||
getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query);
|
getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query);
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ class MaintenanceWindowController {
|
|||||||
|
|
||||||
getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params);
|
getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params);
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ class MaintenanceWindowController {
|
|||||||
};
|
};
|
||||||
deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params);
|
deleteMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
@@ -113,8 +113,8 @@ class MaintenanceWindowController {
|
|||||||
|
|
||||||
editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
|
editMaintenanceWindowByIdParamValidation.parse(req.params);
|
||||||
await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
|
editMaintenanceByIdWindowBodyValidation.parse(req.body);
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
getCertificateParamValidation,
|
getCertificateParamValidation,
|
||||||
getHardwareDetailsByIdParamValidation,
|
getHardwareDetailsByIdParamValidation,
|
||||||
getHardwareDetailsByIdQueryValidation,
|
getHardwareDetailsByIdQueryValidation,
|
||||||
} from "@/validation/joi.js";
|
} from "@/validation/monitorValidation.js";
|
||||||
import sslChecker from "ssl-checker";
|
import sslChecker from "ssl-checker";
|
||||||
import {
|
import {
|
||||||
fetchMonitorCertificate,
|
fetchMonitorCertificate,
|
||||||
@@ -23,9 +23,11 @@ import {
|
|||||||
parseMonitorTypeFilter,
|
parseMonitorTypeFilter,
|
||||||
parseSortOrder,
|
parseSortOrder,
|
||||||
requireTeamId,
|
requireTeamId,
|
||||||
|
requireUserId,
|
||||||
} from "./controllerUtils.js";
|
} from "./controllerUtils.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { IMonitorService } from "@/service/index.js";
|
import { IMonitorService } from "@/service/index.js";
|
||||||
|
import { GeoContinent } from "@/types/geoCheck.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "monitorController";
|
const SERVICE_NAME = "monitorController";
|
||||||
class MonitorController {
|
class MonitorController {
|
||||||
@@ -43,7 +45,7 @@ class MonitorController {
|
|||||||
|
|
||||||
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
|
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getCertificateParamValidation.validateAsync(req.params);
|
getCertificateParamValidation.parse(req.params);
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
const monitorId = requireString(req.params?.monitorId, "Monitor ID");
|
const monitorId = requireString(req.params?.monitorId, "Monitor ID");
|
||||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
||||||
@@ -86,8 +88,8 @@ class MonitorController {
|
|||||||
|
|
||||||
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
|
getHardwareDetailsByIdParamValidation.parse(req.params);
|
||||||
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
|
getHardwareDetailsByIdQueryValidation.parse(req.query);
|
||||||
|
|
||||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||||
const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent";
|
const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent";
|
||||||
@@ -110,8 +112,8 @@ class MonitorController {
|
|||||||
};
|
};
|
||||||
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
|
getHardwareDetailsByIdParamValidation.parse(req.params);
|
||||||
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
|
getHardwareDetailsByIdQueryValidation.parse(req.query);
|
||||||
|
|
||||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||||
@@ -133,10 +135,42 @@ class MonitorController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
getMonitorByIdParamValidation.parse(req.params);
|
||||||
|
getMonitorByIdQueryValidation.parse(req.query);
|
||||||
|
|
||||||
|
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||||
|
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||||
|
const continentParam = req?.query?.continent;
|
||||||
|
const continents = continentParam
|
||||||
|
? Array.isArray(continentParam)
|
||||||
|
? (continentParam as GeoContinent[])
|
||||||
|
: [continentParam as GeoContinent]
|
||||||
|
: undefined;
|
||||||
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
|
const data = await this.monitorService.getGeoChecksByMonitorId({
|
||||||
|
teamId,
|
||||||
|
monitorId,
|
||||||
|
dateRange,
|
||||||
|
continents,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
msg: "Geo checks retrieved successfully",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
getMonitorById = async (req: Request, res: Response, next: NextFunction) => {
|
getMonitorById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
getMonitorByIdParamValidation.parse(req.params);
|
||||||
await getMonitorByIdQueryValidation.validateAsync(req.query);
|
getMonitorByIdQueryValidation.parse(req.query);
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||||
@@ -155,7 +189,7 @@ class MonitorController {
|
|||||||
|
|
||||||
createMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
createMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await createMonitorBodyValidation.validateAsync(req.body);
|
createMonitorBodyValidation.parse(req.body);
|
||||||
|
|
||||||
const userId = requireString(req?.user?.id, "User ID");
|
const userId = requireString(req?.user?.id, "User ID");
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
@@ -172,22 +206,23 @@ class MonitorController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
createBulkMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.user?.id;
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
if (!userId) {
|
const userId = requireUserId(req?.user?.id);
|
||||||
throw new AppError({ message: "User ID is required", status: 400 });
|
|
||||||
|
const { monitors } = req.body;
|
||||||
|
|
||||||
|
if (!monitors || !Array.isArray(monitors)) {
|
||||||
|
throw new AppError({ message: "Invalid request: monitors array is required", status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors });
|
||||||
|
|
||||||
const data = req.body;
|
|
||||||
const monitors = await this.monitorService.createBulkMonitors(data, userId, teamId);
|
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "Bulk monitors created successfully",
|
msg: `Successfully imported ${result.imported} monitor(s)`,
|
||||||
data: monitors,
|
data: result,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -196,7 +231,7 @@ class MonitorController {
|
|||||||
|
|
||||||
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
getMonitorByIdParamValidation.parse(req.params);
|
||||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
@@ -229,8 +264,8 @@ class MonitorController {
|
|||||||
|
|
||||||
editMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
editMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
getMonitorByIdParamValidation.parse(req.params);
|
||||||
await editMonitorBodyValidation.validateAsync(req.body);
|
editMonitorBodyValidation.parse(req.body);
|
||||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
@@ -248,7 +283,7 @@ class MonitorController {
|
|||||||
|
|
||||||
pauseMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
pauseMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await pauseMonitorParamValidation.validateAsync(req.params);
|
pauseMonitorParamValidation.parse(req.params);
|
||||||
|
|
||||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
@@ -301,8 +336,8 @@ class MonitorController {
|
|||||||
|
|
||||||
getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
getMonitorsByTeamIdParamValidation.parse(req.params);
|
||||||
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
|
getMonitorsByTeamIdQueryValidation.parse(req.query);
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
const type = parseMonitorTypeFilter(req.query?.type);
|
const type = parseMonitorTypeFilter(req.query?.type);
|
||||||
@@ -322,8 +357,8 @@ class MonitorController {
|
|||||||
|
|
||||||
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
getMonitorsByTeamIdParamValidation.parse(req.params);
|
||||||
await getMonitorsWithChecksQueryValidation.validateAsync(req.query);
|
getMonitorsWithChecksQueryValidation.parse(req.query);
|
||||||
const explain = optionalBoolean(req?.query?.explain, "explain");
|
const explain = optionalBoolean(req?.query?.explain, "explain");
|
||||||
const limit = optionalNumber(req?.query?.limit, "limit");
|
const limit = optionalNumber(req?.query?.limit, "limit");
|
||||||
const page = optionalNumber(req?.query?.page, "page");
|
const page = optionalNumber(req?.query?.page, "page");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
import { Notification } from "@/types/index.js";
|
import { Notification } from "@/types/index.js";
|
||||||
import { createNotificationBodyValidation } from "@/validation/joi.js";
|
import { createNotificationBodyValidation } from "@/validation/notificationValidation.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||||
import { INotificationsService } from "@/service/index.js";
|
import { INotificationsService } from "@/service/index.js";
|
||||||
@@ -42,9 +42,7 @@ class NotificationController {
|
|||||||
|
|
||||||
createNotification = async (req: Request, res: Response, next: NextFunction) => {
|
createNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await createNotificationBodyValidation.validateAsync(req.body, {
|
createNotificationBodyValidation.parse(req.body);
|
||||||
abortEarly: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const body = req.body;
|
const body = req.body;
|
||||||
|
|
||||||
@@ -134,9 +132,7 @@ class NotificationController {
|
|||||||
|
|
||||||
editNotification = async (req: Request, res: Response, next: NextFunction) => {
|
editNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await createNotificationBodyValidation.validateAsync(req.body, {
|
createNotificationBodyValidation.parse(req.body);
|
||||||
abortEarly: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
const notificationId = req.params.id as string;
|
const notificationId = req.params.id as string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { sendTestEmailBodyValidation, updateAppSettingsBodyValidation } from "@/validation/joi.js";
|
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js";
|
||||||
|
import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "SettingsController";
|
const SERVICE_NAME = "SettingsController";
|
||||||
@@ -55,20 +56,24 @@ class SettingsController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
updateAppSettings = async (req: Request, res: Response, next: NextFunction) => {
|
updateAppSettings = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
await updateAppSettingsBodyValidation.validateAsync(req.body);
|
try {
|
||||||
|
updateAppSettingsBodyValidation.parse(req.body);
|
||||||
|
|
||||||
const updatedSettings = await this.settingsService.updateDbSettings(req.body);
|
const updatedSettings = await this.settingsService.updateDbSettings(req.body);
|
||||||
const returnSettings = this.buildAppSettings(updatedSettings);
|
const returnSettings = this.buildAppSettings(updatedSettings);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "App settings updated successfully",
|
msg: "App settings updated successfully",
|
||||||
data: returnSettings,
|
data: returnSettings,
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sendTestEmail = async (req: Request, res: Response, next: NextFunction) => {
|
sendTestEmail = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await sendTestEmailBodyValidation.validateAsync(req.body);
|
sendTestEmailBodyValidation.parse(req.body);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
to,
|
to,
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "@/validation/joi.js";
|
import {
|
||||||
|
createStatusPageBodyValidation,
|
||||||
|
getStatusPageParamValidation,
|
||||||
|
getStatusPageQueryValidation,
|
||||||
|
imageValidation,
|
||||||
|
} from "@/validation/statusPageValidation.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { requireTeamId, requireUserId } from "@/controllers/controllerUtils.js";
|
import { requireTeamId, requireUserId } from "@/controllers/controllerUtils.js";
|
||||||
import { IStatusPageService } from "@/service/business/statusPageService.js";
|
import { IStatusPageService } from "@/service/business/statusPageService.js";
|
||||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||||
import { ISettingsService } from "@/service/system/settingsService.js";
|
import { ISettingsService } from "@/service/system/settingsService.js";
|
||||||
import { ParseBoolean } from "@/utils/utils.js";
|
|
||||||
import { NormalizeData } from "@/utils/dataUtils.js";
|
import { NormalizeData } from "@/utils/dataUtils.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "statusPageController";
|
const SERVICE_NAME = "statusPageController";
|
||||||
@@ -28,8 +32,10 @@ class StatusPageController {
|
|||||||
|
|
||||||
createStatusPage = async (req: Request, res: Response, next: NextFunction) => {
|
createStatusPage = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await createStatusPageBodyValidation.validateAsync(req.body);
|
createStatusPageBodyValidation.parse(req.body);
|
||||||
await imageValidation.validateAsync(req.file);
|
if (req.file) {
|
||||||
|
imageValidation.parse(req.file);
|
||||||
|
}
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
const userId = requireUserId(req?.user?.id);
|
const userId = requireUserId(req?.user?.id);
|
||||||
@@ -47,8 +53,10 @@ class StatusPageController {
|
|||||||
|
|
||||||
updateStatusPage = async (req: Request, res: Response, next: NextFunction) => {
|
updateStatusPage = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await createStatusPageBodyValidation.validateAsync(req.body);
|
createStatusPageBodyValidation.parse(req.body);
|
||||||
await imageValidation.validateAsync(req.file);
|
if (req.file) {
|
||||||
|
imageValidation.parse(req.file);
|
||||||
|
}
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
const statusPageId = req.params.id as string;
|
const statusPageId = req.params.id as string;
|
||||||
if (!statusPageId) {
|
if (!statusPageId) {
|
||||||
@@ -70,8 +78,8 @@ class StatusPageController {
|
|||||||
|
|
||||||
getStatusPageByUrl = async (req: Request, res: Response, next: NextFunction) => {
|
getStatusPageByUrl = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
await getStatusPageParamValidation.validateAsync(req.params);
|
getStatusPageParamValidation.parse(req.params);
|
||||||
await getStatusPageQueryValidation.validateAsync(req.query);
|
getStatusPageQueryValidation.parse(req.query);
|
||||||
|
|
||||||
if (!req.params.url) {
|
if (!req.params.url) {
|
||||||
throw new AppError({ message: "Status page URL is required", status: 400 });
|
throw new AppError({ message: "Status page URL is required", status: 400 });
|
||||||
@@ -101,6 +109,7 @@ class StatusPageController {
|
|||||||
const normalizedMonitors = sortedMonitors.map((monitor) => {
|
const normalizedMonitors = sortedMonitors.map((monitor) => {
|
||||||
const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100);
|
const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100);
|
||||||
if (!showURL) {
|
if (!showURL) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { url, port, secret, notifications, ...rest } = monitor;
|
const { url, port, secret, notifications, ...rest } = monitor;
|
||||||
return { ...rest, checks: normalizedChecks };
|
return { ...rest, checks: normalizedChecks };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import { Schema, model, Types } from "mongoose";
|
||||||
|
import { MonitorTypes, type MonitorType } from "@/types/monitor.js";
|
||||||
|
import type { GeoCheck, GeoCheckLocation, GeoCheckMetadata, GeoCheckResult, GeoCheckTimings } from "@/types/geoCheck.js";
|
||||||
|
|
||||||
|
type GeoCheckMetadataDocument = Omit<GeoCheckMetadata, "monitorId" | "teamId"> & {
|
||||||
|
monitorId: Types.ObjectId;
|
||||||
|
teamId: Types.ObjectId;
|
||||||
|
type: MonitorType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GeoCheckDocumentBase = Omit<GeoCheck, "id" | "metadata" | "expiry" | "createdAt" | "updatedAt" | "results"> & {
|
||||||
|
metadata: GeoCheckMetadataDocument;
|
||||||
|
results: GeoCheckResult[];
|
||||||
|
expiry: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
__v: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface GeoCheckDocument extends GeoCheckDocumentBase {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const geoCheckMetadataSchema = new Schema<GeoCheckMetadataDocument>(
|
||||||
|
{
|
||||||
|
monitorId: { type: Schema.Types.ObjectId, required: true, index: true },
|
||||||
|
teamId: { type: Schema.Types.ObjectId, required: true, index: true },
|
||||||
|
type: { type: String, required: true, enum: MonitorTypes },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const geoCheckTimingsSchema = new Schema<GeoCheckTimings>(
|
||||||
|
{
|
||||||
|
total: { type: Number, default: 0 },
|
||||||
|
dns: { type: Number, default: 0 },
|
||||||
|
tcp: { type: Number, default: 0 },
|
||||||
|
tls: { type: Number, default: 0 },
|
||||||
|
firstByte: { type: Number, default: 0 },
|
||||||
|
download: { type: Number, default: 0 },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const geoCheckLocationSchema = new Schema<GeoCheckLocation>(
|
||||||
|
{
|
||||||
|
continent: { type: String, required: true },
|
||||||
|
region: { type: String, default: "" },
|
||||||
|
country: { type: String, default: "" },
|
||||||
|
state: { type: String, default: "" },
|
||||||
|
city: { type: String, default: "" },
|
||||||
|
longitude: { type: Number, default: 0 },
|
||||||
|
latitude: { type: Number, default: 0 },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const geoCheckResultSchema = new Schema<GeoCheckResult>(
|
||||||
|
{
|
||||||
|
location: {
|
||||||
|
type: geoCheckLocationSchema,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
statusCode: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
timings: {
|
||||||
|
type: geoCheckTimingsSchema,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const GeoCheckSchema = new Schema<GeoCheckDocument>(
|
||||||
|
{
|
||||||
|
metadata: {
|
||||||
|
type: geoCheckMetadataSchema,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
results: {
|
||||||
|
type: [geoCheckResultSchema],
|
||||||
|
required: true,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
expiry: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
strict: false,
|
||||||
|
timeseries: {
|
||||||
|
timeField: "createdAt",
|
||||||
|
metaField: "metadata",
|
||||||
|
granularity: "seconds",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
GeoCheckSchema.index({ "metadata.monitorId": 1, createdAt: -1 });
|
||||||
|
GeoCheckSchema.index({ "metadata.monitorId": 1, createdAt: 1 });
|
||||||
|
GeoCheckSchema.index({ "metadata.teamId": 1, createdAt: -1 });
|
||||||
|
GeoCheckSchema.index({ createdAt: 1 });
|
||||||
|
|
||||||
|
const GeoCheckModel = model<GeoCheckDocument>("GeoCheck", GeoCheckSchema);
|
||||||
|
|
||||||
|
export type { GeoCheckMetadataDocument };
|
||||||
|
export { GeoCheckModel };
|
||||||
|
export default GeoCheckModel;
|
||||||
@@ -93,6 +93,12 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
|||||||
type: String,
|
type: String,
|
||||||
enum: ["equal", "include", "regex", ""],
|
enum: ["equal", "include", "regex", ""],
|
||||||
},
|
},
|
||||||
|
basicAuthUser: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
basicAuthPassword: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
url: {
|
url: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -160,6 +166,10 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
|||||||
gameId: {
|
gameId: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
grpcServiceName: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
group: {
|
group: {
|
||||||
type: String,
|
type: String,
|
||||||
trim: true,
|
trim: true,
|
||||||
@@ -169,6 +179,18 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
|||||||
return value && value.trim() ? value.trim() : null;
|
return value && value.trim() ? value.trim() : null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
geoCheckEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
geoCheckLocations: {
|
||||||
|
type: [String],
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
geoCheckInterval: {
|
||||||
|
type: Number,
|
||||||
|
default: 300000,
|
||||||
|
},
|
||||||
recentChecks: {
|
recentChecks: {
|
||||||
type: [checkSnapshotSchema],
|
type: [checkSnapshotSchema],
|
||||||
default: [],
|
default: [],
|
||||||
|
|||||||
@@ -33,3 +33,6 @@ export { default as TeamModel } from "@/db/models/Team.js";
|
|||||||
|
|
||||||
export * from "@/db/models/MaintenanceWindow.js";
|
export * from "@/db/models/MaintenanceWindow.js";
|
||||||
export { default as MaintenanceWindowModel } from "@/db/models/MaintenanceWindow.js";
|
export { default as MaintenanceWindowModel } from "@/db/models/MaintenanceWindow.js";
|
||||||
|
|
||||||
|
export * from "@/db/models/GeoCheck.js";
|
||||||
|
export { default as GeoCheckModel } from "@/db/models/GeoCheck.js";
|
||||||
+6
-2
@@ -2,6 +2,7 @@ import { initializeServices } from "./config/services.js";
|
|||||||
import { initializeControllers } from "./config/controllers.js";
|
import { initializeControllers } from "./config/controllers.js";
|
||||||
import { createApp } from "./app.js";
|
import { createApp } from "./app.js";
|
||||||
import { initShutdownListener } from "./shutdown.js";
|
import { initShutdownListener } from "./shutdown.js";
|
||||||
|
import { validateEnv } from "./validation/envValidation.js";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@@ -15,6 +16,9 @@ const SERVICE_NAME = "Server";
|
|||||||
let logger: ILogger;
|
let logger: ILogger;
|
||||||
|
|
||||||
const startApp = async () => {
|
const startApp = async () => {
|
||||||
|
// Validate environment variables first
|
||||||
|
const env = validateEnv();
|
||||||
|
|
||||||
// FE path
|
// FE path
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -23,7 +27,7 @@ const startApp = async () => {
|
|||||||
|
|
||||||
// Create services
|
// Create services
|
||||||
const settingsRepository = new MongoSettingsRepository();
|
const settingsRepository = new MongoSettingsRepository();
|
||||||
const settingsService = new SettingsService(settingsRepository);
|
const settingsService = new SettingsService(settingsRepository, env);
|
||||||
|
|
||||||
const envSettings = settingsService.loadSettings();
|
const envSettings = settingsService.loadSettings();
|
||||||
|
|
||||||
@@ -46,7 +50,7 @@ const startApp = async () => {
|
|||||||
openApiSpec,
|
openApiSpec,
|
||||||
});
|
});
|
||||||
|
|
||||||
const port = envSettings.port || 52345;
|
const port = 52345;
|
||||||
const server = app.listen(port, () => {
|
const server = app.listen(port, () => {
|
||||||
logger.info({ message: `Server started on port:${port}` });
|
logger.info({ message: `Server started on port:${port}` });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,18 +15,10 @@ import type {
|
|||||||
} from "@/types/index.js";
|
} from "@/types/index.js";
|
||||||
import { CheckModel, MonitorModel, type CheckDocument } from "@/db/models/index.js";
|
import { CheckModel, MonitorModel, type CheckDocument } from "@/db/models/index.js";
|
||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
|
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "StatusService";
|
const SERVICE_NAME = "StatusService";
|
||||||
|
|
||||||
const dateRangeLookup: Record<string, Date | undefined> = {
|
|
||||||
recent: new Date(new Date().setHours(new Date().getHours() - 2)),
|
|
||||||
hour: new Date(new Date().setHours(new Date().getHours() - 1)),
|
|
||||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
|
||||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
|
||||||
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
|
||||||
all: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LatestChecksMap = Record<string, Check[]>;
|
export type LatestChecksMap = Record<string, Check[]>;
|
||||||
type DateRange = { start: Date; end: Date };
|
type DateRange = { start: Date; end: Date };
|
||||||
type HardwareUpChecks = { totalChecks: number };
|
type HardwareUpChecks = { totalChecks: number };
|
||||||
@@ -248,9 +240,9 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
const matchStage: Record<string, any> = {
|
const matchStage: Record<string, any> = {
|
||||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||||
...(typeof status !== "undefined" && { status }),
|
...(typeof status !== "undefined" && { status }),
|
||||||
...(dateRangeLookup[dateRange] && {
|
...(getDateForRange(dateRange) && {
|
||||||
createdAt: {
|
createdAt: {
|
||||||
$gte: dateRangeLookup[dateRange],
|
$gte: getDateForRange(dateRange),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -299,9 +291,9 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => {
|
findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => {
|
||||||
const matchStage: Record<string, any> = {
|
const matchStage: Record<string, any> = {
|
||||||
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
||||||
...(dateRangeLookup[dateRange] && {
|
...(getDateForRange(dateRange) && {
|
||||||
createdAt: {
|
createdAt: {
|
||||||
$gte: dateRangeLookup[dateRange],
|
$gte: getDateForRange(dateRange),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -378,7 +370,7 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
|
return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
|
||||||
}
|
}
|
||||||
if (options?.type === "pagespeed") {
|
if (options?.type === "pagespeed") {
|
||||||
return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate);
|
return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
|
||||||
}
|
}
|
||||||
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString);
|
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString);
|
||||||
};
|
};
|
||||||
@@ -386,9 +378,9 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
findSummaryByTeamId = async (teamId: string, dateRange: string) => {
|
findSummaryByTeamId = async (teamId: string, dateRange: string) => {
|
||||||
const baseMatch = {
|
const baseMatch = {
|
||||||
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
||||||
...(dateRangeLookup[dateRange] && {
|
...(getDateForRange(dateRange) && {
|
||||||
createdAt: {
|
createdAt: {
|
||||||
$gte: dateRangeLookup[dateRange],
|
$gte: getDateForRange(dateRange),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -572,16 +564,50 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
private findPageSpeedDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date) => {
|
private findPageSpeedDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date, dateString: string) => {
|
||||||
const matchStage = {
|
const matchStage = {
|
||||||
"metadata.monitorId": monitorObjectId,
|
"metadata.monitorId": monitorObjectId,
|
||||||
createdAt: { $gte: startDate, $lte: endDate },
|
createdAt: { $gte: startDate, $lte: endDate },
|
||||||
};
|
};
|
||||||
|
|
||||||
const checks = await CheckModel.find(matchStage).sort({ createdAt: -1 }).limit(25).lean();
|
const [result] = await CheckModel.aggregate([
|
||||||
|
{ $match: matchStage },
|
||||||
|
{ $sort: { createdAt: 1 } },
|
||||||
|
{
|
||||||
|
$facet: {
|
||||||
|
groupedChecks: [
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: {
|
||||||
|
$dateToString: { format: dateString, date: "$createdAt" },
|
||||||
|
},
|
||||||
|
avgPerformance: { $avg: "$performance" },
|
||||||
|
avgAccessibility: { $avg: "$accessibility" },
|
||||||
|
avgBestPractices: { $avg: "$bestPractices" },
|
||||||
|
avgSeo: { $avg: "$seo" },
|
||||||
|
totalChecks: { $sum: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $sort: { _id: 1 } },
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
bucketDate: "$_id",
|
||||||
|
performance: "$avgPerformance",
|
||||||
|
accessibility: "$avgAccessibility",
|
||||||
|
bestPractices: "$avgBestPractices",
|
||||||
|
seo: "$avgSeo",
|
||||||
|
totalChecks: 1,
|
||||||
|
_id: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
monitorType: "pagespeed" as const,
|
monitorType: "pagespeed" as const,
|
||||||
checks: this.mapDocuments(checks),
|
groupedChecks: result?.groupedChecks ?? [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type { GeoCheck, GroupedGeoCheck } from "@/types/geoCheck.js";
|
||||||
|
import type { GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js";
|
||||||
|
|
||||||
|
export interface GeoChecksQueryResult {
|
||||||
|
geoChecksCount: number;
|
||||||
|
geoChecks: GeoCheck[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlatGeoChecksQueryResult {
|
||||||
|
geoChecksCount: number;
|
||||||
|
geoChecks: FlatGeoCheck[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGeoChecksRepository {
|
||||||
|
createGeoChecks(geoChecks: Omit<GeoCheck, "id" | "__v" | "createdAt" | "updatedAt">[]): Promise<GeoCheck[]>;
|
||||||
|
findByMonitorId(
|
||||||
|
monitorId: string,
|
||||||
|
sortOrder: string,
|
||||||
|
dateRange: string,
|
||||||
|
page: number,
|
||||||
|
rowsPerPage: number,
|
||||||
|
continents?: GeoContinent[]
|
||||||
|
): Promise<FlatGeoChecksQueryResult>;
|
||||||
|
findByMonitorIdAndDateRange(monitorId: string, startDate: Date, endDate: Date): Promise<GeoCheck[]>;
|
||||||
|
findGroupedByMonitorIdAndDateRange(
|
||||||
|
monitorId: string,
|
||||||
|
startDate: Date,
|
||||||
|
endDate: Date,
|
||||||
|
dateFormat: string,
|
||||||
|
continents?: GeoContinent[]
|
||||||
|
): Promise<GroupedGeoCheck[]>;
|
||||||
|
deleteByMonitorId(monitorId: string): Promise<number>;
|
||||||
|
deleteByTeamId(teamId: string): Promise<number>;
|
||||||
|
deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
import { IGeoChecksRepository } from "./IGeoChecksRepository.js";
|
||||||
|
import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js";
|
||||||
|
import type { GeoChecksQueryResult, FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
|
||||||
|
import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
|
||||||
|
import mongoose from "mongoose";
|
||||||
|
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||||
|
|
||||||
|
const SERVICE_NAME = "GeoChecksRepository";
|
||||||
|
|
||||||
|
class MongoGeoChecksRepository implements IGeoChecksRepository {
|
||||||
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
|
private logger: any;
|
||||||
|
constructor(logger: any) {
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toEntity = (doc: GeoCheckDocument): GeoCheck => {
|
||||||
|
const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => {
|
||||||
|
if (!value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toDateString = (value?: Date | string | null): string => {
|
||||||
|
if (!value) {
|
||||||
|
return new Date(0).toISOString();
|
||||||
|
}
|
||||||
|
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapMetadata = (metadata: any): GeoCheckMetadata => ({
|
||||||
|
monitorId: toStringId(metadata.monitorId),
|
||||||
|
teamId: toStringId(metadata.teamId),
|
||||||
|
type: metadata.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapResults = (results: any[]): GeoCheckResult[] => {
|
||||||
|
if (!results || !Array.isArray(results)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return results.map((result) => ({
|
||||||
|
location: {
|
||||||
|
continent: result.location?.continent ?? "",
|
||||||
|
region: result.location?.region ?? "",
|
||||||
|
country: result.location?.country ?? "",
|
||||||
|
state: result.location?.state ?? "",
|
||||||
|
city: result.location?.city ?? "",
|
||||||
|
longitude: result.location?.longitude ?? 0,
|
||||||
|
latitude: result.location?.latitude ?? 0,
|
||||||
|
},
|
||||||
|
status: result.status ?? false,
|
||||||
|
statusCode: result.statusCode ?? 0,
|
||||||
|
timings: {
|
||||||
|
total: result.timings?.total ?? 0,
|
||||||
|
dns: result.timings?.dns ?? 0,
|
||||||
|
tcp: result.timings?.tcp ?? 0,
|
||||||
|
tls: result.timings?.tls ?? 0,
|
||||||
|
firstByte: result.timings?.firstByte ?? 0,
|
||||||
|
download: result.timings?.download ?? 0,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: toStringId(doc._id),
|
||||||
|
metadata: mapMetadata(doc.metadata),
|
||||||
|
results: mapResults(doc.results),
|
||||||
|
expiry: toDateString(doc.expiry),
|
||||||
|
__v: doc.__v ?? 0,
|
||||||
|
createdAt: toDateString(doc.createdAt),
|
||||||
|
updatedAt: toDateString(doc.updatedAt),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
createGeoChecks = async (geoChecks: Omit<GeoCheck, "id" | "__v" | "createdAt" | "updatedAt">[]): Promise<GeoCheck[]> => {
|
||||||
|
try {
|
||||||
|
const docs = await GeoCheckModel.insertMany(
|
||||||
|
geoChecks.map((geoCheck) => ({
|
||||||
|
metadata: {
|
||||||
|
monitorId: new mongoose.Types.ObjectId(geoCheck.metadata.monitorId),
|
||||||
|
teamId: new mongoose.Types.ObjectId(geoCheck.metadata.teamId),
|
||||||
|
type: geoCheck.metadata.type,
|
||||||
|
},
|
||||||
|
results: geoCheck.results,
|
||||||
|
expiry: new Date(geoCheck.expiry),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
return docs.map((doc) => this.toEntity(doc));
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error({
|
||||||
|
message: `Failed to createGeoChecks: ${error.message}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "createGeoChecks",
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
findByMonitorId = async (
|
||||||
|
monitorId: string,
|
||||||
|
sortOrder: string,
|
||||||
|
dateRange: string,
|
||||||
|
page: number,
|
||||||
|
rowsPerPage: number,
|
||||||
|
continents?: GeoContinent[]
|
||||||
|
): Promise<FlatGeoChecksQueryResult> => {
|
||||||
|
try {
|
||||||
|
const matchStage: Record<string, any> = {
|
||||||
|
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||||
|
...(getDateForRange(dateRange) && {
|
||||||
|
createdAt: {
|
||||||
|
$gte: getDateForRange(dateRange),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertedSortOrder = sortOrder === "asc" ? 1 : -1;
|
||||||
|
|
||||||
|
let skip = 0;
|
||||||
|
if (page && rowsPerPage) {
|
||||||
|
skip = page * rowsPerPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (continents && continents.length > 0) {
|
||||||
|
const pipeline: any[] = [
|
||||||
|
{ $match: matchStage },
|
||||||
|
{ $unwind: "$results" },
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
"results.location.continent": { $in: continents },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
const pipeline: any[] = [{ $match: matchStage }, { $unwind: "$results" }];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common pipeline stages for both paths
|
||||||
|
const pipeline: any[] = [
|
||||||
|
{ $match: matchStage },
|
||||||
|
{ $unwind: "$results" },
|
||||||
|
// Filter by continent if specified
|
||||||
|
...(continents && continents.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
"results.location.continent": { $in: continents },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
// Project to flat structure
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 0,
|
||||||
|
monitorId: "$metadata.monitorId",
|
||||||
|
teamId: "$metadata.teamId",
|
||||||
|
type: "$metadata.type",
|
||||||
|
location: "$results.location",
|
||||||
|
status: "$results.status",
|
||||||
|
statusCode: "$results.statusCode",
|
||||||
|
timings: "$results.timings",
|
||||||
|
createdAt: 1,
|
||||||
|
updatedAt: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $sort: { createdAt: convertedSortOrder } },
|
||||||
|
{ $skip: skip },
|
||||||
|
{ $limit: rowsPerPage },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Count pipeline
|
||||||
|
const countPipeline: any[] = [
|
||||||
|
{ $match: matchStage },
|
||||||
|
{ $unwind: "$results" },
|
||||||
|
...(continents && continents.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
"results.location.continent": { $in: continents },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{ $count: "count" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const [countResult, dataResults] = await Promise.all([GeoCheckModel.aggregate(countPipeline), GeoCheckModel.aggregate(pipeline)]);
|
||||||
|
|
||||||
|
const geoChecksCount = countResult[0]?.count || 0;
|
||||||
|
const geoChecks: FlatGeoCheck[] = dataResults.map((doc) => ({
|
||||||
|
id: `${doc.monitorId.toString()}-${new Date(doc.createdAt).getTime()}-${doc.location.continent}-${doc.location.city}-${Math.random().toString(36).substring(2, 15)}`,
|
||||||
|
monitorId: doc.monitorId.toString(),
|
||||||
|
teamId: doc.teamId.toString(),
|
||||||
|
type: doc.type,
|
||||||
|
location: doc.location,
|
||||||
|
status: doc.status,
|
||||||
|
statusCode: doc.statusCode,
|
||||||
|
timings: doc.timings,
|
||||||
|
createdAt: new Date(doc.createdAt).toISOString(),
|
||||||
|
updatedAt: new Date(doc.updatedAt).toISOString(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { geoChecksCount, geoChecks };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error({
|
||||||
|
message: `Error finding geo checks by monitor ID: ${error.message}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "findByMonitorId",
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
findByMonitorIdAndDateRange = async (monitorId: string, startDate: Date, endDate: Date): Promise<GeoCheck[]> => {
|
||||||
|
try {
|
||||||
|
const docs = await GeoCheckModel.find({
|
||||||
|
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||||
|
createdAt: {
|
||||||
|
$gte: startDate,
|
||||||
|
$lte: endDate,
|
||||||
|
},
|
||||||
|
}).sort({ createdAt: -1 });
|
||||||
|
return docs.map(this.toEntity);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error({
|
||||||
|
message: `Error finding geo checks by monitor ID and date range: ${error.message}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "findByMonitorIdAndDateRange",
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
findGroupedByMonitorIdAndDateRange = async (
|
||||||
|
monitorId: string,
|
||||||
|
startDate: Date,
|
||||||
|
endDate: Date,
|
||||||
|
dateFormat: string,
|
||||||
|
continents?: GeoContinent[]
|
||||||
|
): Promise<GroupedGeoCheck[]> => {
|
||||||
|
try {
|
||||||
|
const pipeline: any[] = [
|
||||||
|
// Match geo checks for this monitor in date range
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||||
|
createdAt: {
|
||||||
|
$gte: startDate,
|
||||||
|
$lte: endDate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Unwind the results array to process each location separately
|
||||||
|
{
|
||||||
|
$unwind: "$results",
|
||||||
|
},
|
||||||
|
// Filter by continent if specified
|
||||||
|
...(continents && continents.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
"results.location.continent": { $in: continents },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
// Group by date bucket and continent
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: {
|
||||||
|
bucketDate: {
|
||||||
|
$dateToString: {
|
||||||
|
format: dateFormat,
|
||||||
|
date: "$createdAt",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
continent: "$results.location.continent",
|
||||||
|
},
|
||||||
|
avgResponseTime: { $avg: "$results.timings.total" },
|
||||||
|
totalChecks: { $sum: 1 },
|
||||||
|
upChecks: {
|
||||||
|
$sum: {
|
||||||
|
$cond: ["$results.status", 1, 0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Calculate uptime percentage
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 0,
|
||||||
|
bucketDate: "$_id.bucketDate",
|
||||||
|
continent: "$_id.continent",
|
||||||
|
avgResponseTime: { $round: ["$avgResponseTime", 2] },
|
||||||
|
totalChecks: 1,
|
||||||
|
uptimePercentage: {
|
||||||
|
$round: [
|
||||||
|
{
|
||||||
|
$multiply: [
|
||||||
|
{
|
||||||
|
$divide: ["$upChecks", "$totalChecks"],
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Sort by date and continent
|
||||||
|
{
|
||||||
|
$sort: {
|
||||||
|
bucketDate: 1,
|
||||||
|
continent: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = await GeoCheckModel.aggregate(pipeline);
|
||||||
|
return results as GroupedGeoCheck[];
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error({
|
||||||
|
message: `Error finding grouped geo checks: ${error.message}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "findGroupedByMonitorIdAndDateRange",
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteByMonitorId = async (monitorId: string): Promise<number> => {
|
||||||
|
try {
|
||||||
|
const result = await GeoCheckModel.deleteMany({
|
||||||
|
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||||
|
});
|
||||||
|
return result.deletedCount || 0;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error({
|
||||||
|
message: `Error deleting geo checks by monitor ID: ${error.message}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "deleteByMonitorId",
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteByTeamId = async (teamId: string): Promise<number> => {
|
||||||
|
try {
|
||||||
|
const result = await GeoCheckModel.deleteMany({
|
||||||
|
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
||||||
|
});
|
||||||
|
return result.deletedCount || 0;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error({
|
||||||
|
message: `Error deleting geo checks by team ID: ${error.message}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "deleteByTeamId",
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise<number> => {
|
||||||
|
try {
|
||||||
|
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||||
|
const result = await GeoCheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } });
|
||||||
|
return result.deletedCount || 0;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error({
|
||||||
|
message: `Error deleting orphaned geo checks: ${error.message}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "deleteByMonitorIdsNotIn",
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MongoGeoChecksRepository };
|
||||||
|
export default MongoGeoChecksRepository;
|
||||||
@@ -33,3 +33,6 @@ export { default as MongoTeamsRepository } from "@/repositories/teams/MongoTeams
|
|||||||
|
|
||||||
export * from "@/repositories/maintenance-windows/IMaintenanceWindowsRepository.js";
|
export * from "@/repositories/maintenance-windows/IMaintenanceWindowsRepository.js";
|
||||||
export { default as MongoMaintenanceWindowsRepository } from "@/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.js";
|
export { default as MongoMaintenanceWindowsRepository } from "@/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.js";
|
||||||
|
|
||||||
|
export * from "@/repositories/geo-checks/IGeoChecksRepository.js";
|
||||||
|
export { default as MongoGeoChecksRepository } from "@/repositories/geo-checks/MongoGeoChecksRepository.js";
|
||||||
@@ -36,7 +36,7 @@ class MongoInvitesRepository implements IInvitesRepository {
|
|||||||
create = async (invite: Partial<Invite>) => {
|
create = async (invite: Partial<Invite>) => {
|
||||||
await InviteModel.deleteMany({ email: invite.email });
|
await InviteModel.deleteMany({ email: invite.email });
|
||||||
invite.token = crypto.randomBytes(32).toString("hex");
|
invite.token = crypto.randomBytes(32).toString("hex");
|
||||||
let inviteToken = await InviteModel.create(invite);
|
const inviteToken = await InviteModel.create(invite);
|
||||||
return this.toEntity(inviteToken);
|
return this.toEntity(inviteToken);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
let sort: Record<string, any> = {};
|
const sort: Record<string, any> = {};
|
||||||
if (field !== undefined && order !== undefined) {
|
if (field !== undefined && order !== undefined) {
|
||||||
sort[field] = order === "asc" ? 1 : -1;
|
sort[field] = order === "asc" ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface SummaryConfig {
|
|||||||
export interface IMonitorsRepository {
|
export interface IMonitorsRepository {
|
||||||
// create
|
// create
|
||||||
create(monitor: Monitor, teamId: string, userId: string): Promise<Monitor | null>;
|
create(monitor: Monitor, teamId: string, userId: string): Promise<Monitor | null>;
|
||||||
createBulkMonitors(monitors: Monitor[]): Promise<Monitor[]>;
|
createMonitors(monitors: Monitor[]): Promise<Monitor[]>;
|
||||||
// single fetch
|
// single fetch
|
||||||
findById(monitorId: string, teamId: string): Promise<Monitor>;
|
findById(monitorId: string, teamId: string): Promise<Monitor>;
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,23 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
|||||||
return this.toEntity(saved);
|
return this.toEntity(saved);
|
||||||
};
|
};
|
||||||
|
|
||||||
createBulkMonitors = async (monitors: Monitor[]): Promise<Monitor[]> => {
|
createMonitors = async (monitors: Monitor[]): Promise<Monitor[]> => {
|
||||||
if (!monitors.length) {
|
if (!monitors.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const payload = monitors.map((monitor) => ({ ...monitor, notifications: undefined }));
|
const payload = monitors.map((monitor) => ({ ...monitor, notifications: undefined }));
|
||||||
const inserted = await MonitorModel.insertMany(payload, { ordered: false });
|
try {
|
||||||
return this.mapDocuments(inserted);
|
const inserted = await MonitorModel.insertMany(payload, { ordered: false });
|
||||||
|
return this.mapDocuments(inserted);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.name === "MongoBulkWriteError" || error.name === "BulkWriteError") {
|
||||||
|
const insertedDocs = error.insertedDocs || [];
|
||||||
|
if (insertedDocs.length > 0) {
|
||||||
|
return this.mapDocuments(insertedDocs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
findById = async (monitorId: string, teamId: string): Promise<Monitor> => {
|
findById = async (monitorId: string, teamId: string): Promise<Monitor> => {
|
||||||
@@ -342,8 +352,12 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
|||||||
tempAlertCounter: doc.tempAlertCounter,
|
tempAlertCounter: doc.tempAlertCounter,
|
||||||
selectedDisks: doc.selectedDisks ?? [],
|
selectedDisks: doc.selectedDisks ?? [],
|
||||||
gameId: doc.gameId ?? undefined,
|
gameId: doc.gameId ?? undefined,
|
||||||
|
grpcServiceName: doc.grpcServiceName ?? undefined,
|
||||||
group: doc.group ?? null,
|
group: doc.group ?? null,
|
||||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
||||||
|
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||||
|
geoCheckLocations: doc.geoCheckLocations ?? [],
|
||||||
|
geoCheckInterval: doc.geoCheckInterval ?? 300000,
|
||||||
createdAt: toDateString(doc.createdAt),
|
createdAt: toDateString(doc.createdAt),
|
||||||
updatedAt: toDateString(doc.updatedAt),
|
updatedAt: toDateString(doc.updatedAt),
|
||||||
};
|
};
|
||||||
@@ -397,8 +411,12 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
|||||||
tempAlertCounter: doc.tempAlertCounter,
|
tempAlertCounter: doc.tempAlertCounter,
|
||||||
selectedDisks: doc.selectedDisks ?? [],
|
selectedDisks: doc.selectedDisks ?? [],
|
||||||
gameId: doc.gameId ?? undefined,
|
gameId: doc.gameId ?? undefined,
|
||||||
|
grpcServiceName: doc.grpcServiceName ?? undefined,
|
||||||
group: doc.group ?? null,
|
group: doc.group ?? null,
|
||||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
||||||
|
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||||
|
geoCheckLocations: doc.geoCheckLocations ?? [],
|
||||||
|
geoCheckInterval: doc.geoCheckInterval ?? 300000,
|
||||||
createdAt: toDateString(doc.createdAt),
|
createdAt: toDateString(doc.createdAt),
|
||||||
updatedAt: toDateString(doc.updatedAt),
|
updatedAt: toDateString(doc.updatedAt),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { NotificationModel, type NotificationDocument } from "@/db/models/index.
|
|||||||
import { INotificationsRepository } from "@/repositories/index.js";
|
import { INotificationsRepository } from "@/repositories/index.js";
|
||||||
import type { Notification } from "@/types/index.js";
|
import type { Notification } from "@/types/index.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { not } from "joi";
|
|
||||||
|
|
||||||
class MongoNotificationsRepository implements INotificationsRepository {
|
class MongoNotificationsRepository implements INotificationsRepository {
|
||||||
private mapDocuments = (documents: NotificationDocument[]): Notification[] => {
|
private mapDocuments = (documents: NotificationDocument[]): Notification[] => {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class MongoSettingsRepository implements ISettingsRepository {
|
|||||||
};
|
};
|
||||||
|
|
||||||
findSingleton = async () => {
|
findSingleton = async () => {
|
||||||
let settings = await AppSettingsModel.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean();
|
const settings = await AppSettingsModel.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean();
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
|
||||||
|
class GeoCheckRoutes {
|
||||||
|
private router: Router;
|
||||||
|
private geoCheckController: any;
|
||||||
|
|
||||||
|
constructor(geoCheckController: any) {
|
||||||
|
this.router = Router();
|
||||||
|
this.geoCheckController = geoCheckController;
|
||||||
|
this.initRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
initRoutes() {
|
||||||
|
this.router.get("/:monitorId", this.geoCheckController.getGeoChecksByMonitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRouter() {
|
||||||
|
return this.router;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GeoCheckRoutes;
|
||||||
@@ -26,9 +26,13 @@ class MonitorRoutes {
|
|||||||
|
|
||||||
// Hardware routes
|
// Hardware routes
|
||||||
this.router.get("/hardware/details/:monitorId", this.monitorController.getHardwareDetailsById);
|
this.router.get("/hardware/details/:monitorId", this.monitorController.getHardwareDetailsById);
|
||||||
|
|
||||||
// PageSpeed routes
|
// PageSpeed routes
|
||||||
this.router.get("/pagespeed/details/:monitorId", this.monitorController.getPageSpeedDetailsById);
|
this.router.get("/pagespeed/details/:monitorId", this.monitorController.getPageSpeedDetailsById);
|
||||||
|
|
||||||
|
// Geo checks routes
|
||||||
|
this.router.get("/:monitorId/geo-checks", this.monitorController.getGeoChecksByMonitorId);
|
||||||
|
|
||||||
// General monitor routes
|
// General monitor routes
|
||||||
this.router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.pauseMonitor);
|
this.router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.pauseMonitor);
|
||||||
|
|
||||||
@@ -44,7 +48,8 @@ class MonitorRoutes {
|
|||||||
// Other static routes
|
// Other static routes
|
||||||
this.router.post("/demo", isAllowed(["admin", "superadmin"]), this.monitorController.addDemoMonitors);
|
this.router.post("/demo", isAllowed(["admin", "superadmin"]), this.monitorController.addDemoMonitors);
|
||||||
this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON);
|
this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON);
|
||||||
this.router.post("/bulk", isAllowed(["admin", "superadmin"]), upload.single("csvFile"), this.monitorController.createBulkMonitors);
|
this.router.post("/import/json", isAllowed(["admin", "superadmin"]), this.monitorController.importMonitorsFromJSON);
|
||||||
|
|
||||||
this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail);
|
this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail);
|
||||||
this.router.get("/games", this.monitorController.getAllGames);
|
this.router.get("/games", this.monitorController.getAllGames);
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js";
|
import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js";
|
||||||
import type { MonitorType, MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js";
|
import type { MonitorStatusResponse, CheckErrorInfo, Check, ILighthouseAudit, ChecksQueryResult, ChecksSummary } from "@/types/index.js";
|
||||||
import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js";
|
import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { ParseBoolean } from "@/utils/utils.js";
|
import { ParseBoolean } from "@/utils/utils.js";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "checkService";
|
const SERVICE_NAME = "checkService";
|
||||||
|
|
||||||
class CheckService {
|
export interface ICheckService {
|
||||||
|
createChecks(checks: Check[]): Promise<Check[]>;
|
||||||
|
buildCheck(statusResponse: MonitorStatusResponse<PageSpeedStatusPayload | HardwareStatusPayload | undefined>): Partial<Check> | undefined;
|
||||||
|
getChecksByMonitor(params: { monitorId: string; query: Record<string, string>; teamId: string }): Promise<ChecksQueryResult>;
|
||||||
|
getChecksByTeam(params: { teamId: string; query: Record<string, string> }): Promise<ChecksQueryResult>;
|
||||||
|
getChecksSummaryByTeamId(params: { teamId: string; dateRange: string }): Promise<ChecksSummary>;
|
||||||
|
deleteChecks(params: { monitorId: string; teamId: string }): Promise<number>;
|
||||||
|
deleteChecksByTeamId(params: { teamId: string }): Promise<number>;
|
||||||
|
updateChecksTTL(params: { teamId: string; ttl: string }): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CheckService implements ICheckService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
private checksRepository: IChecksRepository;
|
private checksRepository: IChecksRepository;
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
constructor({
|
constructor(monitorsRepository: IMonitorsRepository, logger: ILogger, checksRepository: IChecksRepository) {
|
||||||
monitorsRepository,
|
|
||||||
logger,
|
|
||||||
checksRepository,
|
|
||||||
}: {
|
|
||||||
monitorsRepository: IMonitorsRepository;
|
|
||||||
logger: any;
|
|
||||||
checksRepository: IChecksRepository;
|
|
||||||
}) {
|
|
||||||
this.monitorsRepository = monitorsRepository;
|
this.monitorsRepository = monitorsRepository;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.checksRepository = checksRepository;
|
this.checksRepository = checksRepository;
|
||||||
@@ -61,13 +64,12 @@ class CheckService {
|
|||||||
message: "Failed to build check",
|
message: "Failed to build check",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "buildCheck",
|
method: "buildCheck",
|
||||||
details: "empty payload",
|
|
||||||
});
|
});
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const categories = pageSpeedPayload.lighthouseResult?.categories ?? {};
|
const categories = pageSpeedPayload.lighthouseResult?.categories ?? {};
|
||||||
const audits = pageSpeedPayload.lighthouseResult?.audits ?? {};
|
const audits = pageSpeedPayload.lighthouseResult?.audits ?? {};
|
||||||
const mapAudit = (audit: any) => {
|
const mapAudit = (audit: ILighthouseAudit | undefined) => {
|
||||||
if (!audit || typeof audit !== "object") {
|
if (!audit || typeof audit !== "object") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -110,7 +112,7 @@ class CheckService {
|
|||||||
return check;
|
return check;
|
||||||
};
|
};
|
||||||
|
|
||||||
getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => {
|
getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: Record<string, string>; teamId: string }) => {
|
||||||
if (!monitorId) {
|
if (!monitorId) {
|
||||||
throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
|
throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
|
||||||
}
|
}
|
||||||
@@ -118,28 +120,34 @@ class CheckService {
|
|||||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
|
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// For verificaiton, throws an error if monitor doesn't belong to team
|
// For verification, throws an error if monitor doesn't belong to team
|
||||||
await this.monitorsRepository.findById(monitorId, teamId);
|
await this.monitorsRepository.findById(monitorId, teamId);
|
||||||
|
|
||||||
let { sortOrder, dateRange, filter, page, rowsPerPage, status } = query;
|
const { sortOrder, dateRange, filter, page, rowsPerPage, status } = query;
|
||||||
|
if (!sortOrder || !dateRange || !filter || !page || !rowsPerPage) {
|
||||||
|
throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
|
||||||
|
}
|
||||||
const parsedStatus = typeof status === "undefined" ? status : ParseBoolean(status);
|
const parsedStatus = typeof status === "undefined" ? status : ParseBoolean(status);
|
||||||
const parsedPage = page ? parseInt(page) : page;
|
const parsedPage = page ? parseInt(page) : 0;
|
||||||
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
|
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5;
|
||||||
|
|
||||||
const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus);
|
const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
getChecksByTeam = async ({ teamId, query }: { teamId: string; query: any }) => {
|
getChecksByTeam = async ({ teamId, query }: { teamId: string; query: Record<string, string> }) => {
|
||||||
let { sortOrder, dateRange, filter, page, rowsPerPage } = query;
|
const { sortOrder, dateRange, filter, page, rowsPerPage } = query;
|
||||||
|
if (!sortOrder || !dateRange || !filter || !page || !rowsPerPage) {
|
||||||
|
throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
if (!teamId) {
|
if (!teamId) {
|
||||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 });
|
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedPage = page ? parseInt(page) : page;
|
const parsedPage = page ? parseInt(page) : 0;
|
||||||
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
|
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5;
|
||||||
|
|
||||||
const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId);
|
const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId);
|
||||||
return checkData;
|
return checkData;
|
||||||
@@ -162,7 +170,7 @@ class CheckService {
|
|||||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 });
|
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// For verificaiton, throws an error if monitor doesn't belong to team
|
// For verification, throws an error if monitor doesn't belong to team
|
||||||
await this.monitorsRepository.findById(monitorId, teamId);
|
await this.monitorsRepository.findById(monitorId, teamId);
|
||||||
|
|
||||||
const deletedCount = await this.checksRepository.deleteByMonitorId(monitorId);
|
const deletedCount = await this.checksRepository.deleteByMonitorId(monitorId);
|
||||||
@@ -178,7 +186,7 @@ class CheckService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => {
|
updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => {
|
||||||
throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500 });
|
throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import type { Monitor, GeoCheck } from "@/types/index.js";
|
||||||
|
import type { GeoCheckResult } from "@/types/geoCheck.js";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import type { IGeoChecksRepository } from "@/repositories/index.js";
|
||||||
|
import type { IMonitorsRepository } from "@/repositories/index.js";
|
||||||
|
import type { IGlobalPingService } from "@/service/infrastructure/globalPingService.js";
|
||||||
|
import type { ILogger } from "@/utils/logger.js";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
|
||||||
|
const SERVICE_NAME = "GeoChecksService";
|
||||||
|
|
||||||
|
export interface IGeoChecksService {
|
||||||
|
readonly serviceName: string;
|
||||||
|
buildGeoCheck(monitor: Monitor): Promise<GeoCheck | null>;
|
||||||
|
createGeoChecks(geoChecks: GeoCheck[]): Promise<GeoCheck[]>;
|
||||||
|
getGeoChecksByMonitor(args: { monitorId: string; query: any; teamId: string }): Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeoChecksService implements IGeoChecksService {
|
||||||
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
|
private logger: ILogger;
|
||||||
|
private geoChecksRepository: IGeoChecksRepository;
|
||||||
|
private globalPingService: IGlobalPingService;
|
||||||
|
private monitorsRepository: IMonitorsRepository;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
logger,
|
||||||
|
geoChecksRepository,
|
||||||
|
globalPingService,
|
||||||
|
monitorsRepository,
|
||||||
|
}: {
|
||||||
|
logger: ILogger;
|
||||||
|
geoChecksRepository: IGeoChecksRepository;
|
||||||
|
globalPingService: IGlobalPingService;
|
||||||
|
monitorsRepository: IMonitorsRepository;
|
||||||
|
}) {
|
||||||
|
this.logger = logger;
|
||||||
|
this.geoChecksRepository = geoChecksRepository;
|
||||||
|
this.globalPingService = globalPingService;
|
||||||
|
this.monitorsRepository = monitorsRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
get serviceName() {
|
||||||
|
return GeoChecksService.SERVICE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
async buildGeoCheck(monitor: Monitor): Promise<GeoCheck | null> {
|
||||||
|
try {
|
||||||
|
if (!monitor.url) {
|
||||||
|
this.logger.warn({
|
||||||
|
message: "Monitor missing URL for geo check",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "buildGeoCheck",
|
||||||
|
details: { monitorId: monitor.id },
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) {
|
||||||
|
this.logger.warn({
|
||||||
|
message: "Monitor missing geo check locations",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "buildGeoCheck",
|
||||||
|
details: { monitorId: monitor.id },
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Create measurement request
|
||||||
|
const measurementId = await this.globalPingService.createMeasurement(monitor.url, monitor.geoCheckLocations);
|
||||||
|
|
||||||
|
if (!measurementId) {
|
||||||
|
// GlobalPing API is down, skip this check
|
||||||
|
this.logger.debug({
|
||||||
|
message: "Skipping geo check due to API unavailability",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "buildGeoCheck",
|
||||||
|
details: { monitorId: monitor.id },
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Poll for results
|
||||||
|
const results = await this.globalPingService.pollForResults(measurementId);
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
// No successful results (all locations timed out or failed)
|
||||||
|
this.logger.debug({
|
||||||
|
message: "No successful geo check results",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "buildGeoCheck",
|
||||||
|
details: { monitorId: monitor.id, measurementId },
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Build GeoCheck document
|
||||||
|
const geoCheck = this.createGeoCheckDocument(monitor, results);
|
||||||
|
|
||||||
|
this.logger.debug({
|
||||||
|
message: `Geo check completed for monitor ${monitor.id}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "buildGeoCheck",
|
||||||
|
details: { monitorId: monitor.id, resultsCount: results.length },
|
||||||
|
});
|
||||||
|
|
||||||
|
return geoCheck;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error({
|
||||||
|
message: "Error executing geo check",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "buildGeoCheck",
|
||||||
|
details: { monitorId: monitor.id, error: error.message },
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createGeoCheckDocument(monitor: Monitor, results: GeoCheckResult[]): GeoCheck {
|
||||||
|
const now = new Date();
|
||||||
|
const ttl = 90 * 24 * 60 * 60 * 1000; // 90 days in ms
|
||||||
|
const expiryDate = new Date(now.getTime() + ttl);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: new Types.ObjectId().toString(),
|
||||||
|
metadata: {
|
||||||
|
monitorId: monitor.id,
|
||||||
|
teamId: monitor.teamId,
|
||||||
|
type: monitor.type,
|
||||||
|
},
|
||||||
|
results,
|
||||||
|
expiry: expiryDate.toISOString(),
|
||||||
|
__v: 0,
|
||||||
|
createdAt: now.toISOString(),
|
||||||
|
updatedAt: now.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
createGeoChecks = async (geoChecks: GeoCheck[]) => {
|
||||||
|
return this.geoChecksRepository.createGeoChecks(geoChecks);
|
||||||
|
};
|
||||||
|
|
||||||
|
getGeoChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => {
|
||||||
|
if (!monitorId) {
|
||||||
|
throw new AppError({
|
||||||
|
message: "No monitor ID in request",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getGeoChecksByMonitor",
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!teamId) {
|
||||||
|
throw new AppError({
|
||||||
|
message: "No team ID in request",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getGeoChecksByMonitor",
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||||
|
if (!monitor) {
|
||||||
|
throw new AppError({
|
||||||
|
message: `Monitor with ID ${monitorId} not found.`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getGeoChecksByMonitor",
|
||||||
|
status: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sortOrder, dateRange, page, rowsPerPage, continent } = query;
|
||||||
|
const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined;
|
||||||
|
|
||||||
|
this.logger.debug({
|
||||||
|
message: "getGeoChecksByMonitor query params",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getGeoChecksByMonitor",
|
||||||
|
details: { continent, continents, query },
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsedPage = page ? parseInt(page) : page;
|
||||||
|
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
|
||||||
|
|
||||||
|
const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GeoChecksService;
|
||||||
@@ -3,42 +3,29 @@ import type { Monitor } from "@/types/monitor.js";
|
|||||||
import type { MonitorStatusResponse } from "@/types/network.js";
|
import type { MonitorStatusResponse } from "@/types/network.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { ParseBoolean } from "@/utils/utils.js";
|
import { ParseBoolean } from "@/utils/utils.js";
|
||||||
|
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||||
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
|
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
|
||||||
import type { Incident } from "@/types/index.js";
|
import type { Incident } from "@/types/index.js";
|
||||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||||
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
|
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
|
||||||
|
import type { ILogger } from "@/utils/logger.js";
|
||||||
const dateRangeLookup: Record<string, Date | undefined> = {
|
|
||||||
recent: new Date(new Date().setHours(new Date().getHours() - 2)),
|
|
||||||
hour: new Date(new Date().setHours(new Date().getHours() - 1)),
|
|
||||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
|
||||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
|
||||||
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
|
||||||
all: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
class IncidentService {
|
class IncidentService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
private incidentsRepository: IIncidentsRepository;
|
private incidentsRepository: IIncidentsRepository;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
private usersRepository: IUsersRepository;
|
private usersRepository: IUsersRepository;
|
||||||
private notificationMessageBuilder: INotificationMessageBuilder;
|
private notificationMessageBuilder: INotificationMessageBuilder;
|
||||||
|
|
||||||
constructor({
|
constructor(
|
||||||
logger,
|
logger: ILogger,
|
||||||
incidentsRepository,
|
incidentsRepository: IIncidentsRepository,
|
||||||
monitorsRepository,
|
monitorsRepository: IMonitorsRepository,
|
||||||
usersRepository,
|
usersRepository: IUsersRepository,
|
||||||
notificationMessageBuilder,
|
notificationMessageBuilder: INotificationMessageBuilder
|
||||||
}: {
|
) {
|
||||||
logger: any;
|
|
||||||
incidentsRepository: IIncidentsRepository;
|
|
||||||
monitorsRepository: IMonitorsRepository;
|
|
||||||
usersRepository: IUsersRepository;
|
|
||||||
notificationMessageBuilder: INotificationMessageBuilder;
|
|
||||||
}) {
|
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.incidentsRepository = incidentsRepository;
|
this.incidentsRepository = incidentsRepository;
|
||||||
this.monitorsRepository = monitorsRepository;
|
this.monitorsRepository = monitorsRepository;
|
||||||
@@ -151,7 +138,7 @@ class IncidentService {
|
|||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "resolveIncidentManually",
|
method: "resolveIncidentManually",
|
||||||
message: `Incident manually resolved by user`,
|
message: `Incident manually resolved by user`,
|
||||||
details: resolvedIncident.id,
|
details: { incidentId: resolvedIncident.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
return resolvedIncident;
|
return resolvedIncident;
|
||||||
@@ -160,7 +147,7 @@ class IncidentService {
|
|||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "resolveIncident",
|
method: "resolveIncident",
|
||||||
message: error.message,
|
message: error.message,
|
||||||
details: incidentId,
|
details: { id: incidentId },
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
@@ -182,7 +169,7 @@ class IncidentService {
|
|||||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getIncidentsByTeam", status: 400 });
|
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getIncidentsByTeam", status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const startDate = dateRangeLookup[dateRange];
|
const startDate = getDateForRange(dateRange);
|
||||||
|
|
||||||
const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0;
|
const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0;
|
||||||
const parsedRowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20;
|
const parsedRowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20;
|
||||||
@@ -207,7 +194,7 @@ class IncidentService {
|
|||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getIncidentsByTeam",
|
method: "getIncidentsByTeam",
|
||||||
message: error.message,
|
message: error.message,
|
||||||
details: teamId,
|
details: { teamId },
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
@@ -229,7 +216,7 @@ class IncidentService {
|
|||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getIncidentSummary",
|
method: "getIncidentSummary",
|
||||||
message: error.message,
|
message: error.message,
|
||||||
details: teamId,
|
details: { teamId },
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
@@ -250,7 +237,7 @@ class IncidentService {
|
|||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "getIncidentById",
|
method: "getIncidentById",
|
||||||
message: error.message,
|
message: error.message,
|
||||||
details: incidentId,
|
details: { incidentId },
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import type {
|
|||||||
PageSpeedDetailsResult,
|
PageSpeedDetailsResult,
|
||||||
GamesMap,
|
GamesMap,
|
||||||
} from "@/types/monitor.js";
|
} from "@/types/monitor.js";
|
||||||
|
import type { GeoContinent } from "@/types/geoCheck.js";
|
||||||
import type {
|
import type {
|
||||||
IChecksRepository,
|
IChecksRepository,
|
||||||
|
IGeoChecksRepository,
|
||||||
IIncidentsRepository,
|
IIncidentsRepository,
|
||||||
IMonitorsRepository,
|
IMonitorsRepository,
|
||||||
IMonitorStatsRepository,
|
IMonitorStatsRepository,
|
||||||
@@ -29,13 +31,14 @@ export interface IMonitorService {
|
|||||||
|
|
||||||
// create
|
// create
|
||||||
createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
|
createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
|
||||||
createBulkMonitors(monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null>;
|
createMonitors(monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null>;
|
||||||
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
|
addDemoMonitors(args: { userId: string; teamId: string }): Promise<Monitor[]>;
|
||||||
|
|
||||||
// read
|
// read
|
||||||
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<UptimeDetailsResult>;
|
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<UptimeDetailsResult>;
|
||||||
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<HardwareDetailsResult>;
|
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<HardwareDetailsResult>;
|
||||||
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<PageSpeedDetailsResult>;
|
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<PageSpeedDetailsResult>;
|
||||||
|
getGeoChecksByMonitorId(args: { teamId: string; monitorId: string; dateRange: string; continents?: GeoContinent[] }): Promise<any>;
|
||||||
getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
||||||
getMonitorsByTeamId(args: {
|
getMonitorsByTeamId(args: {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@@ -72,6 +75,7 @@ export interface IMonitorService {
|
|||||||
// other
|
// other
|
||||||
sendTestEmail(args: { to: string }): Promise<string>;
|
sendTestEmail(args: { to: string }): Promise<string>;
|
||||||
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
|
exportMonitorsToJSON(args: { teamId: string }): Promise<Monitor[]>;
|
||||||
|
importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: any[] }): Promise<{ imported: number; errors: string[] }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MonitorService implements IMonitorService {
|
export class MonitorService implements IMonitorService {
|
||||||
@@ -83,6 +87,7 @@ export class MonitorService implements IMonitorService {
|
|||||||
private games: any;
|
private games: any;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
private checksRepository: IChecksRepository;
|
private checksRepository: IChecksRepository;
|
||||||
|
private geoChecksRepository: IGeoChecksRepository;
|
||||||
private monitorStatsRepository: IMonitorStatsRepository;
|
private monitorStatsRepository: IMonitorStatsRepository;
|
||||||
private statusPagesRepository: IStatusPagesRepository;
|
private statusPagesRepository: IStatusPagesRepository;
|
||||||
private incidentsRepository: IIncidentsRepository;
|
private incidentsRepository: IIncidentsRepository;
|
||||||
@@ -94,6 +99,7 @@ export class MonitorService implements IMonitorService {
|
|||||||
games,
|
games,
|
||||||
monitorsRepository,
|
monitorsRepository,
|
||||||
checksRepository,
|
checksRepository,
|
||||||
|
geoChecksRepository,
|
||||||
monitorStatsRepository,
|
monitorStatsRepository,
|
||||||
statusPagesRepository,
|
statusPagesRepository,
|
||||||
incidentsRepository,
|
incidentsRepository,
|
||||||
@@ -104,6 +110,7 @@ export class MonitorService implements IMonitorService {
|
|||||||
games: any;
|
games: any;
|
||||||
monitorsRepository: IMonitorsRepository;
|
monitorsRepository: IMonitorsRepository;
|
||||||
checksRepository: IChecksRepository;
|
checksRepository: IChecksRepository;
|
||||||
|
geoChecksRepository: IGeoChecksRepository;
|
||||||
monitorStatsRepository: IMonitorStatsRepository;
|
monitorStatsRepository: IMonitorStatsRepository;
|
||||||
statusPagesRepository: IStatusPagesRepository;
|
statusPagesRepository: IStatusPagesRepository;
|
||||||
incidentsRepository: IIncidentsRepository;
|
incidentsRepository: IIncidentsRepository;
|
||||||
@@ -114,6 +121,7 @@ export class MonitorService implements IMonitorService {
|
|||||||
this.games = games;
|
this.games = games;
|
||||||
this.monitorsRepository = monitorsRepository;
|
this.monitorsRepository = monitorsRepository;
|
||||||
this.checksRepository = checksRepository;
|
this.checksRepository = checksRepository;
|
||||||
|
this.geoChecksRepository = geoChecksRepository;
|
||||||
this.monitorStatsRepository = monitorStatsRepository;
|
this.monitorStatsRepository = monitorStatsRepository;
|
||||||
this.statusPagesRepository = statusPagesRepository;
|
this.statusPagesRepository = statusPagesRepository;
|
||||||
this.incidentsRepository = incidentsRepository;
|
this.incidentsRepository = incidentsRepository;
|
||||||
@@ -157,10 +165,10 @@ export class MonitorService implements IMonitorService {
|
|||||||
this.jobQueue.addJob(monitor.id, monitor);
|
this.jobQueue.addJob(monitor.id, monitor);
|
||||||
};
|
};
|
||||||
|
|
||||||
createBulkMonitors = async (monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null> => {
|
createMonitors = async (monitors: Array<Monitor>, userId: string, teamId: string): Promise<Monitor[] | null> => {
|
||||||
const createdMonitors = await this.monitorsRepository.createBulkMonitors(monitors);
|
const createdMonitors = await this.monitorsRepository.createMonitors(monitors);
|
||||||
if (!monitors || monitors.length === 0) {
|
if (!monitors || monitors.length === 0) {
|
||||||
throw new AppError({ message: "Failed to create monitors", status: 500, service: SERVICE_NAME, method: "createBulkMonitors" });
|
throw new AppError({ message: "Failed to create monitors", status: 500, service: SERVICE_NAME, method: "createMonitors" });
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(createdMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor)));
|
await Promise.all(createdMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor)));
|
||||||
@@ -184,7 +192,7 @@ export class MonitorService implements IMonitorService {
|
|||||||
interval: 60000,
|
interval: 60000,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const demoMonitors = await this.monitorsRepository.createBulkMonitors(monitors);
|
const demoMonitors = await this.monitorsRepository.createMonitors(monitors);
|
||||||
|
|
||||||
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor)));
|
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor)));
|
||||||
return demoMonitors;
|
return demoMonitors;
|
||||||
@@ -217,7 +225,8 @@ export class MonitorService implements IMonitorService {
|
|||||||
checksData.monitorType !== "ping" &&
|
checksData.monitorType !== "ping" &&
|
||||||
checksData.monitorType !== "docker" &&
|
checksData.monitorType !== "docker" &&
|
||||||
checksData.monitorType !== "port" &&
|
checksData.monitorType !== "port" &&
|
||||||
checksData.monitorType !== "game"
|
checksData.monitorType !== "game" &&
|
||||||
|
checksData.monitorType !== "grpc"
|
||||||
) {
|
) {
|
||||||
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
|
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
|
||||||
}
|
}
|
||||||
@@ -307,13 +316,47 @@ export class MonitorService implements IMonitorService {
|
|||||||
const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id);
|
const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
monitor: {
|
monitorData: {
|
||||||
...monitor,
|
monitor,
|
||||||
checks: checksData.checks,
|
groupedChecks: checksData.groupedChecks,
|
||||||
},
|
},
|
||||||
monitorStats,
|
monitorStats,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getGeoChecksByMonitorId = async ({
|
||||||
|
teamId,
|
||||||
|
monitorId,
|
||||||
|
dateRange,
|
||||||
|
continents,
|
||||||
|
}: {
|
||||||
|
teamId: string;
|
||||||
|
monitorId: string;
|
||||||
|
dateRange: string;
|
||||||
|
continents?: GeoContinent[];
|
||||||
|
}): Promise<any> => {
|
||||||
|
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||||
|
if (!monitor) {
|
||||||
|
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitor.type !== "http" || !monitor.geoCheckEnabled) {
|
||||||
|
return { groupedGeoChecks: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
|
||||||
|
const { start, end } = this.getDateRange(rangeKey);
|
||||||
|
const groupedGeoChecks = await this.geoChecksRepository.findGroupedByMonitorIdAndDateRange(
|
||||||
|
monitor.id,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
this.getDateFormat(rangeKey),
|
||||||
|
continents
|
||||||
|
);
|
||||||
|
|
||||||
|
return { groupedGeoChecks };
|
||||||
|
};
|
||||||
|
|
||||||
getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
|
getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
|
||||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||||
return monitor;
|
return monitor;
|
||||||
@@ -437,6 +480,14 @@ export class MonitorService implements IMonitorService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.geoChecksRepository.deleteByMonitorId(monitor.id).catch((err: any) => {
|
||||||
|
this.logger.warn({
|
||||||
|
message: `Error deleting geo checks for monitor ${monitor.id} with name ${monitor.name}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
stack: err.stack,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
await this.jobQueue.deleteJob(monitor);
|
await this.jobQueue.deleteJob(monitor);
|
||||||
return monitor;
|
return monitor;
|
||||||
};
|
};
|
||||||
@@ -448,6 +499,7 @@ export class MonitorService implements IMonitorService {
|
|||||||
try {
|
try {
|
||||||
await this.jobQueue.deleteJob(monitor);
|
await this.jobQueue.deleteJob(monitor);
|
||||||
await this.checksRepository.deleteByMonitorId(monitor.id);
|
await this.checksRepository.deleteByMonitorId(monitor.id);
|
||||||
|
await this.geoChecksRepository.deleteByMonitorId(monitor.id);
|
||||||
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id);
|
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id);
|
||||||
await this.monitorStatsRepository.deleteByMonitorId(monitor.id);
|
await this.monitorStatsRepository.deleteByMonitorId(monitor.id);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -486,4 +538,41 @@ export class MonitorService implements IMonitorService {
|
|||||||
|
|
||||||
return monitors;
|
return monitors;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
importMonitorsFromJSON = async ({
|
||||||
|
teamId,
|
||||||
|
userId,
|
||||||
|
monitors,
|
||||||
|
}: {
|
||||||
|
teamId: string;
|
||||||
|
userId: string;
|
||||||
|
monitors: any[];
|
||||||
|
}): Promise<{ imported: number; errors: string[] }> => {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
const cleanedMonitors = monitors.map((monitor) => {
|
||||||
|
const cleanData = { ...monitor };
|
||||||
|
delete cleanData.id;
|
||||||
|
delete cleanData._id;
|
||||||
|
delete cleanData.createdAt;
|
||||||
|
delete cleanData.updatedAt;
|
||||||
|
delete cleanData.recentChecks;
|
||||||
|
// Monitors must belong to current team
|
||||||
|
cleanData.teamId = teamId;
|
||||||
|
return cleanData;
|
||||||
|
});
|
||||||
|
|
||||||
|
const createdMonitors = await this.createMonitors(cleanedMonitors, userId, teamId);
|
||||||
|
|
||||||
|
if (!createdMonitors || createdMonitors.length === 0) {
|
||||||
|
throw new AppError({
|
||||||
|
message: "Failed to import any monitors. Please check the file format and try again.",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "importMonitorsFromJSON",
|
||||||
|
status: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { imported: createdMonitors.length, errors };
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
import Scheduler from "super-simple-scheduler";
|
import Scheduler from "super-simple-scheduler";
|
||||||
|
import { ISuperSimpleQueueHelper } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||||
const SERVICE_NAME = "JobQueue";
|
const SERVICE_NAME = "JobQueue";
|
||||||
|
|
||||||
type QueueJobFailure = {
|
type QueueJobFailure = {
|
||||||
@@ -54,22 +56,12 @@ export interface ISuperSimpleQueue {
|
|||||||
class SuperSimpleQueue implements ISuperSimpleQueue {
|
class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
private helper: any;
|
private helper: ISuperSimpleQueueHelper;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
private readonly scheduler: Scheduler;
|
private readonly scheduler: Scheduler;
|
||||||
|
|
||||||
constructor({
|
constructor(logger: ILogger, helper: ISuperSimpleQueueHelper, monitorsRepository: IMonitorsRepository, scheduler: Scheduler) {
|
||||||
logger,
|
|
||||||
helper,
|
|
||||||
monitorsRepository,
|
|
||||||
scheduler,
|
|
||||||
}: {
|
|
||||||
logger: any;
|
|
||||||
helper: any;
|
|
||||||
monitorsRepository: IMonitorsRepository;
|
|
||||||
scheduler: Scheduler;
|
|
||||||
}) {
|
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.helper = helper;
|
this.helper = helper;
|
||||||
this.monitorsRepository = monitorsRepository;
|
this.monitorsRepository = monitorsRepository;
|
||||||
@@ -80,14 +72,14 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
return SuperSimpleQueue.SERVICE_NAME;
|
return SuperSimpleQueue.SERVICE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async create({ logger, helper, monitorsRepository }: { logger: any; helper: any; monitorsRepository: IMonitorsRepository }) {
|
static async create(logger: ILogger, helper: ISuperSimpleQueueHelper, monitorsRepository: IMonitorsRepository) {
|
||||||
const scheduler = new Scheduler({
|
const scheduler = new Scheduler({
|
||||||
// storeType: "mongo",
|
// storeType: "mongo",
|
||||||
// storeType: "redis",
|
// storeType: "redis",
|
||||||
logLevel: "debug",
|
logLevel: "debug",
|
||||||
// dbUri: envSettings.dbConnectionString,
|
// dbUri: envSettings.dbConnectionString,
|
||||||
});
|
});
|
||||||
const instance = new SuperSimpleQueue({ logger, helper, monitorsRepository, scheduler });
|
const instance = new SuperSimpleQueue(logger, helper, monitorsRepository, scheduler);
|
||||||
await instance.init();
|
await instance.init();
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
@@ -97,6 +89,7 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
this.scheduler.start();
|
this.scheduler.start();
|
||||||
|
|
||||||
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
|
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
|
||||||
|
this.scheduler.addTemplate("geo-check-job", this.helper.getGeoCheckJob());
|
||||||
this.scheduler.addTemplate("cleanup-orphaned", this.helper.getCleanupOrphanedJob());
|
this.scheduler.addTemplate("cleanup-orphaned", this.helper.getCleanupOrphanedJob());
|
||||||
const monitors = await this.monitorsRepository.findAll();
|
const monitors = await this.monitorsRepository.findAll();
|
||||||
if (!monitors) {
|
if (!monitors) {
|
||||||
@@ -112,7 +105,7 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
this.scheduler.addJob({ id: "cleanup-orphaned", template: "cleanup-orphaned", active: true });
|
this.scheduler.addJob({ id: "cleanup-orphaned", template: "cleanup-orphaned", active: true });
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: "Failed to initialize SuperSimpleQueue",
|
message: "Failed to initialize SuperSimpleQueue",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
@@ -131,17 +124,30 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
active: monitor.isActive,
|
active: monitor.isActive,
|
||||||
data: monitor,
|
data: monitor,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add geo check job if enabled for HTTP monitors
|
||||||
|
if (monitor.geoCheckEnabled && monitor.type === "http") {
|
||||||
|
this.scheduler.addJob({
|
||||||
|
id: `${monitorId}-geo`,
|
||||||
|
template: "geo-check-job",
|
||||||
|
repeat: monitor.geoCheckInterval,
|
||||||
|
active: monitor.isActive,
|
||||||
|
data: monitor,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteJob = async (monitor: any) => {
|
deleteJob = async (monitor: any) => {
|
||||||
this.scheduler.removeJob(monitor.id);
|
this.scheduler.removeJob(monitor.id);
|
||||||
|
this.scheduler.removeJob(`${monitor.id}-geo`);
|
||||||
};
|
};
|
||||||
|
|
||||||
pauseJob = async (monitor: any) => {
|
pauseJob = async (monitor: any) => {
|
||||||
const result = await this.scheduler.pauseJob(monitor.id);
|
const result = await this.scheduler.pauseJob(monitor.id);
|
||||||
if (result === false) {
|
if (result === false) {
|
||||||
throw new Error("Failed to resume monitor");
|
throw new Error("Failed to pause monitor");
|
||||||
}
|
}
|
||||||
|
await this.scheduler.pauseJob(`${monitor.id}-geo`);
|
||||||
this.logger.debug({
|
this.logger.debug({
|
||||||
message: `Paused monitor ${monitor.id}`,
|
message: `Paused monitor ${monitor.id}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
@@ -154,6 +160,9 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
if (result === false) {
|
if (result === false) {
|
||||||
throw new Error("Failed to resume monitor");
|
throw new Error("Failed to resume monitor");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.scheduler.resumeJob(`${monitor.id}-geo`);
|
||||||
|
|
||||||
this.logger.debug({
|
this.logger.debug({
|
||||||
message: `Resumed monitor ${monitor.id}`,
|
message: `Resumed monitor ${monitor.id}`,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
@@ -163,6 +172,29 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
|
|
||||||
updateJob = async (monitor: any) => {
|
updateJob = async (monitor: any) => {
|
||||||
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
||||||
|
|
||||||
|
// Handle geo check job lifecycle
|
||||||
|
const geoJobId = `${monitor.id}-geo`;
|
||||||
|
if (monitor.geoCheckEnabled && monitor.type === "http") {
|
||||||
|
// Check if geo job exists
|
||||||
|
const existingGeoJob = await this.scheduler.getJob(geoJobId);
|
||||||
|
if (existingGeoJob) {
|
||||||
|
// Update existing geo job
|
||||||
|
this.scheduler.updateJob(geoJobId, { repeat: monitor.geoCheckInterval, active: monitor.isActive, data: monitor });
|
||||||
|
} else {
|
||||||
|
// Create new geo job
|
||||||
|
this.scheduler.addJob({
|
||||||
|
id: geoJobId,
|
||||||
|
template: "geo-check-job",
|
||||||
|
repeat: monitor.geoCheckInterval,
|
||||||
|
active: monitor.isActive,
|
||||||
|
data: monitor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove geo job if disabled or monitor type changed
|
||||||
|
this.scheduler.removeJob(geoJobId);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
shutdown = async () => {
|
shutdown = async () => {
|
||||||
@@ -250,7 +282,11 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
};
|
};
|
||||||
|
|
||||||
obliterate = async () => {
|
obliterate = async () => {
|
||||||
console.log("obliterate not implemented");
|
this.logger.warn({
|
||||||
|
message: "obliterate method not implemented",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "obliterate",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ const SERVICE_NAME = "JobQueueHelper";
|
|||||||
import type { Monitor } from "@/types/monitor.js";
|
import type { Monitor } from "@/types/monitor.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js";
|
import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js";
|
||||||
import type { StatusChangeResult, MonitorStatusResponse, HardwareStatusPayload, MonitorStatus } from "@/types/index.js";
|
import type { StatusChangeResult } from "@/types/index.js";
|
||||||
import IncidentService from "@/service/business/incidentService.js";
|
import IncidentService from "@/service/business/incidentService.js";
|
||||||
|
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
|
||||||
import {
|
import {
|
||||||
IMaintenanceWindowsRepository,
|
IMaintenanceWindowsRepository,
|
||||||
IMonitorsRepository,
|
IMonitorsRepository,
|
||||||
@@ -11,7 +12,18 @@ import {
|
|||||||
IMonitorStatsRepository,
|
IMonitorStatsRepository,
|
||||||
IChecksRepository,
|
IChecksRepository,
|
||||||
IIncidentsRepository,
|
IIncidentsRepository,
|
||||||
|
IGeoChecksRepository,
|
||||||
} from "@/repositories/index.js";
|
} from "@/repositories/index.js";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
import { IBufferService } from "../bufferService.js";
|
||||||
|
|
||||||
|
export interface ISuperSimpleQueueHelper {
|
||||||
|
readonly serviceName: string;
|
||||||
|
getMonitorJob(): (monitor: Monitor) => Promise<void>;
|
||||||
|
getCleanupOrphanedJob(): () => Promise<void>;
|
||||||
|
getGeoCheckJob(): (monitor: Monitor) => Promise<void>;
|
||||||
|
isInMaintenanceWindow(monitorId: string, teamId: string): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MonitorActionDecision {
|
export interface MonitorActionDecision {
|
||||||
shouldCreateIncident: boolean;
|
shouldCreateIncident: boolean;
|
||||||
@@ -27,7 +39,7 @@ export interface MonitorActionDecision {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class SuperSimpleQueueHelper {
|
class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: any;
|
private logger: any;
|
||||||
@@ -35,7 +47,7 @@ class SuperSimpleQueueHelper {
|
|||||||
private statusService: IStatusService;
|
private statusService: IStatusService;
|
||||||
private notificationsService: INotificationsService;
|
private notificationsService: INotificationsService;
|
||||||
private checkService: any;
|
private checkService: any;
|
||||||
private buffer: any;
|
private buffer: IBufferService;
|
||||||
private incidentService: IncidentService;
|
private incidentService: IncidentService;
|
||||||
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
@@ -43,36 +55,26 @@ class SuperSimpleQueueHelper {
|
|||||||
private monitorStatsRepository: IMonitorStatsRepository;
|
private monitorStatsRepository: IMonitorStatsRepository;
|
||||||
private checksRepository: IChecksRepository;
|
private checksRepository: IChecksRepository;
|
||||||
private incidentsRepository: IIncidentsRepository;
|
private incidentsRepository: IIncidentsRepository;
|
||||||
|
private geoChecksService: IGeoChecksService;
|
||||||
|
private geoChecksRepository: IGeoChecksRepository;
|
||||||
|
|
||||||
constructor({
|
constructor(
|
||||||
logger,
|
logger: ILogger,
|
||||||
networkService,
|
networkService: INetworkService,
|
||||||
statusService,
|
statusService: IStatusService,
|
||||||
notificationsService,
|
notificationsService: INotificationsService,
|
||||||
checkService,
|
checkService: any,
|
||||||
buffer,
|
buffer: IBufferService,
|
||||||
incidentService,
|
incidentService: IncidentService,
|
||||||
maintenanceWindowsRepository,
|
maintenanceWindowsRepository: IMaintenanceWindowsRepository,
|
||||||
monitorsRepository,
|
monitorsRepository: IMonitorsRepository,
|
||||||
teamsRepository,
|
teamsRepository: ITeamsRepository,
|
||||||
monitorStatsRepository,
|
monitorStatsRepository: IMonitorStatsRepository,
|
||||||
checksRepository,
|
checksRepository: IChecksRepository,
|
||||||
incidentsRepository,
|
incidentsRepository: IIncidentsRepository,
|
||||||
}: {
|
geoChecksService: IGeoChecksService,
|
||||||
logger: any;
|
geoChecksRepository: IGeoChecksRepository
|
||||||
networkService: INetworkService;
|
) {
|
||||||
statusService: IStatusService;
|
|
||||||
notificationsService: INotificationsService;
|
|
||||||
checkService: any;
|
|
||||||
buffer: any;
|
|
||||||
incidentService: IncidentService;
|
|
||||||
maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
|
||||||
monitorsRepository: IMonitorsRepository;
|
|
||||||
teamsRepository: ITeamsRepository;
|
|
||||||
monitorStatsRepository: IMonitorStatsRepository;
|
|
||||||
checksRepository: IChecksRepository;
|
|
||||||
incidentsRepository: IIncidentsRepository;
|
|
||||||
}) {
|
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.networkService = networkService;
|
this.networkService = networkService;
|
||||||
this.statusService = statusService;
|
this.statusService = statusService;
|
||||||
@@ -86,6 +88,8 @@ class SuperSimpleQueueHelper {
|
|||||||
this.monitorStatsRepository = monitorStatsRepository;
|
this.monitorStatsRepository = monitorStatsRepository;
|
||||||
this.checksRepository = checksRepository;
|
this.checksRepository = checksRepository;
|
||||||
this.incidentsRepository = incidentsRepository;
|
this.incidentsRepository = incidentsRepository;
|
||||||
|
this.geoChecksService = geoChecksService;
|
||||||
|
this.geoChecksRepository = geoChecksRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
get serviceName() {
|
get serviceName() {
|
||||||
@@ -240,6 +244,16 @@ class SuperSimpleQueueHelper {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove orphaned geo checks
|
||||||
|
const deletedGeoChecksCount = await this.geoChecksRepository.deleteByMonitorIdsNotIn(allMonitorIds);
|
||||||
|
if (deletedGeoChecksCount > 0) {
|
||||||
|
this.logger.info({
|
||||||
|
message: `Deleted ${deletedGeoChecksCount} orphaned geo checks`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getCleanupOrphanedJob",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.info({
|
this.logger.info({
|
||||||
message: "Cleanup of orphaned data completed",
|
message: "Cleanup of orphaned data completed",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
@@ -257,6 +271,81 @@ class SuperSimpleQueueHelper {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getGeoCheckJob = () => {
|
||||||
|
return async (monitor: Monitor) => {
|
||||||
|
try {
|
||||||
|
const monitorId = monitor.id;
|
||||||
|
const teamId = monitor.teamId;
|
||||||
|
|
||||||
|
// Step 1: Validate monitor eligibility
|
||||||
|
if (!monitorId) {
|
||||||
|
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitor.type !== "http") {
|
||||||
|
this.logger.debug({
|
||||||
|
message: `Monitor ${monitorId} is not HTTP type, skipping geo check`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getGeoCheckJob",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!monitor.geoCheckEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) {
|
||||||
|
this.logger.warn({
|
||||||
|
message: `No geo check locations configured for monitor ${monitorId}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getGeoCheckJob",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Check for maintenance window
|
||||||
|
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId);
|
||||||
|
if (maintenanceWindowActive) {
|
||||||
|
this.logger.debug({
|
||||||
|
message: `Monitor ${monitorId} is in maintenance window, skipping geo check`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getGeoCheckJob",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Build geo check (handles API calls and polling)
|
||||||
|
const geoCheck = await this.geoChecksService.buildGeoCheck(monitor);
|
||||||
|
if (!geoCheck) {
|
||||||
|
this.logger.warn({
|
||||||
|
message: `No geo check could be built for monitor ${monitorId}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getGeoCheckJob",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Add geo check to buffer
|
||||||
|
this.buffer.addGeoCheckToBuffer(geoCheck);
|
||||||
|
|
||||||
|
this.logger.debug({
|
||||||
|
message: `Geo check job executed for monitor ${monitorId}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getGeoCheckJob",
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error({
|
||||||
|
message: error.message,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "getGeoCheckJob",
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
|
// Don't throw - geo check failures shouldn't crash the job scheduler
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
async isInMaintenanceWindow(monitorId: string, teamId: string) {
|
async isInMaintenanceWindow(monitorId: string, teamId: string) {
|
||||||
const maintenanceWindows = await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId);
|
const maintenanceWindows = await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId);
|
||||||
// Check for active maintenance window:
|
// Check for active maintenance window:
|
||||||
|
|||||||
@@ -1,29 +1,39 @@
|
|||||||
import type { Check } from "@/types/index.js";
|
import type { Check } from "@/types/index.js";
|
||||||
|
import type { GeoCheck } from "@/types/index.js";
|
||||||
|
import type { IGeoChecksService } from "../business/geoChecksService.js";
|
||||||
|
import type { ILogger } from "@/utils/logger.js";
|
||||||
|
import type { ISettingsService } from "@/service/system/settingsService.js";
|
||||||
|
import { ICheckService } from "../business/checkService.js";
|
||||||
const SERVICE_NAME = "BufferService";
|
const SERVICE_NAME = "BufferService";
|
||||||
|
|
||||||
export interface IBufferService {
|
export interface IBufferService {
|
||||||
addToBuffer(check: Check): void;
|
addToBuffer(check: Check): void;
|
||||||
|
addGeoCheckToBuffer(geoCheck: GeoCheck): void;
|
||||||
removeCheckFromBuffer(check: Check): boolean;
|
removeCheckFromBuffer(check: Check): boolean;
|
||||||
scheduleNextFlush(): void;
|
scheduleNextFlush(): void;
|
||||||
flushBuffer(): Promise<void>;
|
flushBuffer(): Promise<void>;
|
||||||
|
flushGeoBuffer(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class BufferService implements IBufferService {
|
class BufferService implements IBufferService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private BUFFER_TIMEOUT: number;
|
private BUFFER_TIMEOUT: number;
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
private SERVICE_NAME: string;
|
private SERVICE_NAME: string;
|
||||||
private buffer: any[];
|
private buffer: Check[];
|
||||||
|
private geoBuffer: GeoCheck[];
|
||||||
private bufferTimer: NodeJS.Timeout | null = null;
|
private bufferTimer: NodeJS.Timeout | null = null;
|
||||||
private checksService: any;
|
private checksService: ICheckService;
|
||||||
|
private geoChecksService: IGeoChecksService;
|
||||||
|
|
||||||
constructor({ logger, checkService, settingsService }: { logger: any; checkService: any; settingsService: any }) {
|
constructor(logger: ILogger, checkService: ICheckService, geoChecksService: IGeoChecksService, settingsService: ISettingsService) {
|
||||||
this.BUFFER_TIMEOUT = settingsService.getSettings().nodeEnv === "development" ? 10 : 1000 * 60 * 1; // 1 minute
|
this.BUFFER_TIMEOUT = settingsService.getSettings().nodeEnv === "development" ? 10 : 1000 * 60 * 1; // 1 minute
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.checksService = checkService;
|
this.checksService = checkService;
|
||||||
|
this.geoChecksService = geoChecksService;
|
||||||
this.SERVICE_NAME = SERVICE_NAME;
|
this.SERVICE_NAME = SERVICE_NAME;
|
||||||
this.buffer = [];
|
this.buffer = [];
|
||||||
|
this.geoBuffer = [];
|
||||||
this.scheduleNextFlush();
|
this.scheduleNextFlush();
|
||||||
this.logger.info({
|
this.logger.info({
|
||||||
message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`,
|
message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`,
|
||||||
@@ -39,12 +49,25 @@ class BufferService implements IBufferService {
|
|||||||
addToBuffer(check: Check) {
|
addToBuffer(check: Check) {
|
||||||
try {
|
try {
|
||||||
this.buffer.push(check);
|
this.buffer.push(check);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: this.SERVICE_NAME,
|
service: this.SERVICE_NAME,
|
||||||
method: "addToBuffer",
|
method: "addToBuffer",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addGeoCheckToBuffer(geoCheck: GeoCheck) {
|
||||||
|
try {
|
||||||
|
this.geoBuffer.push(geoCheck);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error({
|
||||||
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "addGeoCheckToBuffer",
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,12 +99,12 @@ class BufferService implements IBufferService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: this.SERVICE_NAME,
|
service: this.SERVICE_NAME,
|
||||||
method: "removeCheckFromBuffer",
|
method: "removeCheckFromBuffer",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -94,12 +117,13 @@ class BufferService implements IBufferService {
|
|||||||
this.bufferTimer = setTimeout(async () => {
|
this.bufferTimer = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await this.flushBuffer();
|
await this.flushBuffer();
|
||||||
} catch (error: any) {
|
await this.flushGeoBuffer();
|
||||||
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: `Error in flush cycle: ${error.message}`,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: this.SERVICE_NAME,
|
service: this.SERVICE_NAME,
|
||||||
method: "scheduleNextFlush",
|
method: "scheduleNextFlush",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
// Schedule the next flush only after the current one completes
|
// Schedule the next flush only after the current one completes
|
||||||
@@ -110,18 +134,47 @@ class BufferService implements IBufferService {
|
|||||||
async flushBuffer() {
|
async flushBuffer() {
|
||||||
try {
|
try {
|
||||||
if (this.buffer.length > 0) {
|
if (this.buffer.length > 0) {
|
||||||
|
this.logger.debug({
|
||||||
|
message: `Flushing ${this.buffer.length} checks to database`,
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "flushBuffer",
|
||||||
|
});
|
||||||
await this.checksService.createChecks(this.buffer);
|
await this.checksService.createChecks(this.buffer);
|
||||||
|
this.buffer = [];
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: this.SERVICE_NAME,
|
service: this.SERVICE_NAME,
|
||||||
method: "flushBuffer",
|
method: "flushBuffer",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
|
// Clear buffer even on error to prevent infinite retry loops
|
||||||
|
this.buffer = [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.buffer = [];
|
async flushGeoBuffer() {
|
||||||
|
try {
|
||||||
|
if (this.geoBuffer.length > 0) {
|
||||||
|
this.logger.debug({
|
||||||
|
message: `Flushing ${this.geoBuffer.length} geo checks to database`,
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "flushGeoBuffer",
|
||||||
|
});
|
||||||
|
await this.geoChecksService.createGeoChecks(this.geoBuffer);
|
||||||
|
this.geoBuffer = [];
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error({
|
||||||
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "flushGeoBuffer",
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
});
|
||||||
|
// Clear buffer even on error to prevent infinite retry loops
|
||||||
|
this.geoBuffer = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,51 @@
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import path from "path";
|
|
||||||
import { EmailTransportConfig } from "@/types/index.js";
|
import { EmailTransportConfig } from "@/types/index.js";
|
||||||
|
import { ISettingsService } from "@/service/system/settingsService.js";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import nodemailer from "nodemailer";
|
||||||
|
import mjml2html from "mjml";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const SERVICE_NAME = "EmailService";
|
const SERVICE_NAME = "EmailService";
|
||||||
|
type MjmlFn = typeof mjml2html;
|
||||||
|
type FileSystem = typeof fs;
|
||||||
|
type PathModule = typeof path;
|
||||||
|
type Mailer = typeof nodemailer;
|
||||||
|
type TemplateCompiler = (template: string) => (context: Record<string, unknown>) => string;
|
||||||
|
|
||||||
class EmailService {
|
export interface IEmailService {
|
||||||
|
init(): void;
|
||||||
|
buildEmail(template: string, context: Record<string, unknown>): Promise<string | undefined>;
|
||||||
|
sendEmail(to: string, subject: string, html: string, transportConfig?: EmailTransportConfig): Promise<string | false | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailService implements IEmailService {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private settingsService: any;
|
private settingsService: ISettingsService;
|
||||||
private fs: any;
|
private fs: FileSystem;
|
||||||
private path: any;
|
private path: PathModule;
|
||||||
private compile: any;
|
private compile: TemplateCompiler;
|
||||||
private mjml2html: any;
|
private mjml2html: MjmlFn;
|
||||||
private nodemailer: any;
|
private nodemailer: Mailer;
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
private transporter: any;
|
private transporter: ReturnType<typeof import("nodemailer").createTransport> | null = null;
|
||||||
private templateLookup: Record<string, Function>;
|
private templateLookup: Record<string, ((context: Record<string, unknown>) => string) | undefined>;
|
||||||
private loadTemplate: (templateName: string) => Function;
|
private loadTemplate: (templateName: string) => ((context: Record<string, unknown>) => string) | undefined;
|
||||||
|
|
||||||
constructor(settingsService: any, fs: any, path: any, compile: any, mjml2html: any, nodemailer: any, logger: any) {
|
constructor(
|
||||||
|
settingsService: ISettingsService,
|
||||||
|
fs: FileSystem,
|
||||||
|
path: PathModule,
|
||||||
|
compile: TemplateCompiler,
|
||||||
|
mjml2html: MjmlFn,
|
||||||
|
nodemailer: Mailer,
|
||||||
|
logger: ILogger
|
||||||
|
) {
|
||||||
this.settingsService = settingsService;
|
this.settingsService = settingsService;
|
||||||
this.fs = fs;
|
this.fs = fs;
|
||||||
this.path = path;
|
this.path = path;
|
||||||
@@ -30,9 +54,7 @@ class EmailService {
|
|||||||
this.nodemailer = nodemailer;
|
this.nodemailer = nodemailer;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.templateLookup = {};
|
this.templateLookup = {};
|
||||||
this.loadTemplate = () => {
|
this.loadTemplate = () => undefined;
|
||||||
return () => {};
|
|
||||||
};
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,12 +68,12 @@ class EmailService {
|
|||||||
const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`);
|
const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`);
|
||||||
const templateContent = this.fs.readFileSync(templatePath, "utf8");
|
const templateContent = this.fs.readFileSync(templatePath, "utf8");
|
||||||
return this.compile(templateContent);
|
return this.compile(templateContent);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "loadTemplate",
|
method: "loadTemplate",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -66,17 +88,20 @@ class EmailService {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
buildEmail = async (template: string, context: Record<string, any>) => {
|
buildEmail = async (template: string, context: Record<string, unknown>) => {
|
||||||
try {
|
try {
|
||||||
const mjml = this.templateLookup[template]?.(context);
|
const mjml = this.templateLookup[template]?.(context);
|
||||||
const html = await this.mjml2html(mjml);
|
if (!mjml) {
|
||||||
|
throw new Error(`Template ${template} not found`);
|
||||||
|
}
|
||||||
|
const html = this.mjml2html(mjml);
|
||||||
return html.html;
|
return html.html;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "buildEmail",
|
method: "buildEmail",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -125,11 +150,12 @@ class EmailService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.transporter.verify();
|
await this.transporter.verify();
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: "Email transporter verification failed",
|
message: "Email transporter verification failed",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "verifyTransporter",
|
method: "verifyTransporter",
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -142,12 +168,12 @@ class EmailService {
|
|||||||
html: html,
|
html: html,
|
||||||
});
|
});
|
||||||
return info?.messageId;
|
return info?.messageId;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : "Unknown error",
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "sendEmail",
|
method: "sendEmail",
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js";
|
||||||
|
import type { ILogger } from "@/utils/logger.js";
|
||||||
|
import got from "got";
|
||||||
|
|
||||||
|
const SERVICE_NAME = "GlobalPingService";
|
||||||
|
const GLOBAL_PING_API_BASE = "https://api.globalping.io/v1";
|
||||||
|
const POLL_INTERVAL_MS = 2000;
|
||||||
|
const MAX_POLL_TIMEOUT_MS = 30000;
|
||||||
|
|
||||||
|
interface GlobalPingMeasurementRequest {
|
||||||
|
type: "http";
|
||||||
|
target: string;
|
||||||
|
locations: Array<{ continent: GeoContinent }>;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GlobalPingMeasurementResponse {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
status: "in-progress" | "finished" | "failed";
|
||||||
|
probesCount: number;
|
||||||
|
results?: GlobalPingProbeResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GlobalPingProbeResult {
|
||||||
|
probe: {
|
||||||
|
continent: GeoContinent;
|
||||||
|
region: string;
|
||||||
|
country: string;
|
||||||
|
state: string | null;
|
||||||
|
city: string;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
};
|
||||||
|
result: {
|
||||||
|
status: "finished" | "failed" | "timeout";
|
||||||
|
statusCode?: number;
|
||||||
|
statusCodeName?: string;
|
||||||
|
timings?: {
|
||||||
|
total: number;
|
||||||
|
dns: number;
|
||||||
|
tcp: number;
|
||||||
|
tls: number;
|
||||||
|
firstByte: number;
|
||||||
|
download: number;
|
||||||
|
};
|
||||||
|
rawOutput?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGlobalPingService {
|
||||||
|
readonly serviceName: string;
|
||||||
|
createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null>;
|
||||||
|
pollForResults(measurementId: string, timeoutMs?: number): Promise<GeoCheckResult[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GlobalPingService implements IGlobalPingService {
|
||||||
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
|
private logger: ILogger;
|
||||||
|
|
||||||
|
constructor(logger: ILogger) {
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
get serviceName() {
|
||||||
|
return GlobalPingService.SERVICE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
// GlobalPing API expects target without protocol (http:// or https://)
|
||||||
|
const cleanTarget = url.replace(/^https?:\/\//, "");
|
||||||
|
|
||||||
|
const requestBody: GlobalPingMeasurementRequest = {
|
||||||
|
type: "http",
|
||||||
|
target: cleanTarget,
|
||||||
|
locations: locations.map((continent) => ({ continent })),
|
||||||
|
limit: locations.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await got.post<GlobalPingMeasurementResponse>(`${GLOBAL_PING_API_BASE}/measurements`, {
|
||||||
|
json: requestBody,
|
||||||
|
responseType: "json",
|
||||||
|
timeout: { request: 10000 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const measurementId = response.body.id;
|
||||||
|
|
||||||
|
this.logger.debug({
|
||||||
|
message: `Created GlobalPing measurement: ${measurementId}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "createMeasurement",
|
||||||
|
details: { measurementId, url, locations },
|
||||||
|
});
|
||||||
|
|
||||||
|
return measurementId;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error({
|
||||||
|
message: "GlobalPing API unavailable, skipping geo check",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "createMeasurement",
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async pollForResults(measurementId: string, timeoutMs: number = MAX_POLL_TIMEOUT_MS): Promise<GeoCheckResult[]> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startTime < timeoutMs) {
|
||||||
|
try {
|
||||||
|
const response = await got.get<GlobalPingMeasurementResponse>(`${GLOBAL_PING_API_BASE}/measurements/${measurementId}`, {
|
||||||
|
responseType: "json",
|
||||||
|
timeout: { request: 5000 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const measurement = response.body;
|
||||||
|
|
||||||
|
if (measurement.status === "finished") {
|
||||||
|
const results = this.transformResults(measurement.results || []);
|
||||||
|
this.logger.debug({
|
||||||
|
message: `GlobalPing measurement completed: ${measurementId}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "pollForResults",
|
||||||
|
details: { measurementId, resultsCount: results.length },
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (measurement.status === "failed") {
|
||||||
|
this.logger.warn({
|
||||||
|
message: `GlobalPing measurement failed: ${measurementId}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "pollForResults",
|
||||||
|
});
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still in-progress, wait and poll again
|
||||||
|
await this.sleep(POLL_INTERVAL_MS);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this.logger.error({
|
||||||
|
message: "Error polling GlobalPing API",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "pollForResults",
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
});
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout reached
|
||||||
|
this.logger.warn({
|
||||||
|
message: `GlobalPing measurement polling timeout: ${measurementId}`,
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "pollForResults",
|
||||||
|
details: { measurementId, timeoutMs },
|
||||||
|
});
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private transformResults(probeResults: GlobalPingProbeResult[]): GeoCheckResult[] {
|
||||||
|
const successfulResults: GeoCheckResult[] = [];
|
||||||
|
|
||||||
|
for (const probeResult of probeResults) {
|
||||||
|
// Skip failed or timeout results
|
||||||
|
if (probeResult.result.status !== "finished" || !probeResult.result.statusCode || !probeResult.result.timings) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const location: GeoCheckLocation = {
|
||||||
|
continent: probeResult.probe.continent,
|
||||||
|
region: probeResult.probe.region,
|
||||||
|
country: probeResult.probe.country,
|
||||||
|
state: probeResult.probe.state || "",
|
||||||
|
city: probeResult.probe.city,
|
||||||
|
longitude: probeResult.probe.longitude,
|
||||||
|
latitude: probeResult.probe.latitude,
|
||||||
|
};
|
||||||
|
|
||||||
|
const timings: GeoCheckTimings = {
|
||||||
|
total: probeResult.result.timings.total,
|
||||||
|
dns: probeResult.result.timings.dns,
|
||||||
|
tcp: probeResult.result.timings.tcp,
|
||||||
|
tls: probeResult.result.timings.tls,
|
||||||
|
firstByte: probeResult.result.timings.firstByte,
|
||||||
|
download: probeResult.result.timings.download,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result: GeoCheckResult = {
|
||||||
|
location,
|
||||||
|
status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300,
|
||||||
|
statusCode: probeResult.result.statusCode,
|
||||||
|
timings,
|
||||||
|
};
|
||||||
|
|
||||||
|
successfulResults.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return successfulResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GlobalPingService;
|
||||||
@@ -1,8 +1,25 @@
|
|||||||
import { HTTPError, RequestError } from "got";
|
import { HTTPError, RequestError } from "got";
|
||||||
import type { Got, Response } from "got";
|
import type { Got, Response } from "got";
|
||||||
import type { Monitor, MonitorStatusResponse } from "@/types/index.js";
|
import type {
|
||||||
|
Monitor,
|
||||||
|
MonitorStatusResponse,
|
||||||
|
GrpcStatusPayload,
|
||||||
|
PageSpeedStatusPayload,
|
||||||
|
HttpStatusPayload,
|
||||||
|
HardwareStatusPayload,
|
||||||
|
PingStatusPayload,
|
||||||
|
DockerStatusPayload,
|
||||||
|
PortStatusPayload,
|
||||||
|
GameStatusPayload,
|
||||||
|
} from "@/types/index.js";
|
||||||
|
import type { AxiosStatic } from "axios";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
import CacheableLookup from "cacheable-lookup";
|
import CacheableLookup from "cacheable-lookup";
|
||||||
|
import { ISettingsService } from "../system/settingsService.js";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
const SERVICE_NAME = "NetworkService";
|
const SERVICE_NAME = "NetworkService";
|
||||||
|
|
||||||
type MonitorStatusResponseOverrides<T> = Partial<Omit<MonitorStatusResponse<T>, "monitorId" | "teamId" | "type">>;
|
type MonitorStatusResponseOverrides<T> = Partial<Omit<MonitorStatusResponse<T>, "monitorId" | "teamId" | "type">>;
|
||||||
@@ -21,8 +38,25 @@ interface BuildStatusResponseArgs<T> {
|
|||||||
|
|
||||||
export interface INetworkService {
|
export interface INetworkService {
|
||||||
readonly serviceName: string;
|
readonly serviceName: string;
|
||||||
requestStatus(monitor: Monitor): Promise<MonitorStatusResponse>;
|
requestStatus(
|
||||||
requestWebhook(type: string, url: string, body: any): Promise<{ type: string; status: boolean; code: number; message: string; payload?: unknown }>;
|
monitor: Monitor
|
||||||
|
): Promise<
|
||||||
|
MonitorStatusResponse<
|
||||||
|
| PingStatusPayload
|
||||||
|
| HttpStatusPayload
|
||||||
|
| PageSpeedStatusPayload
|
||||||
|
| HardwareStatusPayload
|
||||||
|
| DockerStatusPayload
|
||||||
|
| PortStatusPayload
|
||||||
|
| GameStatusPayload
|
||||||
|
| GrpcStatusPayload
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
requestWebhook(
|
||||||
|
type: string,
|
||||||
|
url: string,
|
||||||
|
body: unknown
|
||||||
|
): Promise<{ type: string; status: boolean; code: number; message: string; payload?: unknown }>;
|
||||||
requestPagerDuty(args: { message: string; routingKey: string; monitorUrl: string }): Promise<boolean>;
|
requestPagerDuty(args: { message: string; routingKey: string; monitorUrl: string }): Promise<boolean>;
|
||||||
requestMatrix(args: { homeserverUrl: string; accessToken: string; roomId: string; message: string }): Promise<{
|
requestMatrix(args: { homeserverUrl: string; accessToken: string; roomId: string; message: string }): Promise<{
|
||||||
status: boolean;
|
status: boolean;
|
||||||
@@ -42,20 +76,23 @@ class NetworkService implements INetworkService {
|
|||||||
private TYPE_DOCKER: string;
|
private TYPE_DOCKER: string;
|
||||||
private TYPE_PORT: string;
|
private TYPE_PORT: string;
|
||||||
private TYPE_GAME: string;
|
private TYPE_GAME: string;
|
||||||
|
private TYPE_GRPC: string;
|
||||||
private SERVICE_NAME: string;
|
private SERVICE_NAME: string;
|
||||||
private NETWORK_ERROR: number;
|
private NETWORK_ERROR: number;
|
||||||
private PING_ERROR: number;
|
private PING_ERROR: number;
|
||||||
|
|
||||||
private axios: any;
|
private axios: AxiosStatic;
|
||||||
private got: Got;
|
private got: Got;
|
||||||
private https: any;
|
private https: typeof import("https");
|
||||||
private jmespath: any;
|
private jmespath: { search: (data: unknown, expression: string) => unknown };
|
||||||
private GameDig: any;
|
private GameDig: { query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }> };
|
||||||
private ping: any;
|
private ping: typeof import("ping");
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
private Docker: any;
|
private Docker: typeof import("dockerode");
|
||||||
private net: any;
|
private net: typeof import("net");
|
||||||
private settingsService: any;
|
private settingsService: ISettingsService;
|
||||||
|
private grpc: typeof import("@grpc/grpc-js");
|
||||||
|
private protoLoader: typeof import("@grpc/proto-loader");
|
||||||
|
|
||||||
private buildStatusResponse = <T>({
|
private buildStatusResponse = <T>({
|
||||||
monitor,
|
monitor,
|
||||||
@@ -90,7 +127,7 @@ class NetworkService implements INetworkService {
|
|||||||
statusResponse.responseTime = error.timings?.phases?.total ?? 0;
|
statusResponse.responseTime = error.timings?.phases?.total ?? 0;
|
||||||
statusResponse.timings = error.timings;
|
statusResponse.timings = error.timings;
|
||||||
}
|
}
|
||||||
return { ...statusResponse, ...(overrides ?? {}) };
|
return { ...statusResponse, ...(overrides ?? {}) } as MonitorStatusResponse<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -108,34 +145,23 @@ class NetworkService implements INetworkService {
|
|||||||
expectedValue,
|
expectedValue,
|
||||||
extracted,
|
extracted,
|
||||||
...(overrides ?? {}),
|
...(overrides ?? {}),
|
||||||
};
|
} as MonitorStatusResponse<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor({
|
constructor(
|
||||||
axios,
|
axios: AxiosStatic,
|
||||||
got,
|
got: Got,
|
||||||
https,
|
https: typeof import("https"),
|
||||||
jmespath,
|
jmespath: { search: (data: unknown, expression: string) => unknown },
|
||||||
GameDig,
|
GameDig: { query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }> },
|
||||||
ping,
|
ping: typeof import("ping"),
|
||||||
logger,
|
logger: ILogger,
|
||||||
http,
|
Docker: typeof import("dockerode"),
|
||||||
Docker,
|
net: typeof import("net"),
|
||||||
net,
|
settingsService: ISettingsService,
|
||||||
settingsService,
|
grpc: typeof import("@grpc/grpc-js"),
|
||||||
}: {
|
protoLoader: typeof import("@grpc/proto-loader")
|
||||||
axios: any;
|
) {
|
||||||
got: Got;
|
|
||||||
https: any;
|
|
||||||
jmespath: any;
|
|
||||||
GameDig: any;
|
|
||||||
ping: any;
|
|
||||||
logger: any;
|
|
||||||
http: any;
|
|
||||||
Docker: any;
|
|
||||||
net: any;
|
|
||||||
settingsService: any;
|
|
||||||
}) {
|
|
||||||
this.TYPE_PING = "ping";
|
this.TYPE_PING = "ping";
|
||||||
this.TYPE_HTTP = "http";
|
this.TYPE_HTTP = "http";
|
||||||
this.TYPE_PAGESPEED = "pagespeed";
|
this.TYPE_PAGESPEED = "pagespeed";
|
||||||
@@ -143,6 +169,7 @@ class NetworkService implements INetworkService {
|
|||||||
this.TYPE_DOCKER = "docker";
|
this.TYPE_DOCKER = "docker";
|
||||||
this.TYPE_PORT = "port";
|
this.TYPE_PORT = "port";
|
||||||
this.TYPE_GAME = "game";
|
this.TYPE_GAME = "game";
|
||||||
|
this.TYPE_GRPC = "grpc";
|
||||||
this.SERVICE_NAME = SERVICE_NAME;
|
this.SERVICE_NAME = SERVICE_NAME;
|
||||||
this.NETWORK_ERROR = 5000;
|
this.NETWORK_ERROR = 5000;
|
||||||
this.PING_ERROR = 5001;
|
this.PING_ERROR = 5001;
|
||||||
@@ -155,6 +182,8 @@ class NetworkService implements INetworkService {
|
|||||||
this.Docker = Docker;
|
this.Docker = Docker;
|
||||||
this.net = net;
|
this.net = net;
|
||||||
this.settingsService = settingsService;
|
this.settingsService = settingsService;
|
||||||
|
this.grpc = grpc;
|
||||||
|
this.protoLoader = protoLoader;
|
||||||
|
|
||||||
const cacheable = new CacheableLookup();
|
const cacheable = new CacheableLookup();
|
||||||
|
|
||||||
@@ -185,13 +214,26 @@ class NetworkService implements INetworkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Main entry point
|
// Main entry point
|
||||||
async requestStatus(monitor: Monitor): Promise<MonitorStatusResponse> {
|
async requestStatus(
|
||||||
|
monitor: Monitor
|
||||||
|
): Promise<
|
||||||
|
MonitorStatusResponse<
|
||||||
|
| PingStatusPayload
|
||||||
|
| HttpStatusPayload
|
||||||
|
| PageSpeedStatusPayload
|
||||||
|
| HardwareStatusPayload
|
||||||
|
| DockerStatusPayload
|
||||||
|
| PortStatusPayload
|
||||||
|
| GameStatusPayload
|
||||||
|
| GrpcStatusPayload
|
||||||
|
>
|
||||||
|
> {
|
||||||
const type = monitor?.type || "unknown";
|
const type = monitor?.type || "unknown";
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case this.TYPE_PING:
|
case this.TYPE_PING:
|
||||||
return await this.requestPing(monitor);
|
return await this.requestPing(monitor);
|
||||||
case this.TYPE_HTTP:
|
case this.TYPE_HTTP:
|
||||||
return await this.requestHttp(monitor);
|
return await this.requestHttp<HttpStatusPayload>(monitor);
|
||||||
case this.TYPE_PAGESPEED:
|
case this.TYPE_PAGESPEED:
|
||||||
return await this.requestPageSpeed(monitor);
|
return await this.requestPageSpeed(monitor);
|
||||||
case this.TYPE_HARDWARE:
|
case this.TYPE_HARDWARE:
|
||||||
@@ -202,12 +244,14 @@ class NetworkService implements INetworkService {
|
|||||||
return await this.requestPort(monitor);
|
return await this.requestPort(monitor);
|
||||||
case this.TYPE_GAME:
|
case this.TYPE_GAME:
|
||||||
return await this.requestGame(monitor);
|
return await this.requestGame(monitor);
|
||||||
|
case this.TYPE_GRPC:
|
||||||
|
return await this.requestGrpc(monitor);
|
||||||
default:
|
default:
|
||||||
return this.handleUnsupportedType(type);
|
return this.handleUnsupportedType(type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async requestPing(monitor: Monitor): Promise<MonitorStatusResponse> {
|
private async requestPing(monitor: Monitor): Promise<MonitorStatusResponse<PingStatusPayload>> {
|
||||||
try {
|
try {
|
||||||
if (!monitor?.url) {
|
if (!monitor?.url) {
|
||||||
throw new Error("Monitor URL is required");
|
throw new Error("Monitor URL is required");
|
||||||
@@ -227,36 +271,33 @@ class NetworkService implements INetworkService {
|
|||||||
throw new Error("Ping failed - no result returned");
|
throw new Error("Ping failed - no result returned");
|
||||||
}
|
}
|
||||||
|
|
||||||
const pingResponse = this.buildStatusResponse({
|
const pingResponse = this.buildStatusResponse<PingStatusPayload>({
|
||||||
monitor,
|
monitor,
|
||||||
payload: response,
|
payload: response as PingStatusPayload,
|
||||||
overrides: {
|
overrides: {
|
||||||
status: (response as { alive?: boolean })?.alive ?? false,
|
status: (response as { alive?: boolean })?.alive ?? false,
|
||||||
code: 200,
|
code: 200,
|
||||||
message: "Success",
|
message: "Success",
|
||||||
responseTime: (response as { time?: number })?.time ?? 0,
|
responseTime: (response as { time?: number })?.time ?? 0,
|
||||||
payload: response,
|
payload: response as PingStatusPayload,
|
||||||
},
|
} as MonitorStatusResponseOverrides<PingStatusPayload>,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pingResponse.status = false;
|
|
||||||
pingResponse.code = this.PING_ERROR;
|
|
||||||
pingResponse.message = "Ping failed";
|
|
||||||
return pingResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pingResponse;
|
return pingResponse;
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
err.service = this.SERVICE_NAME;
|
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||||
err.method = "requestPing";
|
throw new AppError({
|
||||||
throw err;
|
message: originalMessage || "Error performing ping",
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "requestPing",
|
||||||
|
details: { url: monitor.url },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async requestHttp(monitor: Monitor): Promise<MonitorStatusResponse> {
|
private async requestHttp<T = unknown>(monitor: Monitor): Promise<MonitorStatusResponse<T>> {
|
||||||
const { url, secret, id, teamId, type, ignoreTlsErrors, useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor;
|
const { url, secret, ignoreTlsErrors, useAdvancedMatching, jsonPath, matchMethod, expectedValue, basicAuthUser, basicAuthPassword } = monitor;
|
||||||
const httpResponse = this.buildStatusResponse({
|
const httpResponse = this.buildStatusResponse<T>({
|
||||||
monitor,
|
monitor,
|
||||||
overrides: {
|
overrides: {
|
||||||
status: false,
|
status: false,
|
||||||
@@ -269,8 +310,15 @@ class NetworkService implements INetworkService {
|
|||||||
if (!url) {
|
if (!url) {
|
||||||
throw new Error("Monitor URL is required");
|
throw new Error("Monitor URL is required");
|
||||||
}
|
}
|
||||||
const config: Record<string, any> = {
|
const headers: Record<string, string> = {};
|
||||||
headers: secret ? { Authorization: `Bearer ${secret}` } : undefined,
|
if (secret) {
|
||||||
|
headers["Authorization"] = `Bearer ${secret}`;
|
||||||
|
} else if (basicAuthUser) {
|
||||||
|
const credentials = Buffer.from(`${basicAuthUser}:${basicAuthPassword || ""}`).toString("base64");
|
||||||
|
headers["Authorization"] = `Basic ${credentials}`;
|
||||||
|
}
|
||||||
|
const config: Record<string, unknown> = {
|
||||||
|
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ignoreTlsErrors) {
|
if (ignoreTlsErrors) {
|
||||||
@@ -283,7 +331,7 @@ class NetworkService implements INetworkService {
|
|||||||
|
|
||||||
const response = await this.got(url, config);
|
const response = await this.got(url, config);
|
||||||
|
|
||||||
let payload: any;
|
let payload: T | string | undefined;
|
||||||
const contentType = response.headers["content-type"];
|
const contentType = response.headers["content-type"];
|
||||||
|
|
||||||
if (contentType && contentType.includes("application/json")) {
|
if (contentType && contentType.includes("application/json")) {
|
||||||
@@ -371,23 +419,27 @@ class NetworkService implements INetworkService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return httpResponse;
|
return httpResponse;
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
if (err.name === "HTTPError" || err.name === "RequestError") {
|
if (err instanceof HTTPError || err instanceof RequestError) {
|
||||||
httpResponse.code = err?.response?.statusCode || this.NETWORK_ERROR;
|
httpResponse.code = err.response?.statusCode || this.NETWORK_ERROR;
|
||||||
httpResponse.status = false;
|
httpResponse.status = false;
|
||||||
httpResponse.message = err?.response?.statusCode || err.message;
|
httpResponse.message = String(err.response?.statusCode) || err.message;
|
||||||
httpResponse.responseTime = err?.timings?.phases?.total || 0;
|
httpResponse.responseTime = err.timings?.phases?.total || 0;
|
||||||
httpResponse.payload = null;
|
httpResponse.payload = null;
|
||||||
httpResponse.timings = err?.timings || {};
|
httpResponse.timings = err.timings || undefined;
|
||||||
return httpResponse;
|
return httpResponse;
|
||||||
}
|
}
|
||||||
err.service = this.SERVICE_NAME;
|
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||||
err.method = "requestHttp";
|
throw new AppError({
|
||||||
throw err;
|
message: originalMessage || "Error performing HTTP request",
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "requestHttp",
|
||||||
|
details: { url: monitor.url },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async requestPageSpeed(monitor: Monitor): Promise<MonitorStatusResponse> {
|
private async requestPageSpeed(monitor: Monitor): Promise<MonitorStatusResponse<PageSpeedStatusPayload>> {
|
||||||
try {
|
try {
|
||||||
const url = monitor.url;
|
const url = monitor.url;
|
||||||
if (!url) {
|
if (!url) {
|
||||||
@@ -405,28 +457,36 @@ class NetworkService implements INetworkService {
|
|||||||
details: { url },
|
details: { url },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await this.requestHttp({
|
return await this.requestHttp<PageSpeedStatusPayload>({
|
||||||
...monitor,
|
...monitor,
|
||||||
url: pageSpeedUrl,
|
url: pageSpeedUrl,
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
err.service = this.SERVICE_NAME;
|
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||||
err.method = "requestPageSpeed";
|
throw new AppError({
|
||||||
throw err;
|
message: originalMessage || "Error performing PageSpeed request",
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "requestPageSpeed",
|
||||||
|
details: { url: monitor.url },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async requestHardware(monitor: Monitor): Promise<MonitorStatusResponse> {
|
private async requestHardware(monitor: Monitor): Promise<MonitorStatusResponse<HardwareStatusPayload>> {
|
||||||
try {
|
try {
|
||||||
return await this.requestHttp(monitor);
|
return await this.requestHttp<HardwareStatusPayload>(monitor);
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
err.service = this.SERVICE_NAME;
|
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||||
err.method = "requestHardware";
|
throw new AppError({
|
||||||
throw err;
|
message: originalMessage || "Error performing hardware request",
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "requestHardware",
|
||||||
|
details: { url: monitor.url },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async requestDocker(monitor: Monitor): Promise<MonitorStatusResponse> {
|
private async requestDocker(monitor: Monitor): Promise<MonitorStatusResponse<DockerStatusPayload>> {
|
||||||
try {
|
try {
|
||||||
if (!monitor.url) {
|
if (!monitor.url) {
|
||||||
throw new Error("Monitor URL is required");
|
throw new Error("Monitor URL is required");
|
||||||
@@ -434,10 +494,9 @@ class NetworkService implements INetworkService {
|
|||||||
|
|
||||||
const docker = new this.Docker({
|
const docker = new this.Docker({
|
||||||
socketPath: "/var/run/docker.sock",
|
socketPath: "/var/run/docker.sock",
|
||||||
handleError: true, // Enable error handling
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const dockerResponse = this.buildStatusResponse({
|
const dockerResponse = this.buildStatusResponse<DockerStatusPayload>({
|
||||||
monitor,
|
monitor,
|
||||||
overrides: {
|
overrides: {
|
||||||
status: false,
|
status: false,
|
||||||
@@ -453,10 +512,10 @@ class NetworkService implements INetworkService {
|
|||||||
|
|
||||||
// Priority-based matching to avoid ambiguity:
|
// Priority-based matching to avoid ambiguity:
|
||||||
// 1. Exact full ID match (64-char)
|
// 1. Exact full ID match (64-char)
|
||||||
let exactIdMatch = containers.find((c: any) => c.Id.toLowerCase() === normalizedInput);
|
const exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput);
|
||||||
|
|
||||||
// 2. Exact container name match (case-insensitive)
|
// 2. Exact container name match (case-insensitive)
|
||||||
let exactNameMatch = containers.find((c: any) =>
|
const exactNameMatch = containers.find((c) =>
|
||||||
c.Names.some((name: string) => {
|
c.Names.some((name: string) => {
|
||||||
const cleanName = name.replace(/^\/+/, "").toLowerCase();
|
const cleanName = name.replace(/^\/+/, "").toLowerCase();
|
||||||
return cleanName === normalizedInput;
|
return cleanName === normalizedInput;
|
||||||
@@ -464,10 +523,10 @@ class NetworkService implements INetworkService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 3. Partial ID match (fallback for backwards compatibility)
|
// 3. Partial ID match (fallback for backwards compatibility)
|
||||||
let partialIdMatch = containers.find((c: any) => c.Id.toLowerCase().startsWith(normalizedInput));
|
const partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput));
|
||||||
|
|
||||||
// Select container based on priority
|
// Select container based on priority
|
||||||
let targetContainer = exactIdMatch || exactNameMatch || partialIdMatch;
|
const targetContainer = exactIdMatch || exactNameMatch || partialIdMatch;
|
||||||
|
|
||||||
// Return negative response if no container
|
// Return negative response if no container
|
||||||
if (!targetContainer) {
|
if (!targetContainer) {
|
||||||
@@ -503,9 +562,8 @@ class NetworkService implements INetworkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const container = docker.getContainer(targetContainer.Id);
|
const container = docker.getContainer(targetContainer.Id);
|
||||||
const { response, responseTime, error }: { response?: any; responseTime?: number; error?: any } = await this.timeRequest(() =>
|
const { response, responseTime, error }: { response?: { State: { Status: string } } | null; responseTime: number; error: unknown } =
|
||||||
container.inspect()
|
await this.timeRequest(() => container.inspect());
|
||||||
);
|
|
||||||
|
|
||||||
dockerResponse.responseTime = responseTime;
|
dockerResponse.responseTime = responseTime;
|
||||||
dockerResponse.status = response?.State?.Status === "running" ? true : false;
|
dockerResponse.status = response?.State?.Status === "running" ? true : false;
|
||||||
@@ -514,22 +572,34 @@ class NetworkService implements INetworkService {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
dockerResponse.status = false;
|
dockerResponse.status = false;
|
||||||
dockerResponse.code = error.statusCode || this.NETWORK_ERROR;
|
dockerResponse.code =
|
||||||
dockerResponse.message = error.reason || "Failed to fetch Docker container information";
|
(error && typeof error === "object" && "statusCode" in error ? (error as { statusCode?: number }).statusCode : undefined) ||
|
||||||
|
this.NETWORK_ERROR;
|
||||||
|
dockerResponse.message =
|
||||||
|
(error && typeof error === "object" && "reason" in error ? (error as { reason?: string }).reason : undefined) ||
|
||||||
|
"Failed to fetch Docker container information";
|
||||||
return dockerResponse;
|
return dockerResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
return dockerResponse;
|
return dockerResponse;
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
err.service = this.SERVICE_NAME;
|
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||||
err.method = "requestDocker";
|
throw new AppError({
|
||||||
throw err;
|
message: originalMessage || "Error performing Docker request",
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "requestDocker",
|
||||||
|
details: { url: monitor.url },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async requestPort(monitor: Monitor): Promise<MonitorStatusResponse> {
|
private async requestPort(monitor: Monitor): Promise<MonitorStatusResponse<PortStatusPayload>> {
|
||||||
try {
|
try {
|
||||||
const { url, port } = monitor;
|
const { url, port } = monitor;
|
||||||
|
if (!port) {
|
||||||
|
throw new Error("Port is required for port monitor");
|
||||||
|
}
|
||||||
|
|
||||||
const { response, responseTime, error } = await this.timeRequest(async () => {
|
const { response, responseTime, error } = await this.timeRequest(async () => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const socket = this.net.createConnection(
|
const socket = this.net.createConnection(
|
||||||
@@ -550,14 +620,14 @@ class NetworkService implements INetworkService {
|
|||||||
reject(new Error("Connection timeout"));
|
reject(new Error("Connection timeout"));
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("error", (err: any) => {
|
socket.on("error", (err: unknown) => {
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const portResponse = this.buildStatusResponse({
|
const portResponse = this.buildStatusResponse<PortStatusPayload>({
|
||||||
monitor,
|
monitor,
|
||||||
overrides: {
|
overrides: {
|
||||||
code: 200,
|
code: 200,
|
||||||
@@ -575,18 +645,22 @@ class NetworkService implements INetworkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return portResponse;
|
return portResponse;
|
||||||
} catch (error: any) {
|
} catch (err: unknown) {
|
||||||
error.service = this.SERVICE_NAME;
|
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||||
error.method = "requestTCP";
|
throw new AppError({
|
||||||
throw error;
|
message: originalMessage || "Error performing port check",
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "requestPort",
|
||||||
|
details: { url: monitor.url, port: monitor.port },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async requestGame(monitor: Monitor): Promise<MonitorStatusResponse> {
|
private async requestGame(monitor: Monitor): Promise<MonitorStatusResponse<GameStatusPayload>> {
|
||||||
try {
|
try {
|
||||||
const { url, port, gameId } = monitor;
|
const { url, port, gameId } = monitor;
|
||||||
|
|
||||||
const gameResponse = this.buildStatusResponse({
|
const gameResponse = this.buildStatusResponse<GameStatusPayload>({
|
||||||
monitor,
|
monitor,
|
||||||
overrides: {
|
overrides: {
|
||||||
code: 200,
|
code: 200,
|
||||||
@@ -596,16 +670,17 @@ class NetworkService implements INetworkService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const state = await this.GameDig.query({
|
const state = await this.GameDig.query({
|
||||||
type: gameId,
|
type: gameId ?? "unknown",
|
||||||
host: url,
|
host: url,
|
||||||
port: port,
|
port: port ?? 0,
|
||||||
}).catch((error: any) => {
|
}).catch((error: unknown): undefined => {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: error.message,
|
message: error instanceof Error ? error.message : String(error),
|
||||||
service: this.SERVICE_NAME,
|
service: this.SERVICE_NAME,
|
||||||
method: "requestGame",
|
method: "requestGame",
|
||||||
details: { url, port, gameId },
|
details: { url, port, gameId },
|
||||||
});
|
});
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
@@ -615,15 +690,176 @@ class NetworkService implements INetworkService {
|
|||||||
return gameResponse;
|
return gameResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
gameResponse.responseTime = state.ping;
|
gameResponse.responseTime = state.ping ?? 0;
|
||||||
gameResponse.payload = state;
|
gameResponse.payload = state as GameStatusPayload;
|
||||||
return gameResponse;
|
return gameResponse;
|
||||||
} catch (error: any) {
|
} catch (err: unknown) {
|
||||||
error.service = this.SERVICE_NAME;
|
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||||
error.method = "requestPing";
|
throw new AppError({
|
||||||
throw error;
|
message: originalMessage || "Error performing game server check",
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "requestGame",
|
||||||
|
details: { url: monitor.url, port: monitor.port, gameId: monitor.gameId },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private async requestGrpc(monitor: Monitor): Promise<MonitorStatusResponse<GrpcStatusPayload>> {
|
||||||
|
try {
|
||||||
|
const { url, port, ignoreTlsErrors } = monitor;
|
||||||
|
const grpcServiceName = monitor.grpcServiceName || "";
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
throw new AppError({ message: "Monitor host is required", service: this.SERVICE_NAME, method: "requestGrpc" });
|
||||||
|
}
|
||||||
|
if (!port) {
|
||||||
|
throw new AppError({ message: "Monitor port is required", service: this.SERVICE_NAME, method: "requestGrpc" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = `${url}:${port}`;
|
||||||
|
|
||||||
|
const currentFilePath = fileURLToPath(import.meta.url);
|
||||||
|
const protoPath = path.join(path.dirname(currentFilePath), "protos", "health.proto");
|
||||||
|
|
||||||
|
const packageDefinition = this.protoLoader.loadSync(protoPath, {
|
||||||
|
keepCase: true,
|
||||||
|
longs: String,
|
||||||
|
enums: String,
|
||||||
|
defaults: true,
|
||||||
|
oneofs: true,
|
||||||
|
});
|
||||||
|
const grpcObject = this.grpc.loadPackageDefinition(packageDefinition) as unknown as {
|
||||||
|
grpc: {
|
||||||
|
health: {
|
||||||
|
v1: {
|
||||||
|
Health: new (
|
||||||
|
target: string,
|
||||||
|
credentials: unknown
|
||||||
|
) => {
|
||||||
|
Check: (request: { service: string }, options: { deadline: Date }, callback: (err: unknown, response: unknown) => void) => void;
|
||||||
|
close: () => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const healthService = grpcObject.grpc.health.v1.Health;
|
||||||
|
|
||||||
|
let credentials;
|
||||||
|
if (ignoreTlsErrors) {
|
||||||
|
credentials = this.grpc.credentials.createSsl(null, null, null, {
|
||||||
|
checkServerIdentity: () => undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
credentials = this.grpc.credentials.createInsecure();
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new healthService(target, credentials);
|
||||||
|
|
||||||
|
const TIMEOUT_MS = 10000;
|
||||||
|
const deadline = new Date(Date.now() + TIMEOUT_MS);
|
||||||
|
|
||||||
|
const grpcResponse = this.buildStatusResponse<GrpcStatusPayload>({
|
||||||
|
monitor,
|
||||||
|
overrides: {
|
||||||
|
status: false,
|
||||||
|
code: this.NETWORK_ERROR,
|
||||||
|
message: "gRPC health check not executed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { response, responseTime, error } = await this.timeRequest<GrpcStatusPayload>(() => {
|
||||||
|
return new Promise<GrpcStatusPayload>((resolve, reject) => {
|
||||||
|
client.Check({ service: grpcServiceName }, { deadline }, (err: unknown, response: unknown) => {
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
const grpcErr = err as { code?: number; details?: string; message?: string };
|
||||||
|
const payload: GrpcStatusPayload = {
|
||||||
|
grpcStatusCode: grpcErr.code ?? -1,
|
||||||
|
grpcStatusName: this.getGrpcStatusName(grpcErr.code ?? -1),
|
||||||
|
serviceName: grpcServiceName,
|
||||||
|
servingStatus: "UNKNOWN",
|
||||||
|
};
|
||||||
|
const grpcError = new AppError({
|
||||||
|
message: grpcErr.details || grpcErr.message || "gRPC error",
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "requestGrpc",
|
||||||
|
}) as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number };
|
||||||
|
grpcError.grpcPayload = payload;
|
||||||
|
grpcError.grpcCode = grpcErr.code;
|
||||||
|
reject(grpcError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = response as { status?: string } | undefined;
|
||||||
|
const servingStatus = resp?.status ?? "UNKNOWN";
|
||||||
|
resolve({
|
||||||
|
grpcStatusCode: 0,
|
||||||
|
grpcStatusName: "OK",
|
||||||
|
serviceName: grpcServiceName,
|
||||||
|
servingStatus,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
const grpcError = error as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number };
|
||||||
|
const payload = grpcError.grpcPayload;
|
||||||
|
grpcResponse.status = false;
|
||||||
|
grpcResponse.code = grpcError.grpcCode ?? this.NETWORK_ERROR;
|
||||||
|
grpcResponse.message = grpcError.message ?? "gRPC health check failed";
|
||||||
|
grpcResponse.responseTime = responseTime;
|
||||||
|
grpcResponse.payload = payload ?? null;
|
||||||
|
return grpcResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const grpcPayload = response as GrpcStatusPayload;
|
||||||
|
const isServing = grpcPayload.servingStatus === "SERVING";
|
||||||
|
|
||||||
|
grpcResponse.status = isServing;
|
||||||
|
grpcResponse.code = isServing ? 200 : this.NETWORK_ERROR;
|
||||||
|
grpcResponse.message = isServing
|
||||||
|
? `gRPC service healthy (${grpcPayload.servingStatus})`
|
||||||
|
: `gRPC service unhealthy (${grpcPayload.servingStatus})`;
|
||||||
|
grpcResponse.responseTime = responseTime;
|
||||||
|
grpcResponse.payload = grpcPayload;
|
||||||
|
|
||||||
|
return grpcResponse;
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new AppError({
|
||||||
|
message: originalMessage || "Error performing gRPC health check",
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "requestGrpc",
|
||||||
|
details: { url: monitor.url, port: monitor.port, grpcServiceName: monitor.grpcServiceName },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getGrpcStatusName(code: number): string {
|
||||||
|
const statusNames: Record<number, string> = {
|
||||||
|
0: "OK",
|
||||||
|
1: "CANCELLED",
|
||||||
|
2: "UNKNOWN",
|
||||||
|
3: "INVALID_ARGUMENT",
|
||||||
|
4: "DEADLINE_EXCEEDED",
|
||||||
|
5: "NOT_FOUND",
|
||||||
|
6: "ALREADY_EXISTS",
|
||||||
|
7: "PERMISSION_DENIED",
|
||||||
|
8: "RESOURCE_EXHAUSTED",
|
||||||
|
9: "FAILED_PRECONDITION",
|
||||||
|
10: "ABORTED",
|
||||||
|
11: "OUT_OF_RANGE",
|
||||||
|
12: "UNIMPLEMENTED",
|
||||||
|
13: "INTERNAL",
|
||||||
|
14: "UNAVAILABLE",
|
||||||
|
15: "DATA_LOSS",
|
||||||
|
16: "UNAUTHENTICATED",
|
||||||
|
};
|
||||||
|
return statusNames[code] || "UNKNOWN";
|
||||||
|
}
|
||||||
|
|
||||||
private async handleUnsupportedType(type: string): Promise<MonitorStatusResponse> {
|
private async handleUnsupportedType(type: string): Promise<MonitorStatusResponse> {
|
||||||
return {
|
return {
|
||||||
monitorId: "unknown",
|
monitorId: "unknown",
|
||||||
@@ -636,7 +872,7 @@ class NetworkService implements INetworkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Other network requests unrelated to monitoring:
|
// Other network requests unrelated to monitoring:
|
||||||
async requestWebhook(type: string, url: string, body: any) {
|
async requestWebhook(type: string, url: string, body: unknown) {
|
||||||
try {
|
try {
|
||||||
const response = await this.axios.post(url, body, {
|
const response = await this.axios.post(url, body, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -651,19 +887,29 @@ class NetworkService implements INetworkService {
|
|||||||
message: `Successfully sent ${type} notification`,
|
message: `Successfully sent ${type} notification`,
|
||||||
payload: response.data,
|
payload: response.data,
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (err: unknown) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: error.message,
|
message: err instanceof Error ? err.message : String(err),
|
||||||
service: this.SERVICE_NAME,
|
service: this.SERVICE_NAME,
|
||||||
method: "requestWebhook",
|
method: "requestWebhook",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (err && typeof err === "object" && "response" in err) {
|
||||||
|
const axiosError = err as { response?: { status?: number; data?: unknown } };
|
||||||
|
return {
|
||||||
|
type: "webhook",
|
||||||
|
status: false,
|
||||||
|
code: axiosError.response?.status ?? this.NETWORK_ERROR,
|
||||||
|
message: `Failed to send ${type} notification`,
|
||||||
|
payload: axiosError.response?.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "webhook",
|
type: "webhook",
|
||||||
status: false,
|
status: false,
|
||||||
code: error.response?.status || this.NETWORK_ERROR,
|
code: this.NETWORK_ERROR,
|
||||||
message: `Failed to send ${type} notification`,
|
message: `Failed to send ${type} notification`,
|
||||||
payload: error.response?.data,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -683,11 +929,17 @@ class NetworkService implements INetworkService {
|
|||||||
|
|
||||||
if (response?.data?.status !== "success") return false;
|
if (response?.data?.status !== "success") return false;
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (err: unknown) {
|
||||||
error.details = error.response?.data;
|
const originalMessage = err instanceof Error ? err.message : String(err);
|
||||||
error.service = this.SERVICE_NAME;
|
|
||||||
error.method = "requestPagerDuty";
|
throw new AppError({
|
||||||
throw error;
|
message: originalMessage || "Error sending PagerDuty notification",
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "requestPagerDuty",
|
||||||
|
details: {
|
||||||
|
responseData: err && typeof err === "object" && "response" in err ? (err as { response?: { data?: unknown } }).response?.data : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,18 +973,35 @@ class NetworkService implements INetworkService {
|
|||||||
code: response.status,
|
code: response.status,
|
||||||
message: "Successfully sent Matrix notification",
|
message: "Successfully sent Matrix notification",
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
this.logger.warn({
|
||||||
|
message: err.message,
|
||||||
|
service: this.SERVICE_NAME,
|
||||||
|
method: "requestMatrix",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (err && typeof err === "object" && "response" in err) {
|
||||||
|
const axiosError = err as { response?: { status?: number; data?: unknown } };
|
||||||
|
return {
|
||||||
|
status: false,
|
||||||
|
code: axiosError.response?.status || this.NETWORK_ERROR,
|
||||||
|
message: "Failed to send Matrix notification",
|
||||||
|
payload: axiosError.response?.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
message: error.message,
|
message: String(err),
|
||||||
service: this.SERVICE_NAME,
|
service: this.SERVICE_NAME,
|
||||||
method: "requestMatrix",
|
method: "requestMatrix",
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: false,
|
status: false,
|
||||||
code: error.response?.status || this.NETWORK_ERROR,
|
code: this.NETWORK_ERROR,
|
||||||
message: "Failed to send Matrix notification",
|
message: "Failed to send Matrix notification",
|
||||||
payload: error.response?.data,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Monitor, MonitorStatusResponse } from "@/types/index.js";
|
import type { HardwareStatusPayload, Monitor, MonitorStatusResponse } from "@/types/index.js";
|
||||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||||
import type {
|
import type {
|
||||||
NotificationMessage,
|
NotificationMessage,
|
||||||
@@ -30,8 +30,8 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
|
|||||||
clientHost: string
|
clientHost: string
|
||||||
): NotificationMessage {
|
): NotificationMessage {
|
||||||
const type = this.determineNotificationType(decision, monitor);
|
const type = this.determineNotificationType(decision, monitor);
|
||||||
const severity = this.determineSeverity(type, monitor.status);
|
const severity = this.determineSeverity(type);
|
||||||
const content = this.buildContent(type, monitor, monitorStatusResponse, decision, clientHost);
|
const content = this.buildContent(type, monitor, monitorStatusResponse);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
@@ -77,7 +77,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
|
|||||||
return "monitor_up";
|
return "monitor_up";
|
||||||
}
|
}
|
||||||
|
|
||||||
private determineSeverity(type: NotificationType, monitorStatus: string): NotificationSeverity {
|
private determineSeverity(type: NotificationType): NotificationSeverity {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "monitor_down":
|
case "monitor_down":
|
||||||
return "critical";
|
return "critical";
|
||||||
@@ -93,28 +93,22 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildContent(
|
private buildContent(type: NotificationType, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): NotificationContent {
|
||||||
type: NotificationType,
|
|
||||||
monitor: Monitor,
|
|
||||||
monitorStatusResponse: MonitorStatusResponse,
|
|
||||||
decision: MonitorActionDecision,
|
|
||||||
clientHost: string
|
|
||||||
): NotificationContent {
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "monitor_down":
|
case "monitor_down":
|
||||||
return this.buildMonitorDownContent(monitor, monitorStatusResponse, clientHost);
|
return this.buildMonitorDownContent(monitor, monitorStatusResponse);
|
||||||
case "monitor_up":
|
case "monitor_up":
|
||||||
return this.buildMonitorUpContent(monitor, clientHost);
|
return this.buildMonitorUpContent(monitor);
|
||||||
case "threshold_breach":
|
case "threshold_breach":
|
||||||
return this.buildThresholdBreachContent(monitor, monitorStatusResponse, decision, clientHost);
|
return this.buildThresholdBreachContent(monitor, monitorStatusResponse as MonitorStatusResponse<HardwareStatusPayload>);
|
||||||
case "threshold_resolved":
|
case "threshold_resolved":
|
||||||
return this.buildThresholdResolvedContent(monitor, clientHost);
|
return this.buildThresholdResolvedContent(monitor);
|
||||||
default:
|
default:
|
||||||
return this.buildDefaultContent(monitor, clientHost);
|
return this.buildDefaultContent(monitor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildMonitorDownContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, clientHost: string): NotificationContent {
|
private buildMonitorDownContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): NotificationContent {
|
||||||
const title = `Monitor Down: ${monitor.name}`;
|
const title = `Monitor Down: ${monitor.name}`;
|
||||||
const summary = `Monitor "${monitor.name}" is currently down and unreachable.`;
|
const summary = `Monitor "${monitor.name}" is currently down and unreachable.`;
|
||||||
const details = [`URL: ${monitor.url}`, `Status: Down`, `Type: ${monitor.type}`];
|
const details = [`URL: ${monitor.url}`, `Status: Down`, `Type: ${monitor.type}`];
|
||||||
@@ -137,7 +131,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildMonitorUpContent(monitor: Monitor, clientHost: string): NotificationContent {
|
private buildMonitorUpContent(monitor: Monitor): NotificationContent {
|
||||||
const title = `Monitor Recovered: ${monitor.name}`;
|
const title = `Monitor Recovered: ${monitor.name}`;
|
||||||
const summary = `Monitor "${monitor.name}" is back up and operational.`;
|
const summary = `Monitor "${monitor.name}" is back up and operational.`;
|
||||||
const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`];
|
const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`];
|
||||||
@@ -150,12 +144,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildThresholdBreachContent(
|
private buildThresholdBreachContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse<HardwareStatusPayload>): NotificationContent {
|
||||||
monitor: Monitor,
|
|
||||||
monitorStatusResponse: MonitorStatusResponse,
|
|
||||||
decision: MonitorActionDecision,
|
|
||||||
clientHost: string
|
|
||||||
): NotificationContent {
|
|
||||||
const title = `Threshold Breach: ${monitor.name}`;
|
const title = `Threshold Breach: ${monitor.name}`;
|
||||||
const summary = `Monitor "${monitor.name}" has breached one or more thresholds.`;
|
const summary = `Monitor "${monitor.name}" has breached one or more thresholds.`;
|
||||||
const details = [`URL: ${monitor.url}`, `Status: Breached`, `Type: ${monitor.type}`];
|
const details = [`URL: ${monitor.url}`, `Status: Breached`, `Type: ${monitor.type}`];
|
||||||
@@ -171,7 +160,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildThresholdResolvedContent(monitor: Monitor, clientHost: string): NotificationContent {
|
private buildThresholdResolvedContent(monitor: Monitor): NotificationContent {
|
||||||
const title = `Thresholds Resolved: ${monitor.name}`;
|
const title = `Thresholds Resolved: ${monitor.name}`;
|
||||||
const summary = `Monitor "${monitor.name}" thresholds have returned to normal.`;
|
const summary = `Monitor "${monitor.name}" thresholds have returned to normal.`;
|
||||||
const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`];
|
const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`];
|
||||||
@@ -184,7 +173,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildDefaultContent(monitor: Monitor, clientHost: string): NotificationContent {
|
private buildDefaultContent(monitor: Monitor): NotificationContent {
|
||||||
return {
|
return {
|
||||||
title: `Monitor: ${monitor.name}`,
|
title: `Monitor: ${monitor.name}`,
|
||||||
summary: `Status update for monitor "${monitor.name}".`,
|
summary: `Status update for monitor "${monitor.name}".`,
|
||||||
@@ -193,7 +182,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public extractThresholdBreaches(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): ThresholdBreach[] {
|
public extractThresholdBreaches(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse<HardwareStatusPayload>): ThresholdBreach[] {
|
||||||
const breaches: ThresholdBreach[] = [];
|
const breaches: ThresholdBreach[] = [];
|
||||||
|
|
||||||
// Check if this is a hardware monitor with threshold data
|
// Check if this is a hardware monitor with threshold data
|
||||||
@@ -202,7 +191,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cast to HardwareStatusPayload type
|
// Cast to HardwareStatusPayload type
|
||||||
const payload = monitorStatusResponse.payload as { data?: any };
|
const payload = monitorStatusResponse.payload;
|
||||||
const hardware = payload.data;
|
const hardware = payload.data;
|
||||||
|
|
||||||
if (!hardware) {
|
if (!hardware) {
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
const SERVICE_NAME = "DiscordProvider";
|
const SERVICE_NAME = "DiscordProvider";
|
||||||
import type { Notification } from "@/types/index.js";
|
import type { AlertDiscordPayload, DiscordEmbedField, Notification } from "@/types/index.js";
|
||||||
import { INotificationProvider } from "@/service/index.js";
|
import { INotificationProvider } from "@/service/index.js";
|
||||||
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
import type { NotificationMessage, NotificationSeverity } from "@/types/notificationMessage.js";
|
||||||
import got from "got";
|
import got from "got";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
|
||||||
export class DiscordProvider implements INotificationProvider {
|
export class DiscordProvider implements INotificationProvider {
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
|
|
||||||
constructor(logger: any) {
|
constructor(logger: ILogger) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,9 +37,6 @@ export class DiscordProvider implements INotificationProvider {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* New unified message format implementation
|
|
||||||
*/
|
|
||||||
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
|
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
|
||||||
if (!notification.address) {
|
if (!notification.address) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
@@ -71,18 +69,17 @@ export class DiscordProvider implements INotificationProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildDiscordEmbed(message: NotificationMessage): any {
|
private buildDiscordEmbed(message: NotificationMessage): AlertDiscordPayload {
|
||||||
// Map severity to Discord embed colors
|
const colorMap: Record<NotificationSeverity, number> = {
|
||||||
const colorMap: Record<string, number> = {
|
|
||||||
critical: 0xdc2626, // red-600
|
critical: 0xdc2626, // red-600
|
||||||
warning: 0xf59e0b, // amber-500
|
warning: 0xf59e0b, // amber-500
|
||||||
info: 0x3b82f6, // blue-500
|
info: 0x3b82f6, // blue-500
|
||||||
success: 0x10b981, // green-500
|
success: 0x10b981, // green-500
|
||||||
};
|
};
|
||||||
|
|
||||||
const color = colorMap[message.severity] || colorMap.info;
|
const color = colorMap[message.severity] ?? colorMap.info;
|
||||||
|
|
||||||
const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
|
const fields: Array<DiscordEmbedField> = [];
|
||||||
|
|
||||||
// Add monitor details
|
// Add monitor details
|
||||||
fields.push({
|
fields.push({
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
const SERVICE_NAME = "EmailProvider";
|
const SERVICE_NAME = "EmailProvider";
|
||||||
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
|
import type { Notification } from "@/types/index.js";
|
||||||
import { INotificationProvider } from "@/service/index.js";
|
import { INotificationProvider } from "@/service/index.js";
|
||||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
|
||||||
import { buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js";
|
import { buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||||
|
import type { ILogger } from "@/utils/logger.js";
|
||||||
|
import { IEmailService } from "@/service/infrastructure/emailService.js";
|
||||||
export class EmailProvider implements INotificationProvider {
|
export class EmailProvider implements INotificationProvider {
|
||||||
private emailService: any;
|
private emailService: IEmailService;
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
|
|
||||||
constructor(emailService: any, logger: any) {
|
constructor(emailService: IEmailService, logger: ILogger) {
|
||||||
this.emailService = emailService;
|
this.emailService = emailService;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,24 @@ export class EmailProvider implements INotificationProvider {
|
|||||||
const subject = "Test notification";
|
const subject = "Test notification";
|
||||||
const html = await buildTestEmail(this.emailService);
|
const html = await buildTestEmail(this.emailService);
|
||||||
|
|
||||||
|
if (!notification.address) {
|
||||||
|
this.logger.warn({
|
||||||
|
message: "Missing address",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "sendTestAlert",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!html) {
|
||||||
|
this.logger.warn({
|
||||||
|
message: "Failed to build test email content",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "sendTestAlert",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const messageId = await this.emailService.sendEmail(notification.address, subject, html);
|
const messageId = await this.emailService.sendEmail(notification.address, subject, html);
|
||||||
if (!messageId) {
|
if (!messageId) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
@@ -38,6 +56,15 @@ export class EmailProvider implements INotificationProvider {
|
|||||||
const subject = this.buildSubject(message);
|
const subject = this.buildSubject(message);
|
||||||
const html = await this.buildEmailFromMessage(message);
|
const html = await this.buildEmailFromMessage(message);
|
||||||
|
|
||||||
|
if (!html) {
|
||||||
|
this.logger.warn({
|
||||||
|
message: "Failed to build email content",
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
method: "sendMessage",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const messageId = await this.emailService.sendEmail(notification.address, subject, html);
|
const messageId = await this.emailService.sendEmail(notification.address, subject, html);
|
||||||
if (!messageId) {
|
if (!messageId) {
|
||||||
this.logger.warn({
|
this.logger.warn({
|
||||||
@@ -65,7 +92,7 @@ export class EmailProvider implements INotificationProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildEmailFromMessage(message: NotificationMessage): Promise<string> {
|
private async buildEmailFromMessage(message: NotificationMessage): Promise<string | undefined> {
|
||||||
const context = {
|
const context = {
|
||||||
title: message.content.title,
|
title: message.content.title,
|
||||||
summary: message.content.summary,
|
summary: message.content.summary,
|
||||||
@@ -88,16 +115,6 @@ export class EmailProvider implements INotificationProvider {
|
|||||||
|
|
||||||
const html = await this.emailService.buildEmail("unifiedNotificationTemplate", context);
|
const html = await this.emailService.buildEmail("unifiedNotificationTemplate", context);
|
||||||
|
|
||||||
this.logger.info({
|
|
||||||
message: "[DEBUG] Email HTML generated",
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
method: "buildEmailFromMessage",
|
|
||||||
details: {
|
|
||||||
htmlLength: html?.length || 0,
|
|
||||||
htmlPreview: html?.substring(0, 200),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
const SERVICE_NAME = "MatrixProvider";
|
const SERVICE_NAME = "MatrixProvider";
|
||||||
import got from "got";
|
import got from "got";
|
||||||
import type { INotificationProvider } from "@/service/index.js";
|
import type { INotificationProvider } from "@/service/index.js";
|
||||||
import type { Notification } from "@/types/index.js";
|
import type { AlertMatrixPayload, Notification } from "@/types/index.js";
|
||||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||||
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
|
||||||
export class MatrixProvider implements INotificationProvider {
|
export class MatrixProvider implements INotificationProvider {
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
|
|
||||||
constructor(logger: any) {
|
constructor(logger: ILogger) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,9 +43,6 @@ export class MatrixProvider implements INotificationProvider {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* New unified message format - builds Matrix HTML message from NotificationMessage
|
|
||||||
*/
|
|
||||||
sendMessage = async (notification: Notification, message: NotificationMessage): Promise<boolean> => {
|
sendMessage = async (notification: Notification, message: NotificationMessage): Promise<boolean> => {
|
||||||
const { homeserverUrl, accessToken, roomId } = notification;
|
const { homeserverUrl, accessToken, roomId } = notification;
|
||||||
|
|
||||||
@@ -87,7 +85,7 @@ export class MatrixProvider implements INotificationProvider {
|
|||||||
* Returns both plain text and HTML formatted versions
|
* Returns both plain text and HTML formatted versions
|
||||||
* Matrix supports HTML subset for rich formatting
|
* Matrix supports HTML subset for rich formatting
|
||||||
*/
|
*/
|
||||||
private buildMatrixMessage(message: NotificationMessage): { plainText: string; htmlText: string } {
|
private buildMatrixMessage(message: NotificationMessage): AlertMatrixPayload {
|
||||||
const plainLines: string[] = [];
|
const plainLines: string[] = [];
|
||||||
const htmlLines: string[] = [];
|
const htmlLines: string[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
const SERVICE_NAME = "PagerDutyProvider";
|
const SERVICE_NAME = "PagerDutyProvider";
|
||||||
import got from "got";
|
import got from "got";
|
||||||
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
|
import type { Notification } from "@/types/index.js";
|
||||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
|
||||||
import { INotificationProvider } from "@/service/index.js";
|
import { INotificationProvider } from "@/service/index.js";
|
||||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||||
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
import { AlertPagerDutyPayload } from "@/types/index.js";
|
||||||
|
|
||||||
export class PagerDutyProvider implements INotificationProvider {
|
export class PagerDutyProvider implements INotificationProvider {
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
|
|
||||||
constructor(logger: any) {
|
constructor(logger: ILogger) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ export class PagerDutyProvider implements INotificationProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildPagerDutyPayload(notification: Notification, message: NotificationMessage): any {
|
private buildPagerDutyPayload(notification: Notification, message: NotificationMessage): AlertPagerDutyPayload {
|
||||||
// Map our notification type to PagerDuty event_action
|
// Map our notification type to PagerDuty event_action
|
||||||
const eventAction = message.type === "monitor_up" || message.type === "threshold_resolved" ? "resolve" : "trigger";
|
const eventAction = message.type === "monitor_up" || message.type === "threshold_resolved" ? "resolve" : "trigger";
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ export class PagerDutyProvider implements INotificationProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build custom details
|
// Build custom details
|
||||||
const customDetails: any = {
|
const customDetails: Record<string, unknown> = {
|
||||||
monitor_name: message.monitor.name,
|
monitor_name: message.monitor.name,
|
||||||
monitor_url: message.monitor.url,
|
monitor_url: message.monitor.url,
|
||||||
monitor_type: message.monitor.type,
|
monitor_type: message.monitor.type,
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
const SERVICE_NAME = "SlackProvider";
|
const SERVICE_NAME = "SlackProvider";
|
||||||
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
|
import type { Notification } from "@/types/index.js";
|
||||||
import { INotificationProvider } from "@/service/index.js";
|
import { INotificationProvider } from "@/service/index.js";
|
||||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
|
||||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||||
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||||
import got, { HTTPError } from "got";
|
import got, { HTTPError } from "got";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
|
||||||
export class SlackProvider implements INotificationProvider {
|
export class SlackProvider implements INotificationProvider {
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
|
|
||||||
constructor(logger: any) {
|
constructor(logger: ILogger) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ export class SlackProvider implements INotificationProvider {
|
|||||||
* Uses Slack's rich formatting with sections, fields, and context blocks
|
* Uses Slack's rich formatting with sections, fields, and context blocks
|
||||||
*/
|
*/
|
||||||
private buildSlackPayload(message: NotificationMessage): object {
|
private buildSlackPayload(message: NotificationMessage): object {
|
||||||
const blocks: any[] = [];
|
const blocks: unknown[] = [];
|
||||||
|
|
||||||
// Determine color based on severity
|
// Determine color based on severity
|
||||||
const colorMap = {
|
const colorMap = {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js";
|
import { IEmailService } from "@/service/infrastructure/emailService.js";
|
||||||
// Test notification helpers - used by all providers for test alerts
|
export const buildTestEmail = async (emailService: IEmailService) => {
|
||||||
export const buildTestEmail = async (emailService: any) => {
|
|
||||||
const context = { testName: "Monitoring System" };
|
const context = { testName: "Monitoring System" };
|
||||||
const html = await emailService.buildEmail("testEmailTemplate", context);
|
const html = await emailService.buildEmail("testEmailTemplate", context);
|
||||||
return html;
|
return html;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
const SERVICE_NAME = "WebhookProvider";
|
const SERVICE_NAME = "WebhookProvider";
|
||||||
import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js";
|
import type { Notification } from "@/types/index.js";
|
||||||
import { INotificationProvider } from "@/service/index.js";
|
import { INotificationProvider } from "@/service/index.js";
|
||||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
|
||||||
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
import type { NotificationMessage } from "@/types/notificationMessage.js";
|
||||||
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||||
import got from "got";
|
import got from "got";
|
||||||
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
|
||||||
export class WebhookProvider implements INotificationProvider {
|
export class WebhookProvider implements INotificationProvider {
|
||||||
private logger: any;
|
private logger: ILogger;
|
||||||
|
|
||||||
constructor(logger: any) {
|
constructor(logger: ILogger) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +46,6 @@ export class WebhookProvider implements INotificationProvider {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Build webhook payload from NotificationMessage
|
|
||||||
* Format: { text: string, severity: string, monitor: object, details: object }
|
|
||||||
*/
|
|
||||||
private buildWebhookPayload(message: NotificationMessage): object {
|
private buildWebhookPayload(message: NotificationMessage): object {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
|||||||
Loaded 100 of 129 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user