Compare commits

..
Author SHA1 Message Date
Alex Holliday 35c022df0f geo job 2026-02-23 00:00:45 +00:00
Alex Holliday 19fc85ae2d front end 2026-02-22 23:25:04 +00:00
Alex Holliday b4f8fa7d72 aggregation 2026-02-22 21:15:02 +00:00
Alex Holliday ef5629d8e4 remove unused 2026-02-22 20:35:13 +00:00
Alex Holliday 4018125224 initial commit 2026-02-22 20:28:42 +00:00
127 changed files with 2323 additions and 5459 deletions

No files matched your search

+1 -3
View File
@@ -5,6 +5,4 @@
mongo mongo
node_modules/ node_modules/
docs/architecture docs/architecture
docs/reviews docs/reviews
docs/todo
docs/frontend
+1 -1
View File
@@ -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 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). 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).
## User's guide ## User's guide
+24 -640
View File
File diff suppressed because it is too large. Load diff
-3
View File
@@ -25,7 +25,6 @@
"@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",
@@ -34,7 +33,6 @@
"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",
@@ -43,7 +41,6 @@
"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",
+23 -36
View File
@@ -4,52 +4,39 @@ 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 (
<SWRConfig value={swrConfig}> <ThemeProvider theme={theme}>
<ThemeProvider theme={theme}> <CssBaseline />
<CssBaseline /> <GlobalStyles
<GlobalStyles styles={{
styles={{ body: {
body: { backgroundColor: theme.palette.background.default,
backgroundColor: theme.palette.background.default, },
}, }}
}} />
/> <AppLayout>
<AppLayout> <Routes />
<Routes /> </AppLayout>
</AppLayout> <ToastContainer
<ToastContainer newestOnTop={true}
newestOnTop={true} theme={mode}
theme={mode} style={
style={ {
{ "--toastify-color-progress-light": "#7C8BA1",
"--toastify-color-progress-light": "#7C8BA1", "--toastify-color-progress-dark": "#7C8BA1",
"--toastify-color-progress-dark": "#7C8BA1", } as CSSProperties
} as CSSProperties }
} />
/> </ThemeProvider>
</ThemeProvider>
</SWRConfig>
); );
} }
@@ -2,7 +2,6 @@ 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";
@@ -27,7 +26,7 @@ export const BaseChart = ({
return ( return (
<BaseBox <BaseBox
sx={{ sx={{
padding: padding ?? theme.spacing(LAYOUT.MD), padding: padding ?? theme.spacing(8),
display: "flex", display: "flex",
flex: 1, flex: 1,
width: width, width: width,
@@ -35,13 +34,13 @@ export const BaseChart = ({
}} }}
> >
<Stack <Stack
gap={theme.spacing(LAYOUT.MD)} gap={theme.spacing(8)}
flex={1} flex={1}
> >
<Stack <Stack
direction="row" direction="row"
alignItems={"center"} alignItems={"center"}
gap={theme.spacing(LAYOUT.XS)} gap={theme.spacing(4)}
> >
{icon && ( {icon && (
<BaseBox <BaseBox
@@ -3,7 +3,6 @@ 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,
@@ -92,7 +91,7 @@ export const BasePage = ({
return ( return (
<Stack <Stack
spacing={theme.spacing(LAYOUT.LG)} spacing={theme.spacing(10)}
{...props} {...props}
> >
<Breadcrumb breadcrumbOverride={breadcrumbOverride} /> <Breadcrumb breadcrumbOverride={breadcrumbOverride} />
@@ -221,7 +220,7 @@ export const BaseAuthPage = ({
return ( return (
<BasePage <BasePage
breadcrumbOverride={[]} breadcrumbOverride={[]}
gap={theme.spacing(LAYOUT.MD)} gap={theme.spacing(8)}
alignItems={"center"} alignItems={"center"}
justifyContent={"center"} justifyContent={"center"}
minHeight="100vh" minHeight="100vh"
@@ -230,7 +229,7 @@ export const BaseAuthPage = ({
> >
<HeaderAuthControls <HeaderAuthControls
hideLogo hideLogo
py={theme.spacing(LAYOUT.XS)} py={theme.spacing(4)}
position={"absolute"} position={"absolute"}
top={0} top={0}
left={0} left={0}
@@ -244,14 +243,14 @@ export const BaseAuthPage = ({
<Stack alignItems={"center"}> <Stack alignItems={"center"}>
<Typography <Typography
variant="h1" variant="h1"
mb={theme.spacing(SPACING.LG)} mb={theme.spacing(2)}
> >
{title} {title}
</Typography> </Typography>
<Typography variant="h2">{subtitle}</Typography> <Typography variant="h2">{subtitle}</Typography>
</Stack> </Stack>
<Stack <Stack
gap={theme.spacing(LAYOUT.MD)} gap={theme.spacing(8)}
width={{ width={{
xs: "80%", xs: "80%",
md: "25%", md: "25%",
@@ -2,7 +2,6 @@ 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,
@@ -22,7 +21,7 @@ export const BulletPointCheck = ({
return ( return (
<Stack <Stack
direction="row" direction="row"
gap={theme.spacing(LAYOUT.SM)} gap={theme.spacing(6)}
alignItems="center" alignItems="center"
> >
<Check <Check
@@ -2,7 +2,6 @@ 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";
@@ -28,7 +27,7 @@ export const BaseFallback = ({ children, ...props }: BaseFallbackProps) => {
lg: "50%", lg: "50%",
xl: "40%", xl: "40%",
}} }}
padding={{ xs: theme.spacing(LAYOUT.MD), md: theme.spacing(LAYOUT.XXL) }} padding={{ xs: theme.spacing(8), md: theme.spacing(16) }}
bgcolor={theme.palette.background.paper} bgcolor={theme.palette.background.paper}
border={1} border={1}
borderColor={theme.palette.divider} borderColor={theme.palette.divider}
@@ -56,7 +55,7 @@ export const ErrorFallback = ({
<BaseFallback> <BaseFallback>
<Typography <Typography
variant="h1" variant="h1"
marginY={theme.spacing(LAYOUT.XS)} marginY={theme.spacing(4)}
color={theme.palette.text.secondary} color={theme.palette.text.secondary}
> >
{title} {title}
@@ -82,7 +81,7 @@ export const EmptyFallback = ({
return ( return (
<BaseFallback> <BaseFallback>
<Stack <Stack
gap={theme.spacing(LAYOUT.MD)} gap={theme.spacing(10)}
zIndex={1} zIndex={1}
alignItems="center" alignItems="center"
> >
@@ -90,7 +89,7 @@ export const EmptyFallback = ({
<Stack <Stack
sx={{ sx={{
flexWrap: "wrap", flexWrap: "wrap",
gap: theme.spacing(SPACING.MD), gap: theme.spacing(2),
maxWidth: "1100px", maxWidth: "1100px",
width: "100%", width: "100%",
}} }}
@@ -135,7 +134,7 @@ export const EmptyMonitorFallback = ({
return ( return (
<BaseFallback> <BaseFallback>
<Stack <Stack
gap={theme.spacing(LAYOUT.MD)} gap={theme.spacing(10)}
zIndex={1} zIndex={1}
alignItems="center" alignItems="center"
> >
@@ -148,7 +147,7 @@ export const EmptyMonitorFallback = ({
<Stack <Stack
sx={{ sx={{
flexWrap: "wrap", flexWrap: "wrap",
gap: theme.spacing(SPACING.MD), gap: theme.spacing(2),
maxWidth: "1100px", maxWidth: "1100px",
width: "100%", width: "100%",
}} }}
@@ -3,7 +3,6 @@ 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,
@@ -19,10 +18,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.shape.borderRadius} borderRadius={theme.spacing(2)}
> >
<Box <Box
padding={theme.spacing(LAYOUT.XXL)} padding={theme.spacing(15)}
borderRight={isSmall ? 0 : 1} borderRight={isSmall ? 0 : 1}
borderBottom={isSmall ? 1 : 0} borderBottom={isSmall ? 1 : 0}
borderColor={theme.palette.divider} borderColor={theme.palette.divider}
@@ -38,7 +37,7 @@ export const SplitBox = ({
</Box> </Box>
<Box <Box
flex={1} flex={1}
padding={theme.spacing(LAYOUT.XXL)} padding={theme.spacing(15)}
> >
{right} {right}
</Box> </Box>
@@ -61,7 +60,7 @@ export const ConfigBox = ({
return ( return (
<SplitBox <SplitBox
left={ left={
<Stack spacing={theme.spacing(LAYOUT.XS)}> <Stack spacing={theme.spacing(4)}>
<Typography <Typography
textTransform={"capitalize"} textTransform={"capitalize"}
component="h2" component="h2"
+10 -11
View File
@@ -29,7 +29,6 @@ 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";
@@ -91,20 +90,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(LAYOUT.XS)}> <Stack spacing={theme.spacing(4)}>
{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(LAYOUT.XS)} spacing={theme.spacing(4)}
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(LAYOUT.XS), padding: theme.spacing(4),
cursor: onRowClick ? "pointer" : "default", cursor: onRowClick ? "pointer" : "default",
}} }}
key={key} key={key}
@@ -156,7 +155,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(LAYOUT.MD), paddingLeft: theme.spacing(8),
}, },
"& .MuiTableCell-root": { "& .MuiTableCell-root": {
borderBottom: `1px solid ${theme.palette.divider}`, borderBottom: `1px solid ${theme.palette.divider}`,
@@ -169,13 +168,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(SPACING.LG)} ${theme.spacing(LAYOUT.MD)}`, padding: `${theme.spacing(2)} ${theme.spacing(8)}`,
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(LAYOUT.MD)} ${theme.spacing(LAYOUT.MD)}`, padding: `${theme.spacing(7)} ${theme.spacing(8)}`,
fontSize: theme.typography.fontSize, fontSize: theme.typography.fontSize,
overflowX: "hidden", overflowX: "hidden",
}, },
@@ -515,7 +514,7 @@ export const Pagination = ({ ...props }: PaginationProps) => {
display: "grid", display: "grid",
gridTemplateColumns: "1fr 1fr", gridTemplateColumns: "1fr 1fr",
gridAutoRows: "auto", gridAutoRows: "auto",
rowGap: theme.spacing(SPACING.SM), rowGap: theme.spacing(1),
alignItems: "center", alignItems: "center",
justifyItems: "center", justifyItems: "center",
paddingLeft: 0, paddingLeft: 0,
@@ -557,8 +556,8 @@ const EmptyView = ({ text, positive }: { text?: string; positive?: boolean }) =>
alignItems={"center"} alignItems={"center"}
justifyContent={"center"} justifyContent={"center"}
sx={{ sx={{
py: theme.spacing(LAYOUT.XL), py: theme.spacing(12),
px: theme.spacing(LAYOUT.XS), px: theme.spacing(4),
borderWidth: 1, borderWidth: 1,
borderStyle: "solid", borderStyle: "solid",
borderColor: theme.palette.divider, borderColor: theme.palette.divider,
@@ -575,7 +574,7 @@ const EmptyView = ({ text, positive }: { text?: string; positive?: boolean }) =>
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
mb: theme.spacing(SPACING.LG), mb: theme.spacing(2),
}} }}
> >
<Icon <Icon
@@ -5,17 +5,8 @@ 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", "grpc"]; const types = ["http", "ping", "port", "docker", "game"];
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"];
@@ -39,7 +30,6 @@ 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 !== "";
@@ -60,7 +50,7 @@ export const ControlsFilter = ({
key={type} key={type}
value={type} value={type}
> >
<Typography>{typeDisplayNames[type] ?? type}</Typography> <Typography textTransform={"capitalize"}>{type}</Typography>
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
@@ -98,7 +88,7 @@ export const ControlsFilter = ({
variant="contained" variant="contained"
onClick={onClearFilters} onClick={onClearFilters}
> >
{t("common.buttons.clearFilters")} Clear Filters
</Button> </Button>
)} )}
</Stack> </Stack>
@@ -1,203 +0,0 @@
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>
</>
);
};
@@ -1,54 +0,0 @@
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>
);
};
@@ -2,7 +2,6 @@ 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";
@@ -31,8 +30,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(SPACING.LG), py: theme.spacing(2),
px: theme.spacing(LAYOUT.XS), px: theme.spacing(4),
}} }}
> >
<Typography <Typography
@@ -55,12 +54,12 @@ export const HistogramPageSpeedScoresTooltip = ({
key={`${key}-tooltip`} key={`${key}-tooltip`}
direction="row" direction="row"
alignItems="center" alignItems="center"
gap={theme.spacing(SPACING.XXL)} gap={theme.spacing(3)}
mt={theme.spacing(SPACING.SM)} mt={theme.spacing(1)}
> >
<Box <Box
width={theme.spacing(LAYOUT.XS)} width={theme.spacing(4)}
height={theme.spacing(LAYOUT.XS)} height={theme.spacing(4)}
sx={{ borderRadius: "50%", backgroundColor: dotColor }} sx={{ borderRadius: "50%", backgroundColor: dotColor }}
/> />
<Typography <Typography
-2
View File
@@ -1,8 +1,6 @@
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";
+8 -14
View File
@@ -5,7 +5,6 @@ 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";
@@ -65,9 +64,9 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
}); });
const menuItemSx = { const menuItemSx = {
gap: theme.spacing(LAYOUT.MD), gap: theme.spacing(2),
borderRadius: theme.shape.borderRadius, borderRadius: theme.shape.borderRadius,
pl: theme.spacing(LAYOUT.XS), pl: theme.spacing(4),
"& svg": { stroke: theme.palette.text.secondary }, "& svg": { stroke: theme.palette.text.secondary },
}; };
@@ -75,9 +74,9 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
<Stack <Stack
direction="row" direction="row"
alignItems="center" alignItems="center"
py={theme.spacing(LAYOUT.XS)} py={theme.spacing(4)}
px={theme.spacing(LAYOUT.MD)} px={theme.spacing(8)}
gap={theme.spacing(LAYOUT.MD)} gap={theme.spacing(4)}
> >
<Avatar <Avatar
small small
@@ -89,7 +88,7 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
<Stack <Stack
direction="row" direction="row"
alignItems="center" alignItems="center"
gap={theme.spacing(SPACING.MD)} gap={theme.spacing(2)}
minWidth={0} minWidth={0}
flex={1} flex={1}
> >
@@ -133,15 +132,10 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
disableScrollLock disableScrollLock
anchorOrigin={{ vertical: "top", horizontal: "right" }} anchorOrigin={{ vertical: "top", horizontal: "right" }}
slotProps={{ slotProps={{
paper: { paper: { sx: { mt: theme.spacing(-4), ml: collapsed ? theme.spacing(2) : 0 } },
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(LAYOUT.XS) }} sx={{ ml: theme.spacing(4) }}
> >
{collapsed && ( {collapsed && (
<Box <Box
+12 -10
View File
@@ -1,7 +1,6 @@
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";
@@ -35,22 +34,25 @@ export const StarPrompt = ({
return ( return (
<Stack <Stack
direction="column" direction="column"
gap={theme.spacing(LAYOUT.MD)} sx={{
padding={theme.spacing(LAYOUT.SM)} width: "100%",
width={"100%"} padding: `${theme.spacing(6)} ${theme.spacing(6)}`,
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(LAYOUT.XS)} pl={theme.spacing(4)}
> >
<Typography <Typography
variant="subtitle2" variant="subtitle2"
mt={theme.spacing(SPACING.XXL)} mt={theme.spacing(3)}
> >
{t("components.sidebar.starPrompt.title")} {t("components.sidebar.starPrompt.title")}
</Typography> </Typography>
@@ -76,7 +78,7 @@ export const StarPrompt = ({
<Typography <Typography
sx={{ sx={{
mb: 1, mb: 1,
px: theme.spacing(LAYOUT.XS), px: theme.spacing(4),
}} }}
> >
{t("components.sidebar.starPrompt.description")} {t("components.sidebar.starPrompt.description")}
@@ -94,7 +96,7 @@ export const StarPrompt = ({
"&:hover": { "&:hover": {
opacity: 0.8, opacity: 0.8,
}, },
pl: theme.spacing(LAYOUT.XS), pl: theme.spacing(4),
filter: mode === "dark" ? "invert(1)" : "none", filter: mode === "dark" ? "invert(1)" : "none",
}} }}
/> />
+7 -8
View File
@@ -3,7 +3,6 @@ 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";
@@ -66,9 +65,9 @@ export const Sidebar = () => {
left={0} left={0}
minHeight={"100vh"} minHeight={"100vh"}
maxHeight={"100vh"} maxHeight={"100vh"}
paddingTop={theme.spacing(LAYOUT.SM)} paddingTop={theme.spacing(6)}
paddingBottom={theme.spacing(LAYOUT.SM)} paddingBottom={theme.spacing(6)}
gap={theme.spacing(LAYOUT.SM)} gap={theme.spacing(6)}
borderRight={`1px solid ${theme.palette.divider}`} borderRight={`1px solid ${theme.palette.divider}`}
width={width} width={width}
sx={{ sx={{
@@ -82,13 +81,13 @@ export const Sidebar = () => {
component="nav" component="nav"
disablePadding disablePadding
sx={{ sx={{
px: theme.spacing(LAYOUT.SM), px: theme.spacing(6),
flex: 1, flex: 1,
}} }}
> >
<Logo <Logo
pt={theme.spacing(LAYOUT.MD)} pt={theme.spacing(8)}
pb={theme.spacing(LAYOUT.MD)} pb={theme.spacing(10)}
/> />
{menu.map((item) => { {menu.map((item) => {
const selected = location.pathname.startsWith(`/${item.path}`); const selected = location.pathname.startsWith(`/${item.path}`);
@@ -106,7 +105,7 @@ export const Sidebar = () => {
<List <List
component="nav" component="nav"
disablePadding disablePadding
sx={{ px: theme.spacing(LAYOUT.SM) }} sx={{ px: theme.spacing(6) }}
> >
{bottomMenu.map((item) => { {bottomMenu.map((item) => {
const selected = location.pathname.startsWith(`/${item.path}`); const selected = location.pathname.startsWith(`/${item.path}`);
+6 -7
View File
@@ -4,7 +4,6 @@ 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;
@@ -18,7 +17,7 @@ const fetcher = async <T>(url: string, config?: AxiosRequestConfig) => {
}; };
export const useGet = <T>( export const useGet = <T>(
url: string | null | undefined, url: string | null,
axiosConfig?: AxiosRequestConfig, axiosConfig?: AxiosRequestConfig,
swrConfig?: SWRConfiguration swrConfig?: SWRConfiguration
) => { ) => {
@@ -62,7 +61,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";
logger.error("POST request failed", err, { endpoint, body }); console.error(err);
toastError(errMsg); toastError(errMsg);
setError(errMsg); setError(errMsg);
return null; return null;
@@ -97,7 +96,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) {
logger.error("PATCH request failed", err, { endpoint, body }); console.error(err);
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);
@@ -133,7 +132,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) {
logger.error("PUT request failed", err, { endpoint, body }); console.error(err);
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);
@@ -168,7 +167,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) {
logger.error("DELETE request failed", err, { endpoint }); console.error(err);
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);
@@ -202,7 +201,7 @@ export const useLazyGet = <R = any>() => {
}); });
return res.data; return res.data;
} catch (err: any) { } catch (err: any) {
logger.error("GET request failed", err, { endpoint }); console.error(err);
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);
+1 -15
View File
@@ -14,7 +14,7 @@ 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, geoCheckEnabled: data?.geoCheckEnabled || false,
geoCheckLocations: data?.geoCheckLocations || [], geoCheckLocations: data?.geoCheckLocations || [],
geoCheckInterval: data?.geoCheckInterval || 300000, geoCheckInterval: data?.geoCheckInterval || 300000,
}); });
@@ -40,8 +40,6 @@ 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":
@@ -75,16 +73,6 @@ 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,
@@ -115,8 +103,6 @@ export const useMonitorForm = ({
matchMethod: "", matchMethod: "",
expectedValue: "", expectedValue: "",
jsonPath: "", jsonPath: "",
basicAuthUser: "",
basicAuthPassword: "",
}; };
} }
+4 -16
View File
@@ -22,22 +22,10 @@ export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) =>
systemEmailTLSServername: data?.systemEmailTLSServername || "", systemEmailTLSServername: data?.systemEmailTLSServername || "",
systemEmailPort: data?.systemEmailPort, systemEmailPort: data?.systemEmailPort,
globalThresholds: { globalThresholds: {
cpu: cpu: data?.globalThresholds?.cpu ?? 0,
data?.globalThresholds?.cpu && data.globalThresholds.cpu >= 1 memory: data?.globalThresholds?.memory ?? 0,
? data.globalThresholds.cpu disk: data?.globalThresholds?.disk ?? 0,
: 80, temperature: data?.globalThresholds?.temperature ?? 0,
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,6 +5,7 @@ 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";
@@ -28,6 +29,7 @@ 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>[] = [
@@ -107,6 +109,9 @@ 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
+36 -191
View File
@@ -1,6 +1,5 @@
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";
@@ -16,7 +15,6 @@ 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 {
@@ -29,10 +27,10 @@ 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";
import { GeoCheckLocations } from "@/Types/Monitor";
import type { Notification } from "@/Types/Notification"; import type { Notification } from "@/Types/Notification";
import type { MonitorFormData } from "@/Validation/monitor"; import type { MonitorFormData } from "@/Validation/monitor";
@@ -44,8 +42,6 @@ interface GeneralSettingsConfig {
showPort: boolean; showPort: boolean;
showGameSelect: boolean; showGameSelect: boolean;
showSecret: boolean; showSecret: boolean;
showGrpcServiceName: boolean;
showIgnoreTls: boolean;
} }
const getGeneralSettingsConfig = ( const getGeneralSettingsConfig = (
@@ -61,8 +57,6 @@ 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"),
@@ -72,8 +66,6 @@ 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"),
@@ -83,8 +75,6 @@ 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"),
@@ -94,8 +84,6 @@ 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"),
@@ -105,19 +93,6 @@ 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"),
@@ -127,8 +102,6 @@ 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"),
@@ -138,8 +111,6 @@ 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;
@@ -255,7 +226,7 @@ const CreateMonitorPage = () => {
}; };
const onError = (errors: unknown) => { const onError = (errors: unknown) => {
logger.debug("Monitor creation validation errors", errors); console.log(errors);
}; };
return ( return (
@@ -282,7 +253,7 @@ const CreateMonitorPage = () => {
<FormControl error={!!fieldState.error}> <FormControl error={!!fieldState.error}>
<RadioGroup <RadioGroup
{...field} {...field}
sx={{ gap: theme.spacing(LAYOUT.MD) }} sx={{ gap: theme.spacing(6) }}
> >
<RadioWithDescription <RadioWithDescription
value="http" value="http"
@@ -319,13 +290,6 @@ 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>
)} )}
@@ -338,7 +302,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(LAYOUT.MD)}> <Stack spacing={theme.spacing(8)}>
{/* URL/Host/Container field - not shown for hardware */} {/* URL/Host/Container field - not shown for hardware */}
{generalSettingsConfig.showUrl && ( {generalSettingsConfig.showUrl && (
<Controller <Controller
@@ -414,30 +378,6 @@ 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
@@ -557,7 +497,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(LAYOUT.MD)}> <Stack spacing={theme.spacing(8)}>
<Controller <Controller
name="cpuAlertThreshold" name="cpuAlertThreshold"
control={control} control={control}
@@ -639,7 +579,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(LAYOUT.MD)}> <Stack spacing={theme.spacing(12)}>
<Controller <Controller
name="statusWindowSize" name="statusWindowSize"
control={control} control={control}
@@ -691,7 +631,7 @@ const CreateMonitorPage = () => {
(field.value ?? []).includes(n.id) (field.value ?? []).includes(n.id)
); );
return ( return (
<Stack spacing={theme.spacing(LAYOUT.MD)}> <Stack spacing={theme.spacing(4)}>
<Autocomplete <Autocomplete
multiple multiple
options={notificationOptions} options={notificationOptions}
@@ -742,7 +682,7 @@ const CreateMonitorPage = () => {
} }
/> />
{(watchedType === "http" || watchedType === "grpc") && ( {watchedType === "http" && (
<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")}
@@ -754,7 +694,7 @@ const CreateMonitorPage = () => {
<Stack <Stack
direction="row" direction="row"
alignItems="center" alignItems="center"
spacing={theme.spacing(SPACING.LG)} spacing={theme.spacing(2)}
> >
<Switch <Switch
checked={field.value ?? false} checked={field.value ?? false}
@@ -770,56 +710,12 @@ 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(LAYOUT.MD)}> <Stack spacing={theme.spacing(8)}>
<Controller <Controller
name="useAdvancedMatching" name="useAdvancedMatching"
control={control} control={control}
@@ -827,7 +723,7 @@ const CreateMonitorPage = () => {
<Stack <Stack
direction="row" direction="row"
alignItems="center" alignItems="center"
spacing={theme.spacing(SPACING.LG)} spacing={theme.spacing(2)}
> >
<Switch <Switch
checked={field.value ?? false} checked={field.value ?? false}
@@ -842,7 +738,7 @@ const CreateMonitorPage = () => {
)} )}
/> />
{watchedUseAdvancedMatching && ( {watchedUseAdvancedMatching && (
<Stack spacing={theme.spacing(LAYOUT.MD)}> <Stack spacing={theme.spacing(8)}>
<Controller <Controller
name="matchMethod" name="matchMethod"
control={control} control={control}
@@ -929,12 +825,13 @@ const CreateMonitorPage = () => {
/> />
)} )}
{/* Geo-distributed Checks Configuration */}
{watchedType === "http" && ( {watchedType === "http" && (
<ConfigBox <ConfigBox
title={t("pages.createMonitor.form.geoChecks.title")} title={t("pages.createMonitor.form.geoChecks.title")}
subtitle={t("pages.createMonitor.form.geoChecks.description")} subtitle={t("pages.createMonitor.form.geoChecks.description")}
rightContent={ rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}> <Stack spacing={theme.spacing(8)}>
<Controller <Controller
name="geoCheckEnabled" name="geoCheckEnabled"
control={control} control={control}
@@ -942,7 +839,7 @@ const CreateMonitorPage = () => {
<Stack <Stack
direction="row" direction="row"
alignItems="center" alignItems="center"
spacing={theme.spacing(SPACING.LG)} spacing={theme.spacing(2)}
> >
<Switch <Switch
checked={field.value ?? false} checked={field.value ?? false}
@@ -955,70 +852,34 @@ const CreateMonitorPage = () => {
)} )}
/> />
{watchGeoCheckEnabled && ( {watchGeoCheckEnabled && (
<Stack spacing={theme.spacing(LAYOUT.MD)}> <Stack spacing={theme.spacing(8)}>
<Controller <Controller
name="geoCheckLocations" name="geoCheckLocations"
control={control} control={control}
render={({ field }) => { render={({ field }) => {
// Map continents to have 'name' property for Autocomplete const locationOptions = GeoCheckLocations.map((loc) => ({
const locationOptions = GeoContinents.map((continent) => ({ id: loc,
id: continent,
name: t( name: t(
`pages.createMonitor.form.geoChecks.option.locations.options.${continent}` `pages.createMonitor.form.geoChecks.option.locations.options.${loc}`
), ),
})); }));
const selectedLocations = locationOptions.filter((loc) => const selectedLocations = locationOptions.filter((loc) =>
(field.value ?? []).includes(loc.id) (field.value ?? []).includes(loc.id)
); );
return ( return (
<Stack spacing={theme.spacing(LAYOUT.MD)}> <Autocomplete
<Autocomplete multiple={true}
multiple options={locationOptions}
options={locationOptions} value={selectedLocations}
value={selectedLocations} getOptionLabel={(option) => option.name}
getOptionLabel={(option) => option.name} onChange={(_, newValue: typeof locationOptions) => {
onChange={(_: unknown, newValue: typeof locationOptions) => { field.onChange(newValue.map((loc) => loc.id));
field.onChange(newValue.map((loc) => loc.id)); }}
}} isOptionEqualToValue={(option, value) => option.id === value.id}
isOptionEqualToValue={(option, value) => fieldLabel={t(
option.id === value.id "pages.createMonitor.form.geoChecks.option.locations.label"
}
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> />
); );
}} }}
/> />
@@ -1027,32 +888,16 @@ const CreateMonitorPage = () => {
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<Select <Select
{...field}
value={field.value ?? 300000} value={field.value ?? 300000}
onChange={(e) => field.onChange(Number(e.target.value))}
fieldLabel={t( fieldLabel={t(
"pages.createMonitor.form.geoChecks.option.interval.label" "pages.createMonitor.form.geoChecks.option.interval.label"
)} )}
> >
<MenuItem value={300000}> <MenuItem value={300000}>5 min</MenuItem>
{t( <MenuItem value={600000}>10 min</MenuItem>
"pages.createMonitor.form.geoChecks.option.interval.value.fiveMinutes" <MenuItem value={900000}>15 min</MenuItem>
)} <MenuItem value={1800000}>30 min</MenuItem>
</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> </Select>
)} )}
/> />
@@ -3,7 +3,6 @@ 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";
@@ -30,14 +29,14 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
} }
return ( return (
<Stack <Stack
gap={theme.spacing(LAYOUT.MD)} gap={theme.spacing(4)}
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={LAYOUT.MD}> <BaseBox padding={8}>
<Stack gap={theme.spacing(LAYOUT.MD)}> <Stack gap={theme.spacing(4)}>
<Typography textTransform={"uppercase"}> <Typography textTransform={"uppercase"}>
{t("pages.incidents.dialog.details.overview")} {t("pages.incidents.dialog.details.overview")}
</Typography> </Typography>
@@ -45,7 +44,7 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
<Grid <Grid
container container
spacing={theme.spacing(LAYOUT.MD)} spacing={theme.spacing(4)}
alignItems="center" alignItems="center"
> >
<Grid size={2}>{t("pages.incidents.dialog.details.status")}</Grid> <Grid size={2}>{t("pages.incidents.dialog.details.status")}</Grid>
@@ -76,15 +75,15 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
</Grid> </Grid>
</Stack> </Stack>
</BaseBox> </BaseBox>
<BaseBox padding={LAYOUT.MD}> <BaseBox padding={8}>
<Stack gap={theme.spacing(LAYOUT.MD)}> <Stack gap={theme.spacing(4)}>
<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(LAYOUT.MD)} spacing={theme.spacing(2)}
> >
<Grid size={6}> <Grid size={6}>
<Typography>{t("pages.incidents.dialog.details.timeline")}</Typography> <Typography>{t("pages.incidents.dialog.details.timeline")}</Typography>
@@ -128,15 +127,15 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
</Stack> </Stack>
</BaseBox> </BaseBox>
{!incident.status && ( {!incident.status && (
<BaseBox padding={LAYOUT.MD}> <BaseBox padding={8}>
<Stack gap={theme.spacing(LAYOUT.XS)}> <Stack gap={theme.spacing(4)}>
<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(LAYOUT.MD)} spacing={theme.spacing(2)}
alignItems="center" alignItems="center"
> >
<Grid size={2}> <Grid size={2}>
+1 -1
View File
@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
const LogsPage = () => { const LogsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<number>(1); const [activeTab, setActiveTab] = useState<number>(2);
return ( return (
<BasePage> <BasePage>
<Tabs <Tabs
+10 -11
View File
@@ -1,9 +1,7 @@
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,
@@ -110,7 +108,7 @@ const CreateMaintenanceWindowPage = () => {
const isLoading = isPosting || isPatching; const isLoading = isPosting || isPatching;
const onError = (errors: any) => { const onError = (errors: any) => {
logger.error("Maintenance form submission failed", undefined, { errors }); console.error("Form submission errors:", errors);
}; };
return ( return (
@@ -122,7 +120,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(LAYOUT.MD)}> <Stack spacing={theme.spacing(8)}>
<Controller <Controller
name="name" name="name"
control={control} control={control}
@@ -172,7 +170,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(LAYOUT.MD)}> <Stack spacing={theme.spacing(8)}>
<Controller <Controller
name="startDate" name="startDate"
control={control} control={control}
@@ -199,7 +197,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(LAYOUT.MD)}> <Stack spacing={theme.spacing(8)}>
<Controller <Controller
name="startTime" name="startTime"
control={control} control={control}
@@ -220,7 +218,7 @@ const CreateMaintenanceWindowPage = () => {
<Stack <Stack
direction="row" direction="row"
alignItems="flex-start" alignItems="flex-start"
spacing={theme.spacing(LAYOUT.MD)} spacing={theme.spacing(4)}
> >
<Controller <Controller
name="duration" name="duration"
@@ -285,7 +283,7 @@ const CreateMaintenanceWindowPage = () => {
.filter((m): m is Monitor => m !== undefined); .filter((m): m is Monitor => m !== undefined);
return ( return (
<Stack spacing={theme.spacing(LAYOUT.MD)}> <Stack spacing={theme.spacing(4)}>
<Autocomplete <Autocomplete
multiple multiple
options={monitorsList} options={monitorsList}
@@ -319,8 +317,9 @@ const CreateMaintenanceWindowPage = () => {
key={monitor.id} key={monitor.id}
direction="row" direction="row"
alignItems="center" alignItems="center"
padding={theme.spacing(LAYOUT.XS)} spacing={theme.spacing(4)}
marginTop={theme.spacing(SPACING.LG)} padding={theme.spacing(4)}
marginTop={theme.spacing(2)}
borderRadius={1} borderRadius={1}
sx={{ sx={{
border: `1px solid ${theme.palette.divider}`, border: `1px solid ${theme.palette.divider}`,
@@ -351,7 +350,7 @@ const CreateMaintenanceWindowPage = () => {
<Stack <Stack
direction="row" direction="row"
justifyContent="flex-end" justifyContent="flex-end"
spacing={theme.spacing(SPACING.LG)} spacing={theme.spacing(2)}
> >
<Button <Button
loading={isLoading} loading={isLoading}
+9 -23
View File
@@ -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 { useMemo, useState } from "react"; import { 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,6 +20,14 @@ 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,
@@ -34,28 +42,6 @@ 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;
+80 -89
View File
@@ -1,15 +1,6 @@
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 { logger } from "@/Utils/logger"; import { Stack, useTheme, MenuItem, Link, type SelectChangeEvent } from "@mui/material";
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";
@@ -23,7 +14,7 @@ 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, SliderWithLabel } from "@/Components/inputs"; import { TextField, Button, FieldLabel } 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";
@@ -266,7 +257,8 @@ 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]: nestedValue === undefined ? "" : nestedValue, [nestedKey]:
nestedValue === undefined || nestedValue === 0 ? "" : nestedValue,
}), }),
{} {}
); );
@@ -290,7 +282,7 @@ export const SettingsPage = () => {
}; };
const onError = (errors: unknown) => { const onError = (errors: unknown) => {
logger.debug("Form validation errors", errors); console.log("Form validation errors:", errors);
}; };
const languages = Object.keys(i18n.options.resources || {}); const languages = Object.keys(i18n.options.resources || {});
@@ -300,7 +292,7 @@ export const SettingsPage = () => {
component="form" component="form"
onSubmit={form.handleSubmit(onSubmit, onError)} onSubmit={form.handleSubmit(onSubmit, onError)}
> >
<Stack gap={theme.spacing(LAYOUT.MD)}> <Stack gap={theme.spacing(8)}>
<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")}
@@ -323,7 +315,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(LAYOUT.MD)}> <Stack gap={theme.spacing(8)}>
<Select <Select
value={mode} value={mode}
onChange={handleModeChange} onChange={handleModeChange}
@@ -473,16 +465,24 @@ 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 }) => ( render={({ field, fieldState }) => (
<SliderWithLabel <TextField
{...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")}
min={1} type="number"
max={100} inputProps={{ min: 0 }}
sliderMaxWidth={{ xs: "100%", md: "50%" }} placeholder={t(
value={field.value || 1} "pages.settings.form.thresholds.option.cpu.placeholder"
onChange={(_, value) => field.onChange(value)} )}
valueLabelDisplay="auto" error={!!fieldState.error}
helperText={fieldState.error?.message}
/> />
)} )}
/> />
@@ -490,16 +490,24 @@ 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 }) => ( render={({ field, fieldState }) => (
<SliderWithLabel <TextField
{...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")}
min={1} type="number"
max={100} inputProps={{ min: 0 }}
sliderMaxWidth={{ xs: "100%", md: "50%" }} placeholder={t(
value={field.value || 1} "pages.settings.form.thresholds.option.memory.placeholder"
onChange={(_, value) => field.onChange(value)} )}
valueLabelDisplay="auto" error={!!fieldState.error}
helperText={fieldState.error?.message}
/> />
)} )}
/> />
@@ -507,16 +515,24 @@ 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 }) => ( render={({ field, fieldState }) => (
<SliderWithLabel <TextField
{...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")}
min={1} type="number"
max={100} inputProps={{ min: 0 }}
sliderMaxWidth={{ xs: "100%", md: "50%" }} placeholder={t(
value={field.value || 1} "pages.settings.form.thresholds.option.disk.placeholder"
onChange={(_, value) => field.onChange(value)} )}
valueLabelDisplay="auto" error={!!fieldState.error}
helperText={fieldState.error?.message}
/> />
)} )}
/> />
@@ -524,18 +540,26 @@ 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 }) => ( render={({ field, fieldState }) => (
<SliderWithLabel <TextField
{...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"
)} )}
min={1} type="number"
max={150} inputProps={{ min: 0 }}
sliderMaxWidth={{ xs: "100%", md: "50%" }} placeholder={t(
value={field.value || 1} "pages.settings.form.thresholds.option.temperature.placeholder"
onChange={(_, value) => field.onChange(value)} )}
valueLabelDisplay="auto" error={!!fieldState.error}
helperText={fieldState.error?.message}
/> />
)} )}
/> />
@@ -551,7 +575,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(LAYOUT.MD)}> <Stack gap={theme.spacing(8)}>
<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")}
@@ -599,7 +623,7 @@ export const SettingsPage = () => {
</Stack> </Stack>
} }
rightContent={ rightContent={
<Stack gap={theme.spacing(LAYOUT.MD)}> <Stack gap={theme.spacing(10)}>
{/* Email Host */} {/* Email Host */}
<Controller <Controller
name="systemEmailHost" name="systemEmailHost"
@@ -686,7 +710,7 @@ export const SettingsPage = () => {
<Stack <Stack
direction="row" direction="row"
alignItems="center" alignItems="center"
gap={theme.spacing(LAYOUT.XS)} gap={theme.spacing(4)}
> >
<Button <Button
variant="contained" variant="contained"
@@ -761,7 +785,7 @@ export const SettingsPage = () => {
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: theme.spacing(LAYOUT.XS), gap: theme.spacing(4),
}} }}
> >
{[ {[
@@ -880,7 +904,7 @@ export const SettingsPage = () => {
subtitle={t("pages.settings.form.importExportMonitors.description")} subtitle={t("pages.settings.form.importExportMonitors.description")}
rightContent={ rightContent={
<Stack <Stack
gap={theme.spacing(LAYOUT.MD)} gap={theme.spacing(4)}
direction={"row"} direction={"row"}
> >
<input <input
@@ -966,46 +990,13 @@ 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(LAYOUT.MD), padding: theme.spacing(8),
marginLeft: theme.spacing(-LAYOUT.MD), marginLeft: theme.spacing(-8),
marginRight: theme.spacing(-LAYOUT.MD), marginRight: theme.spacing(-8),
marginBottom: theme.spacing(-LAYOUT.MD), marginBottom: theme.spacing(-8),
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"
+10 -12
View File
@@ -1,7 +1,5 @@
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";
@@ -82,7 +80,7 @@ const CreateStatusPage = () => {
}, [defaults, reset]); }, [defaults, reset]);
const onError = (errors: any) => { const onError = (errors: any) => {
logger.debug("Status page validation errors", errors); console.log(errors);
}; };
const handleDeleteClick = () => { const handleDeleteClick = () => {
@@ -130,7 +128,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) {
logger.error("Failed to fetch logo blob", e instanceof Error ? e : undefined); console.error("Error fetching logo blob:", e);
} }
} }
} }
@@ -167,7 +165,7 @@ const CreateStatusPage = () => {
<Stack <Stack
direction="row" direction="row"
alignItems="center" alignItems="center"
spacing={theme.spacing(SPACING.MD)} spacing={theme.spacing(2)}
> >
<Controller <Controller
name="isPublished" name="isPublished"
@@ -189,7 +187,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(LAYOUT.MD)}> <Stack spacing={theme.spacing(6)}>
<Controller <Controller
name="companyName" name="companyName"
control={control} control={control}
@@ -244,7 +242,7 @@ const CreateStatusPage = () => {
}; };
return ( return (
<Stack spacing={theme.spacing(LAYOUT.MD)}> <Stack spacing={theme.spacing(4)}>
<Autocomplete <Autocomplete
multiple multiple
options={monitors} options={monitors}
@@ -292,9 +290,9 @@ const CreateStatusPage = () => {
{...provided.dragHandleProps} {...provided.dragHandleProps}
direction="row" direction="row"
alignItems="center" alignItems="center"
spacing={theme.spacing(LAYOUT.XS)} 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}`,
@@ -368,7 +366,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(LAYOUT.MD)}> <Stack spacing={theme.spacing(8)}>
<Stack alignItems={"center"}> <Stack alignItems={"center"}>
<Controller <Controller
name="logo" name="logo"
@@ -409,7 +407,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(LAYOUT.MD)}> <Stack spacing={theme.spacing(2)}>
<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("common.table.headers.dateTime"), content: t("pages.checks.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);
}, },
@@ -1,107 +0,0 @@
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>
);
};
+3 -94
View File
@@ -6,12 +6,9 @@ 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";
@@ -22,15 +19,9 @@ 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";
@@ -46,10 +37,7 @@ 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) {
@@ -68,7 +56,7 @@ const UptimeDetailsPage = () => {
} = useGet<MonitorDetailsResponse>( } = useGet<MonitorDetailsResponse>(
monitorDetailsUrl, monitorDetailsUrl,
{}, {},
{ refreshInterval: 10000, keepPreviousData: true, revalidateOnFocus: false } { refreshInterval: 10000, keepPreviousData: true }
); );
const monitorData = monitorDetailsData?.monitorData; const monitorData = monitorDetailsData?.monitorData;
@@ -83,11 +71,7 @@ const UptimeDetailsPage = () => {
return `/monitors/certificate/${monitorId}`; return `/monitors/certificate/${monitorId}`;
}, [monitorId, monitor?.type]); }, [monitorId, monitor?.type]);
const { data: certificateData } = useGet<CertificateResponse>( const { data: certificateData } = useGet<CertificateResponse>(certificateUrl);
certificateUrl,
{},
{ revalidateOnFocus: false }
);
const certificateExpiry = useMemo(() => { const certificateExpiry = useMemo(() => {
if (!certificateData?.certificateDate) { if (!certificateData?.certificateDate) {
@@ -118,58 +102,9 @@ const UptimeDetailsPage = () => {
const { data: checksData, isLoading: checksIsLoading } = useGet<ChecksResponse>( const { data: checksData, isLoading: checksIsLoading } = useGet<ChecksResponse>(
checksUrl, checksUrl,
{}, {},
{ keepPreviousData: true, revalidateOnFocus: false } { keepPreviousData: true }
); );
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;
@@ -192,7 +127,6 @@ 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)}
@@ -226,31 +160,6 @@ 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,7 +181,6 @@ 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>
@@ -212,7 +211,6 @@ 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>
+4 -6
View File
@@ -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: pass "up"/"down" directly to the API // Status: "up" -> true, "down" -> false
// State: "active" -> true, "paused" -> false // State: "active" -> true, "paused" -> false
const toFilterStatus = useMemo(() => { const toFilterStatus = useMemo(() => {
if (selectedStatus === "up") return "up"; if (selectedStatus === "up") return "true";
if (selectedStatus === "down") return "down"; if (selectedStatus === "down") return "false";
return undefined; return undefined;
}, [selectedStatus]); }, [selectedStatus]);
@@ -68,9 +68,7 @@ const UptimeMonitorsPage = () => {
// Default to all types when none selected // Default to all types when none selected
const effectiveTypes = const effectiveTypes =
selectedTypes.length > 0 selectedTypes.length > 0 ? selectedTypes : ["http", "ping", "docker", "port", "game"];
? 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(() => {
+9
View File
@@ -160,6 +160,14 @@ export interface PageSpeedGroupedCheck {
totalChecks: number; totalChecks: number;
} }
export interface GroupedGeoCheck {
bucketDate: string;
continent: string;
avgResponseTime: number;
totalChecks: number;
uptimePercentage: number;
}
export interface LatestCheck { export interface LatestCheck {
status: boolean; status: boolean;
responseTime: number; responseTime: number;
@@ -236,6 +244,7 @@ export interface UptimeChecksResult {
groupedDownChecks: GroupedCheck[]; groupedDownChecks: GroupedCheck[];
uptimePercentage: number; uptimePercentage: number;
avgResponseTime: number; avgResponseTime: number;
groupedGeoChecks?: GroupedGeoCheck[];
} }
export interface ChecksSummary { export interface ChecksSummary {
-80
View File
@@ -1,80 +0,0 @@
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;
}
+4 -7
View File
@@ -1,7 +1,5 @@
import type { GroupedCheck, CheckSnapshot } from "@/Types/Check"; import type { GroupedCheck, CheckSnapshot } from "@/Types/Check";
import type { PageSpeedGroupedCheck } 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",
@@ -11,7 +9,6 @@ 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];
@@ -28,6 +25,9 @@ export type MonitorStatus = (typeof MonitorStatuses)[number];
export type MonitorMatchMethod = "equal" | "include" | "regex" | ""; export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
export const GeoCheckLocations = ["EU", "NA", "AS", "SA", "AF", "OC"] as const;
export type GeoCheckLocation = (typeof GeoCheckLocations)[number];
export interface Monitor { export interface Monitor {
id: string; id: string;
userId: string; userId: string;
@@ -44,8 +44,6 @@ 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;
@@ -63,10 +61,9 @@ 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; geoCheckEnabled?: boolean;
geoCheckLocations?: GeoContinent[]; geoCheckLocations?: GeoCheckLocation[];
geoCheckInterval?: number; geoCheckInterval?: number;
recentChecks: CheckSnapshot[]; recentChecks: CheckSnapshot[];
createdAt: string; createdAt: string;
-6
View File
@@ -4,7 +4,6 @@ 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 {
@@ -18,8 +17,3 @@ declare module "*.svg?react" {
>; >;
export default ReactComponent; export default ReactComponent;
} }
declare module "*.css" {
const content: string;
export default content;
}
-1
View File
@@ -8,7 +8,6 @@ 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",
-17
View File
@@ -1,17 +0,0 @@
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;
-137
View File
@@ -1,137 +0,0 @@
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,
};
+2 -22
View File
@@ -1,5 +1,4 @@
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" });
@@ -22,11 +21,8 @@ const baseSchema = z.object({
.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(), geoCheckEnabled: z.boolean().optional(),
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(), geoCheckLocations: z.array(z.enum(["EU", "NA", "AS", "SA", "AF", "OC"])).optional(),
geoCheckInterval: z geoCheckInterval: z.number().min(5, "Interval must be at least 5 minutes").optional(),
.number()
.min(300000, "Interval must be at least 5 minutes")
.optional(),
}); });
// HTTP monitor schema // HTTP monitor schema
@@ -38,8 +34,6 @@ 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
@@ -75,18 +69,6 @@ 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"),
@@ -124,7 +106,6 @@ export const monitorSchema = z.discriminatedUnion("type", [
portSchema, portSchema,
dockerSchema, dockerSchema,
gameSchema, gameSchema,
grpcSchema,
pagespeedSchema, pagespeedSchema,
hardwareSchema, hardwareSchema,
]); ]);
@@ -138,7 +119,6 @@ export {
portSchema, portSchema,
dockerSchema, dockerSchema,
gameSchema, gameSchema,
grpcSchema,
pagespeedSchema, pagespeedSchema,
hardwareSchema, hardwareSchema,
}; };
+28 -6
View File
@@ -40,12 +40,34 @@ 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.object({ globalThresholds: z
cpu: z.number().int().min(1).max(100), .object({
memory: z.number().int().min(1).max(100), cpu: z
disk: z.number().int().min(1).max(100), .number()
temperature: z.number().int().min(1).max(150), .int()
}), .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>;
+30 -58
View File
@@ -45,8 +45,7 @@
"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", "importFromJSON": "Import from JSON"
"clearFilters": "Clear filters"
}, },
"charts": { "charts": {
"labels": { "labels": {
@@ -116,8 +115,7 @@
"type": "Type", "type": "Type",
"url": "Url", "url": "Url",
"interval": "Interval", "interval": "Interval",
"active": "Active", "active": "Active"
"responseTime": "Response time"
} }
} }
}, },
@@ -374,8 +372,8 @@
"table": { "table": {
"empty": "No down checks in this time range", "empty": "No down checks in this time range",
"headers": { "headers": {
"statusCode": "Status code", "dateTime": "Date & time",
"location": "Location" "statusCode": "Status code"
} }
} }
}, },
@@ -433,6 +431,31 @@
}, },
"title": "Advanced settings" "title": "Advanced settings"
}, },
"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 (minutes)",
"helper": "Minimum 5 minutes between geo checks"
}
}
},
"frequency": { "frequency": {
"description": "How often do you want to check the status of this monitor?", "description": "How often do you want to check the status of this monitor?",
"option": { "option": {
@@ -483,10 +506,6 @@
"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",
@@ -497,22 +516,9 @@
"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": {
@@ -544,8 +550,6 @@
"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",
@@ -572,36 +576,6 @@
} }
} }
}, },
"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.",
@@ -671,6 +645,7 @@
}, },
"filters": { "filters": {
"allMonitors": "All monitors", "allMonitors": "All monitors",
"clearFilters": "Clear filters",
"monitor": "Monitor", "monitor": "Monitor",
"resolutionType": "Resolution type", "resolutionType": "Resolution type",
"resolutionTypes": { "resolutionTypes": {
@@ -1146,9 +1121,6 @@
"about": { "about": {
"title": "About", "title": "About",
"developedBy": "Developed by Bluewave Labs" "developedBy": "Developed by Bluewave Labs"
},
"validation": {
"errorMessage": "Please fix the following validation errors:"
} }
} }
}, },
+1 -1
View File
@@ -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.5.0"; let version = "3.4.0";
return { return {
base: "/", base: "/",
-26
View File
@@ -1,26 +0,0 @@
# ==============================================
# 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
-14
View File
@@ -1,7 +1,6 @@
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.
@@ -11,9 +10,6 @@ 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: {
@@ -25,19 +21,9 @@ 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
}, },
}, },
+40
View File
@@ -432,6 +432,46 @@
} }
}, },
"/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",
+144 -482
View File
@@ -9,8 +9,6 @@
"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",
@@ -30,6 +28,7 @@
"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",
@@ -42,8 +41,7 @@
"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",
@@ -63,8 +61,6 @@
"@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",
@@ -78,8 +74,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"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
@@ -2046,9 +2041,9 @@
} }
}, },
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1", "version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2203,9 +2198,9 @@
} }
}, },
"node_modules/@grpc/grpc-js": { "node_modules/@grpc/grpc-js": {
"version": "1.14.3", "version": "1.14.0",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.0.tgz",
"integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "integrity": "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@grpc/proto-loader": "^0.8.0", "@grpc/proto-loader": "^0.8.0",
@@ -2215,7 +2210,7 @@
"node": ">=12.10.0" "node": ">=12.10.0"
} }
}, },
"node_modules/@grpc/proto-loader": { "node_modules/@grpc/grpc-js/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==",
@@ -2233,6 +2228,39 @@
"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",
@@ -3447,6 +3475,27 @@
"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",
@@ -4174,6 +4223,16 @@
"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",
@@ -4690,360 +4749,6 @@
"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",
@@ -7243,24 +6948,6 @@
"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",
@@ -10251,6 +9938,19 @@
"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",
@@ -14473,6 +14173,63 @@
"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",
@@ -14608,55 +14365,6 @@
"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",
@@ -14746,19 +14454,6 @@
"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",
@@ -14990,30 +14685,6 @@
"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",
@@ -15810,15 +15481,6 @@
"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"
}
} }
} }
} }
+3 -8
View File
@@ -24,8 +24,6 @@
"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",
@@ -45,6 +43,7 @@
"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",
@@ -57,8 +56,7 @@
"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",
@@ -78,8 +76,6 @@
"@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",
@@ -93,7 +89,6 @@
"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 -1
View File
@@ -3,6 +3,8 @@ 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;
@@ -10,7 +12,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 { } catch (error) {
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);
} }
-3
View File
@@ -2,7 +2,6 @@ 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";
@@ -18,7 +17,6 @@ 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;
@@ -34,7 +32,6 @@ 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),
-3
View File
@@ -6,7 +6,6 @@ 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";
@@ -23,7 +22,6 @@ 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);
@@ -38,7 +36,6 @@ 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());
+25 -36
View File
@@ -17,8 +17,6 @@ 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";
@@ -29,6 +27,7 @@ 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";
@@ -42,14 +41,11 @@ 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,
@@ -61,7 +57,6 @@ import {
MongoMaintenanceWindowsRepository, MongoMaintenanceWindowsRepository,
IMonitorsRepository, IMonitorsRepository,
IChecksRepository, IChecksRepository,
IGeoChecksRepository,
IMonitorStatsRepository, IMonitorStatsRepository,
IStatusPagesRepository, IStatusPagesRepository,
IUsersRepository, IUsersRepository,
@@ -86,13 +81,12 @@ 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: ILogger; logger: any;
notificationsService: INotificationsService; notificationsService: INotificationsService;
statusPageService: IStatusPageService; statusPageService: IStatusPageService;
notificationMessageBuilder: INotificationMessageBuilder; notificationMessageBuilder: INotificationMessageBuilder;
@@ -100,7 +94,6 @@ 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;
@@ -133,7 +126,6 @@ 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();
@@ -144,40 +136,38 @@ 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 as unknown as { GameDig,
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(logger, incidentsRepository, monitorsRepository, usersRepository, notificationMessageBuilder); const incidentService = new IncidentService({
const checkService = new CheckService(monitorsRepository, logger, checksRepository);
const globalPingService = new GlobalPingService(logger);
const geoChecksService = new GeoChecksService({
logger, logger,
geoChecksRepository, incidentsRepository,
globalPingService,
monitorsRepository, monitorsRepository,
usersRepository,
notificationMessageBuilder,
}); });
const bufferService = new BufferService(logger, checkService, geoChecksService, settingsService); const checkService = new CheckService({
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);
@@ -202,13 +192,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,
bufferService, buffer: bufferService,
incidentService, incidentService,
maintenanceWindowsRepository, maintenanceWindowsRepository,
monitorsRepository, monitorsRepository,
@@ -216,11 +206,13 @@ export const initializeServices = async ({
monitorStatsRepository, monitorStatsRepository,
checksRepository, checksRepository,
incidentsRepository, incidentsRepository,
geoChecksService, });
geoChecksRepository
);
const superSimpleQueue = await SuperSimpleQueue.create(logger, superSimpleQueueHelper, monitorsRepository); const superSimpleQueue = await SuperSimpleQueue.create({
logger,
helper: superSimpleQueueHelper,
monitorsRepository,
});
// Business services // Business services
const userService = new UserService({ const userService = new UserService({
@@ -255,7 +247,6 @@ export const initializeServices = async ({
games, games,
monitorsRepository, monitorsRepository,
checksRepository, checksRepository,
geoChecksRepository,
monitorStatsRepository, monitorStatsRepository,
statusPagesRepository, statusPagesRepository,
incidentsRepository, incidentsRepository,
@@ -274,7 +265,6 @@ export const initializeServices = async ({
jobQueue: superSimpleQueue, jobQueue: superSimpleQueue,
userService, userService,
checkService, checkService,
geoChecksService,
diagnosticService, diagnosticService,
inviteService, inviteService,
maintenanceWindowService, maintenanceWindowService,
@@ -288,7 +278,6 @@ export const initializeServices = async ({
// Repositories // Repositories
monitorsRepository, monitorsRepository,
checksRepository, checksRepository,
geoChecksRepository,
monitorStatsRepository, monitorStatsRepository,
statusPagesRepository, statusPagesRepository,
usersRepository, usersRepository,
+16 -19
View File
@@ -6,20 +6,17 @@ 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/userValidation.js"; } from "@/validation/joi.js";
const SERVICE_NAME = "authController"; const SERVICE_NAME = "authController";
@@ -43,7 +40,7 @@ class AuthController {
if (newUser?.email) { if (newUser?.email) {
newUser.email = newUser.email.toLowerCase(); newUser.email = newUser.email.toLowerCase();
} }
registrationBodyValidation.parse(newUser); await registrationBodyValidation.validateAsync(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({
@@ -62,7 +59,7 @@ class AuthController {
if (userData?.email) { if (userData?.email) {
userData.email = userData.email.toLowerCase(); userData.email = userData.email.toLowerCase();
} }
createUserBodyValidation.parse(userData); await createUserBodyValidation.validateAsync(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[];
@@ -82,7 +79,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();
} }
loginValidation.parse(req.body); await loginValidation.validateAsync(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({
@@ -100,7 +97,7 @@ class AuthController {
editUser = async (req: Request, res: Response, next: NextFunction) => { editUser = async (req: Request, res: Response, next: NextFunction) => {
try { try {
editUserBodyValidation.parse(req.body); await editUserBodyValidation.validateAsync(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({
@@ -128,7 +125,7 @@ class AuthController {
requestRecovery = async (req: Request, res: Response, next: NextFunction) => { requestRecovery = async (req: Request, res: Response, next: NextFunction) => {
try { try {
recoveryValidation.parse(req.body); await recoveryValidation.validateAsync(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({
@@ -143,7 +140,7 @@ class AuthController {
validateRecovery = async (req: Request, res: Response, next: NextFunction) => { validateRecovery = async (req: Request, res: Response, next: NextFunction) => {
try { try {
recoveryTokenBodyValidation.parse(req.body); await recoveryTokenBodyValidation.validateAsync(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,
@@ -156,7 +153,7 @@ class AuthController {
resetPassword = async (req: Request, res: Response, next: NextFunction) => { resetPassword = async (req: Request, res: Response, next: NextFunction) => {
try { try {
newPasswordValidation.parse(req.body); await newPasswordValidation.validateAsync(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,
@@ -195,7 +192,7 @@ class AuthController {
getUserById = async (req: Request, res: Response, next: NextFunction) => { getUserById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
getUserByIdParamValidation.parse(req.params); await getUserByIdParamValidation.validateAsync(req.params);
const userId = req?.params?.userId; const userId = req?.params?.userId;
const roles = req?.user?.role; const roles = req?.user?.role;
@@ -226,12 +223,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 };
editUserByIdParamValidation.parse(req.params); await editUserByIdParamValidation.validateAsync(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) {
editSuperadminUserByIdBodyValidation.parse(req.body); await editSuperadminUserByIdBodyValidation.validateAsync(req.body);
} else { } else {
editUserByIdBodyValidation.parse(req.body); await editUserByIdBodyValidation.validateAsync(req.body);
} }
await this.userService.editUserById(userId, user); await this.userService.editUserById(userId, user);
@@ -249,8 +246,8 @@ class AuthController {
} }
const userId = req.params.userId as string; const userId = req.params.userId as string;
editUserByIdParamValidation.parse(req.params); await editUserByIdParamValidation.validateAsync(req.params);
editUserPasswordByIdBodyValidation.parse(req.body); await editUserPasswordByIdBodyValidation.validateAsync(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" });
+10 -7
View File
@@ -7,7 +7,10 @@ import {
deleteChecksParamValidation, deleteChecksParamValidation,
deleteChecksByTeamIdParamValidation, deleteChecksByTeamIdParamValidation,
updateChecksTTLBodyValidation, updateChecksTTLBodyValidation,
} from "@/validation/checkValidation.js"; ackCheckBodyValidation,
ackAllChecksParamValidation,
ackAllChecksBodyValidation,
} from "@/validation/joi.js";
const SERVICE_NAME = "checkController"; const SERVICE_NAME = "checkController";
@@ -25,8 +28,8 @@ class CheckController {
getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
getChecksParamValidation.parse(req.params); await getChecksParamValidation.validateAsync(req.params);
getChecksQueryValidation.parse(req.query); await getChecksQueryValidation.validateAsync(req.query);
const result = await this.checkService.getChecksByMonitor({ const result = await this.checkService.getChecksByMonitor({
monitorId: req?.params?.monitorId, monitorId: req?.params?.monitorId,
@@ -46,7 +49,7 @@ class CheckController {
getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => { getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => {
try { try {
getTeamChecksQueryValidation.parse(req.query); await getTeamChecksQueryValidation.validateAsync(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,
@@ -77,7 +80,7 @@ class CheckController {
deleteChecks = async (req: Request, res: Response, next: NextFunction) => { deleteChecks = async (req: Request, res: Response, next: NextFunction) => {
try { try {
deleteChecksParamValidation.parse(req.params); await deleteChecksParamValidation.validateAsync(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,
@@ -96,7 +99,7 @@ class CheckController {
deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
deleteChecksByTeamIdParamValidation.parse(req.params); await deleteChecksByTeamIdParamValidation.validateAsync(req.params);
const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId }); const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId });
@@ -112,7 +115,7 @@ class CheckController {
updateChecksTTL = async (req: Request, res: Response, next: NextFunction) => { updateChecksTTL = async (req: Request, res: Response, next: NextFunction) => {
try { try {
updateChecksTTLBodyValidation.parse(req.body); await updateChecksTTLBodyValidation.validateAsync(req.body);
await this.checkService.updateChecksTTL({ await this.checkService.updateChecksTTL({
teamId: req?.user?.teamId, teamId: req?.user?.teamId,
@@ -1,41 +0,0 @@
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;
+4 -4
View File
@@ -1,5 +1,5 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js"; import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/joi.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;
inviteBodyValidation.parse(invite); await inviteBodyValidation.validateAsync(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;
inviteBodyValidation.parse(inviteRequest); await inviteBodyValidation.validateAsync(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 {
inviteVerificationBodyValidation.parse(req.body); await inviteVerificationBodyValidation.validateAsync(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/maintenanceWindowValidation.js"; } from "@/validation/joi.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 {
createMaintenanceWindowBodyValidation.parse(req.body); await createMaintenanceWindowBodyValidation.validateAsync(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 {
getMaintenanceWindowByIdParamValidation.parse(req.params); await getMaintenanceWindowByIdParamValidation.validateAsync(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 {
getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query); await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(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 {
getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params); await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(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 {
deleteMaintenanceWindowByIdParamValidation.parse(req.params); await deleteMaintenanceWindowByIdParamValidation.validateAsync(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 {
editMaintenanceWindowByIdParamValidation.parse(req.params); await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
editMaintenanceByIdWindowBodyValidation.parse(req.body); await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
+17 -50
View File
@@ -12,7 +12,7 @@ import {
getCertificateParamValidation, getCertificateParamValidation,
getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdParamValidation,
getHardwareDetailsByIdQueryValidation, getHardwareDetailsByIdQueryValidation,
} from "@/validation/monitorValidation.js"; } from "@/validation/joi.js";
import sslChecker from "ssl-checker"; import sslChecker from "ssl-checker";
import { import {
fetchMonitorCertificate, fetchMonitorCertificate,
@@ -27,7 +27,6 @@ import {
} 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 {
@@ -45,7 +44,7 @@ class MonitorController {
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
try { try {
getCertificateParamValidation.parse(req.params); await getCertificateParamValidation.validateAsync(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 });
@@ -88,8 +87,8 @@ class MonitorController {
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => { getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
getHardwareDetailsByIdParamValidation.parse(req.params); await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
getHardwareDetailsByIdQueryValidation.parse(req.query); await getHardwareDetailsByIdQueryValidation.validateAsync(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";
@@ -112,8 +111,8 @@ class MonitorController {
}; };
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => { getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
getHardwareDetailsByIdParamValidation.parse(req.params); await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
getHardwareDetailsByIdQueryValidation.parse(req.query); await getHardwareDetailsByIdQueryValidation.validateAsync(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");
@@ -135,42 +134,10 @@ 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 {
getMonitorByIdParamValidation.parse(req.params); await getMonitorByIdParamValidation.validateAsync(req.params);
getMonitorByIdQueryValidation.parse(req.query); await getMonitorByIdQueryValidation.validateAsync(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");
@@ -189,7 +156,7 @@ class MonitorController {
createMonitor = async (req: Request, res: Response, next: NextFunction) => { createMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
createMonitorBodyValidation.parse(req.body); await createMonitorBodyValidation.validateAsync(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);
@@ -231,7 +198,7 @@ class MonitorController {
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => { deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
getMonitorByIdParamValidation.parse(req.params); await getMonitorByIdParamValidation.validateAsync(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);
@@ -264,8 +231,8 @@ class MonitorController {
editMonitor = async (req: Request, res: Response, next: NextFunction) => { editMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
getMonitorByIdParamValidation.parse(req.params); await getMonitorByIdParamValidation.validateAsync(req.params);
editMonitorBodyValidation.parse(req.body); await editMonitorBodyValidation.validateAsync(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);
@@ -283,7 +250,7 @@ class MonitorController {
pauseMonitor = async (req: Request, res: Response, next: NextFunction) => { pauseMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
pauseMonitorParamValidation.parse(req.params); await pauseMonitorParamValidation.validateAsync(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);
@@ -336,8 +303,8 @@ class MonitorController {
getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => { getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
getMonitorsByTeamIdParamValidation.parse(req.params); await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
getMonitorsByTeamIdQueryValidation.parse(req.query); await getMonitorsByTeamIdQueryValidation.validateAsync(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);
@@ -357,8 +324,8 @@ class MonitorController {
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
getMonitorsByTeamIdParamValidation.parse(req.params); await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
getMonitorsWithChecksQueryValidation.parse(req.query); await getMonitorsWithChecksQueryValidation.validateAsync(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/notificationValidation.js"; import { createNotificationBodyValidation } from "@/validation/joi.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,7 +42,9 @@ class NotificationController {
createNotification = async (req: Request, res: Response, next: NextFunction) => { createNotification = async (req: Request, res: Response, next: NextFunction) => {
try { try {
createNotificationBodyValidation.parse(req.body); await createNotificationBodyValidation.validateAsync(req.body, {
abortEarly: false,
});
const body = req.body; const body = req.body;
@@ -132,7 +134,9 @@ class NotificationController {
editNotification = async (req: Request, res: Response, next: NextFunction) => { editNotification = async (req: Request, res: Response, next: NextFunction) => {
try { try {
createNotificationBodyValidation.parse(req.body); await createNotificationBodyValidation.validateAsync(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;
+10 -15
View File
@@ -1,6 +1,5 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js"; import { sendTestEmailBodyValidation, updateAppSettingsBodyValidation } from "@/validation/joi.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";
@@ -56,24 +55,20 @@ class SettingsController {
}; };
updateAppSettings = async (req: Request, res: Response, next: NextFunction) => { updateAppSettings = async (req: Request, res: Response, next: NextFunction) => {
try { await updateAppSettingsBodyValidation.validateAsync(req.body);
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 {
sendTestEmailBodyValidation.parse(req.body); await sendTestEmailBodyValidation.validateAsync(req.body);
const { const {
to, to,
+8 -17
View File
@@ -1,16 +1,12 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "@/validation/joi.js";
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";
@@ -32,10 +28,8 @@ class StatusPageController {
createStatusPage = async (req: Request, res: Response, next: NextFunction) => { createStatusPage = async (req: Request, res: Response, next: NextFunction) => {
try { try {
createStatusPageBodyValidation.parse(req.body); await createStatusPageBodyValidation.validateAsync(req.body);
if (req.file) { await imageValidation.validateAsync(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);
@@ -53,10 +47,8 @@ class StatusPageController {
updateStatusPage = async (req: Request, res: Response, next: NextFunction) => { updateStatusPage = async (req: Request, res: Response, next: NextFunction) => {
try { try {
createStatusPageBodyValidation.parse(req.body); await createStatusPageBodyValidation.validateAsync(req.body);
if (req.file) { await imageValidation.validateAsync(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) {
@@ -78,8 +70,8 @@ class StatusPageController {
getStatusPageByUrl = async (req: Request, res: Response, next: NextFunction) => { getStatusPageByUrl = async (req: Request, res: Response, next: NextFunction) => {
try { try {
getStatusPageParamValidation.parse(req.params); await getStatusPageParamValidation.validateAsync(req.params);
getStatusPageQueryValidation.parse(req.query); await getStatusPageQueryValidation.validateAsync(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 });
@@ -109,7 +101,6 @@ 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 };
} }
+34
View File
@@ -11,6 +11,8 @@ import type {
CheckMemoryInfo, CheckMemoryInfo,
CheckMetadata, CheckMetadata,
CheckNetworkInterfaceInfo, CheckNetworkInterfaceInfo,
GeoCheckResult,
GeoCheckTimings,
GotTimings, GotTimings,
ILighthouseAudit, ILighthouseAudit,
} from "@/types/check.js"; } from "@/types/check.js";
@@ -175,6 +177,34 @@ const auditsSchema = new Schema<CheckAudits>(
{ _id: false } { _id: false }
); );
const geoCheckTimingsSchema = new Schema<GeoCheckTimings>(
{
total: { type: Number, required: true },
dns: { type: Number },
tcp: { type: Number },
tls: { type: Number },
firstByte: { type: Number },
download: { type: Number },
},
{ _id: false }
);
const geoCheckResultSchema = new Schema<GeoCheckResult>(
{
continent: { type: String, required: true },
region: { type: String },
country: { type: String, required: true },
state: { type: String },
city: { type: String, required: true },
longitude: { type: Number, required: true },
latitude: { type: Number, required: true },
status: { type: Boolean, required: true },
statusCode: { type: Number, required: true },
timings: { type: geoCheckTimingsSchema, required: true },
},
{ _id: false }
);
const metadataSchema = new Schema<CheckMetadataDocument>( const metadataSchema = new Schema<CheckMetadataDocument>(
{ {
monitorId: { monitorId: {
@@ -281,6 +311,10 @@ const CheckSchema = new Schema<CheckDocument>(
type: auditsSchema, type: auditsSchema,
default: undefined, default: undefined,
}, },
geoResults: {
type: [geoCheckResultSchema],
default: undefined,
},
}, },
{ {
timestamps: true, timestamps: true,
-116
View File
@@ -1,116 +0,0 @@
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;
+3 -12
View File
@@ -1,6 +1,6 @@
import { Schema, model, Types, type UpdateQuery } from "mongoose"; import { Schema, model, Types } from "mongoose";
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js"; import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js"; import { MonitorTypes, MonitorStatuses, GeoCheckLocations } from "@/types/monitor.js";
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date }; type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
@@ -93,12 +93,6 @@ 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,
@@ -166,10 +160,6 @@ 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,
@@ -185,6 +175,7 @@ const MonitorSchema = new Schema<MonitorDocument>(
}, },
geoCheckLocations: { geoCheckLocations: {
type: [String], type: [String],
enum: GeoCheckLocations,
default: [], default: [],
}, },
geoCheckInterval: { geoCheckInterval: {
-3
View File
@@ -33,6 +33,3 @@ 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";
+2 -6
View File
@@ -2,7 +2,6 @@ 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";
@@ -16,9 +15,6 @@ 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);
@@ -27,7 +23,7 @@ const startApp = async () => {
// Create services // Create services
const settingsRepository = new MongoSettingsRepository(); const settingsRepository = new MongoSettingsRepository();
const settingsService = new SettingsService(settingsRepository, env); const settingsService = new SettingsService(settingsRepository);
const envSettings = settingsService.loadSettings(); const envSettings = settingsService.loadSettings();
@@ -50,7 +46,7 @@ const startApp = async () => {
openApiSpec, openApiSpec,
}); });
const port = 52345; const port = envSettings.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}` });
}); });
@@ -10,15 +10,25 @@ import type {
CheckMemoryInfo, CheckMemoryInfo,
CheckMetadata, CheckMetadata,
CheckNetworkInterfaceInfo, CheckNetworkInterfaceInfo,
GeoCheckResult,
GeoCheckTimings,
GotTimings, GotTimings,
MonitorType, MonitorType,
} 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 };
@@ -157,6 +167,33 @@ class MongoChecksRepository implements IChecksRepository {
}; };
}; };
const mapGeoTimings = (timings?: GeoCheckTimings): GeoCheckTimings => ({
total: timings?.total ?? 0,
dns: timings?.dns,
tcp: timings?.tcp,
tls: timings?.tls,
firstByte: timings?.firstByte,
download: timings?.download,
});
const mapGeoResults = (geoResults?: GeoCheckResult[]): GeoCheckResult[] | undefined => {
if (!geoResults?.length) {
return undefined;
}
return geoResults.map((result) => ({
continent: result.continent ?? "",
region: result.region,
country: result.country ?? "",
state: result.state,
city: result.city ?? "",
longitude: result.longitude ?? 0,
latitude: result.latitude ?? 0,
status: result.status ?? false,
statusCode: result.statusCode ?? 0,
timings: mapGeoTimings(result.timings),
}));
};
const mapMetadata = (metadata: CheckDocument["metadata"]): CheckMetadata => ({ const mapMetadata = (metadata: CheckDocument["metadata"]): CheckMetadata => ({
monitorId: toStringId(metadata.monitorId), monitorId: toStringId(metadata.monitorId),
teamId: toStringId(metadata.teamId), teamId: toStringId(metadata.teamId),
@@ -186,6 +223,7 @@ class MongoChecksRepository implements IChecksRepository {
seo: doc.seo, seo: doc.seo,
performance: doc.performance, performance: doc.performance,
audits: mapAudits(doc.audits), audits: mapAudits(doc.audits),
geoResults: mapGeoResults(doc.geoResults),
__v: doc.__v ?? 0, __v: doc.__v ?? 0,
createdAt: toDateString(doc.createdAt), createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt), updatedAt: toDateString(doc.updatedAt),
@@ -240,9 +278,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 }),
...(getDateForRange(dateRange) && { ...(dateRangeLookup[dateRange] && {
createdAt: { createdAt: {
$gte: getDateForRange(dateRange), $gte: dateRangeLookup[dateRange],
}, },
}), }),
}; };
@@ -291,9 +329,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),
...(getDateForRange(dateRange) && { ...(dateRangeLookup[dateRange] && {
createdAt: { createdAt: {
$gte: getDateForRange(dateRange), $gte: dateRangeLookup[dateRange],
}, },
}), }),
}; };
@@ -378,9 +416,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),
...(getDateForRange(dateRange) && { ...(dateRangeLookup[dateRange] && {
createdAt: { createdAt: {
$gte: getDateForRange(dateRange), $gte: dateRangeLookup[dateRange],
}, },
}), }),
}; };
@@ -419,85 +457,131 @@ class MongoChecksRepository implements IChecksRepository {
endDate: Date, endDate: Date,
dateString: string dateString: string
) => { ) => {
const matchStage = { // Match stage for primary checks (exclude geo checks)
const primaryCheckMatchStage = {
"metadata.monitorId": monitorObjectId, "metadata.monitorId": monitorObjectId,
createdAt: { $gte: startDate, $lte: endDate }, createdAt: { $gte: startDate, $lte: endDate },
geoResults: { $exists: false },
}; };
const [result] = await CheckModel.aggregate([
{ $match: matchStage }, // Match stage for geo checks only
{ $sort: { createdAt: 1 } }, const geoCheckMatchStage = {
{ "metadata.monitorId": monitorObjectId,
$facet: { createdAt: { $gte: startDate, $lte: endDate },
uptimePercentage: [ geoResults: { $exists: true, $ne: null },
{ };
$group: {
_id: null, // Run two separate aggregations - one for primary checks, one for geo checks
upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } }, // TODO, only do this if the monitor has geochecks enabled, otherwise we only need one aggregation
totalChecks: { $sum: 1 }, const [primaryResult, geoResults] = await Promise.all([
}, CheckModel.aggregate([
}, { $match: primaryCheckMatchStage },
{ { $sort: { createdAt: 1 } },
$project: { {
_id: 0, $facet: {
percentage: { uptimePercentage: [
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }], {
$group: {
_id: null,
upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } },
totalChecks: { $sum: 1 },
}, },
}, },
}, {
], $project: {
groupedAvgResponseTime: [ _id: 0,
{ percentage: {
$group: { $cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
_id: null, },
avgResponseTime: { $avg: "$responseTime" },
},
},
],
groupedChecks: [
{
$group: {
_id: {
$dateToString: { format: dateString, date: "$createdAt" },
}, },
avgResponseTime: { $avg: "$responseTime" },
totalChecks: { $sum: 1 },
}, },
}, ],
{ $sort: { _id: 1 } }, groupedAvgResponseTime: [
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } }, {
], $group: {
groupedUpChecks: [ _id: null,
{ $match: { status: true } }, avgResponseTime: { $avg: "$responseTime" },
{
$group: {
_id: {
$dateToString: { format: dateString, date: "$createdAt" },
}, },
totalChecks: { $sum: 1 },
avgResponseTime: { $avg: "$responseTime" },
}, },
}, ],
{ $sort: { _id: 1 } }, groupedChecks: [
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } }, {
], $group: {
groupedDownChecks: [ _id: {
{ $match: { status: false } }, $dateToString: { format: dateString, date: "$createdAt" },
{ },
$group: { avgResponseTime: { $avg: "$responseTime" },
_id: { totalChecks: { $sum: 1 },
$dateToString: { format: dateString, date: "$createdAt" },
}, },
totalChecks: { $sum: 1 },
avgResponseTime: { $avg: "$responseTime" },
}, },
}, { $sort: { _id: 1 } },
{ $sort: { _id: 1 } }, { $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } }, ],
], groupedUpChecks: [
{ $match: { status: true } },
{
$group: {
_id: {
$dateToString: { format: dateString, date: "$createdAt" },
},
totalChecks: { $sum: 1 },
avgResponseTime: { $avg: "$responseTime" },
},
},
{ $sort: { _id: 1 } },
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
],
groupedDownChecks: [
{ $match: { status: false } },
{
$group: {
_id: {
$dateToString: { format: dateString, date: "$createdAt" },
},
totalChecks: { $sum: 1 },
avgResponseTime: { $avg: "$responseTime" },
},
},
{ $sort: { _id: 1 } },
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
],
},
}, },
}, ]),
CheckModel.aggregate([
{ $match: geoCheckMatchStage },
{ $sort: { createdAt: 1 } },
{ $unwind: "$geoResults" },
{
$group: {
_id: {
date: { $dateToString: { format: dateString, date: "$createdAt" } },
continent: "$geoResults.continent",
},
avgResponseTime: { $avg: "$geoResults.timings.total" },
totalChecks: { $sum: 1 },
upChecks: { $sum: { $cond: [{ $eq: ["$geoResults.status", true] }, 1, 0] } },
},
},
{
$project: {
_id: 0,
bucketDate: "$_id.date",
continent: "$_id.continent",
avgResponseTime: 1,
totalChecks: 1,
uptimePercentage: {
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
},
},
},
{ $sort: { bucketDate: 1, continent: 1 } },
]),
]); ]);
const [result] = primaryResult;
const groupedGeoChecks = geoResults ?? [];
const uptimePercentage = result?.uptimePercentage?.[0]?.percentage ?? 0; const uptimePercentage = result?.uptimePercentage?.[0]?.percentage ?? 0;
const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0; const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0;
@@ -508,6 +592,7 @@ class MongoChecksRepository implements IChecksRepository {
groupedDownChecks: result?.groupedDownChecks ?? [], groupedDownChecks: result?.groupedDownChecks ?? [],
uptimePercentage, uptimePercentage,
avgResponseTime, avgResponseTime,
groupedGeoChecks,
}; };
}; };
@@ -1,35 +0,0 @@
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>;
}
@@ -1,386 +0,0 @@
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;
-3
View File
@@ -33,6 +33,3 @@ 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");
const inviteToken = await InviteModel.create(invite); let inviteToken = await InviteModel.create(invite);
return this.toEntity(inviteToken); return this.toEntity(inviteToken);
}; };
@@ -92,7 +92,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
} }
// Sorting // Sorting
const sort: Record<string, any> = {}; let 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;
} }
@@ -352,12 +352,11 @@ 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)),
geoCheckEnabled: doc.geoCheckEnabled ?? false, geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [], geoCheckLocations: doc.geoCheckLocations ?? [],
geoCheckInterval: doc.geoCheckInterval ?? 300000, geoCheckInterval: doc.geoCheckInterval ?? 5,
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
createdAt: toDateString(doc.createdAt), createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt), updatedAt: toDateString(doc.updatedAt),
}; };
@@ -411,12 +410,11 @@ 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)),
geoCheckEnabled: doc.geoCheckEnabled ?? false, geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [], geoCheckLocations: doc.geoCheckLocations ?? [],
geoCheckInterval: doc.geoCheckInterval ?? 300000, geoCheckInterval: doc.geoCheckInterval ?? 5,
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
createdAt: toDateString(doc.createdAt), createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt), updatedAt: toDateString(doc.updatedAt),
}; };
@@ -3,6 +3,7 @@ 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 () => {
const settings = await AppSettingsModel.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); let settings = await AppSettingsModel.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean();
if (!settings) { if (!settings) {
return null; return null;
} }
-22
View File
@@ -1,22 +0,0 @@
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;
-3
View File
@@ -30,9 +30,6 @@ class MonitorRoutes {
// 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);
+26 -34
View File
@@ -1,31 +1,28 @@
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 { MonitorStatusResponse, CheckErrorInfo, Check, ILighthouseAudit, ChecksQueryResult, ChecksSummary } from "@/types/index.js"; import type { MonitorType, MonitorStatusResponse, CheckErrorInfo, Check } 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";
export interface ICheckService { class CheckService {
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: ILogger; private logger: any;
constructor(monitorsRepository: IMonitorsRepository, logger: ILogger, checksRepository: IChecksRepository) { constructor({
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;
@@ -64,12 +61,13 @@ class CheckService implements ICheckService {
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: ILighthouseAudit | undefined) => { const mapAudit = (audit: any) => {
if (!audit || typeof audit !== "object") { if (!audit || typeof audit !== "object") {
return undefined; return undefined;
} }
@@ -112,7 +110,7 @@ class CheckService implements ICheckService {
return check; return check;
}; };
getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: Record<string, string>; teamId: string }) => { getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; 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 });
} }
@@ -120,34 +118,28 @@ class CheckService implements ICheckService {
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 verification, throws an error if monitor doesn't belong to team // For verificaiton, throws an error if monitor doesn't belong to team
await this.monitorsRepository.findById(monitorId, teamId); await this.monitorsRepository.findById(monitorId, teamId);
const { sortOrder, dateRange, filter, page, rowsPerPage, status } = query; let { 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) : 0; const parsedPage = page ? parseInt(page) : page;
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5; const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
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: Record<string, string> }) => { getChecksByTeam = async ({ teamId, query }: { teamId: string; query: any }) => {
const { sortOrder, dateRange, filter, page, rowsPerPage } = query; let { 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) : 0; const parsedPage = page ? parseInt(page) : page;
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5; const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
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;
@@ -170,7 +162,7 @@ class CheckService implements ICheckService {
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 verification, throws an error if monitor doesn't belong to team // For verificaiton, 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);
@@ -186,7 +178,7 @@ class CheckService implements ICheckService {
}; };
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, details: { teamId, ttl } }); throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500 });
}; };
} }
@@ -1,192 +0,0 @@
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;
+29 -16
View File
@@ -3,29 +3,42 @@ 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: ILogger; private logger: any;
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: ILogger, logger,
incidentsRepository: IIncidentsRepository, incidentsRepository,
monitorsRepository: IMonitorsRepository, monitorsRepository,
usersRepository: IUsersRepository, usersRepository,
notificationMessageBuilder: INotificationMessageBuilder notificationMessageBuilder,
) { }: {
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;
@@ -138,7 +151,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: { incidentId: resolvedIncident.id }, details: resolvedIncident.id,
}); });
return resolvedIncident; return resolvedIncident;
@@ -147,7 +160,7 @@ class IncidentService {
service: SERVICE_NAME, service: SERVICE_NAME,
method: "resolveIncident", method: "resolveIncident",
message: error.message, message: error.message,
details: { id: incidentId }, details: incidentId,
stack: error.stack, stack: error.stack,
}); });
throw error; throw error;
@@ -169,7 +182,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 = getDateForRange(dateRange); const startDate = dateRangeLookup[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;
@@ -194,7 +207,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;
@@ -216,7 +229,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;
@@ -237,7 +250,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;
+1 -52
View File
@@ -8,10 +8,8 @@ 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,
@@ -38,7 +36,6 @@ export interface IMonitorService {
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;
@@ -87,7 +84,6 @@ 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;
@@ -99,7 +95,6 @@ export class MonitorService implements IMonitorService {
games, games,
monitorsRepository, monitorsRepository,
checksRepository, checksRepository,
geoChecksRepository,
monitorStatsRepository, monitorStatsRepository,
statusPagesRepository, statusPagesRepository,
incidentsRepository, incidentsRepository,
@@ -110,7 +105,6 @@ 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;
@@ -121,7 +115,6 @@ 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;
@@ -225,8 +218,7 @@ 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 });
} }
@@ -323,40 +315,6 @@ export class MonitorService implements IMonitorService {
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;
@@ -480,14 +438,6 @@ 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;
}; };
@@ -499,7 +449,6 @@ 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) {
@@ -1,7 +1,5 @@
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 = {
@@ -56,12 +54,22 @@ export interface ISuperSimpleQueue {
class SuperSimpleQueue implements ISuperSimpleQueue { class SuperSimpleQueue implements ISuperSimpleQueue {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger; private logger: any;
private helper: ISuperSimpleQueueHelper; private helper: any;
private monitorsRepository: IMonitorsRepository; private monitorsRepository: IMonitorsRepository;
private readonly scheduler: Scheduler; private readonly scheduler: Scheduler;
constructor(logger: ILogger, helper: ISuperSimpleQueueHelper, monitorsRepository: IMonitorsRepository, scheduler: Scheduler) { constructor({
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;
@@ -72,14 +80,14 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
return SuperSimpleQueue.SERVICE_NAME; return SuperSimpleQueue.SERVICE_NAME;
} }
static async create(logger: ILogger, helper: ISuperSimpleQueueHelper, monitorsRepository: IMonitorsRepository) { static async create({ logger, helper, monitorsRepository }: { logger: any; helper: any; 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;
} }
@@ -105,7 +113,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: any) { } catch (error) {
this.logger.error({ this.logger.error({
message: "Failed to initialize SuperSimpleQueue", message: "Failed to initialize SuperSimpleQueue",
service: SERVICE_NAME, service: SERVICE_NAME,
@@ -125,7 +133,7 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
data: monitor, data: monitor,
}); });
// Add geo check job if enabled for HTTP monitors // Add geo check job if enabled
if (monitor.geoCheckEnabled && monitor.type === "http") { if (monitor.geoCheckEnabled && monitor.type === "http") {
this.scheduler.addJob({ this.scheduler.addJob({
id: `${monitorId}-geo`, id: `${monitorId}-geo`,
@@ -144,10 +152,11 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
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) { // Also pause geo check job if it exists
throw new Error("Failed to pause monitor");
}
await this.scheduler.pauseJob(`${monitor.id}-geo`); await this.scheduler.pauseJob(`${monitor.id}-geo`);
if (result === false) {
throw new Error("Failed to resume monitor");
}
this.logger.debug({ this.logger.debug({
message: `Paused monitor ${monitor.id}`, message: `Paused monitor ${monitor.id}`,
service: SERVICE_NAME, service: SERVICE_NAME,
@@ -157,12 +166,11 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
resumeJob = async (monitor: any) => { resumeJob = async (monitor: any) => {
const result = await this.scheduler.resumeJob(monitor.id); const result = await this.scheduler.resumeJob(monitor.id);
// Also resume geo check job if it exists
await this.scheduler.resumeJob(`${monitor.id}-geo`);
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,
@@ -173,16 +181,16 @@ 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 // Handle geo check job
const geoJobId = `${monitor.id}-geo`; const geoJobId = `${monitor.id}-geo`;
const geoJobExists = await this.scheduler.getJob(geoJobId);
if (monitor.geoCheckEnabled && monitor.type === "http") { if (monitor.geoCheckEnabled && monitor.type === "http") {
// Check if geo job exists if (geoJobExists) {
const existingGeoJob = await this.scheduler.getJob(geoJobId);
if (existingGeoJob) {
// Update existing geo job // Update existing geo job
this.scheduler.updateJob(geoJobId, { repeat: monitor.geoCheckInterval, active: monitor.isActive, data: monitor }); this.scheduler.updateJob(geoJobId, { repeat: monitor.geoCheckInterval, data: monitor, active: monitor.isActive });
} else { } else {
// Create new geo job // Add new geo job
this.scheduler.addJob({ this.scheduler.addJob({
id: geoJobId, id: geoJobId,
template: "geo-check-job", template: "geo-check-job",
@@ -191,8 +199,8 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
data: monitor, data: monitor,
}); });
} }
} else { } else if (geoJobExists) {
// Remove geo job if disabled or monitor type changed // Remove geo job if it exists but is no longer enabled
this.scheduler.removeJob(geoJobId); this.scheduler.removeJob(geoJobId);
} }
}; };
@@ -282,11 +290,7 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
}; };
obliterate = async () => { obliterate = async () => {
this.logger.warn({ console.log("obliterate not implemented");
message: "obliterate method not implemented",
service: SERVICE_NAME,
method: "obliterate",
});
}; };
} }
@@ -2,9 +2,8 @@ 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 } from "@/types/index.js"; import type { StatusChangeResult, MonitorStatusResponse, HardwareStatusPayload, MonitorStatus } 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,
@@ -12,18 +11,7 @@ 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;
@@ -39,7 +27,7 @@ export interface MonitorActionDecision {
}; };
} }
class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { class SuperSimpleQueueHelper {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private logger: any; private logger: any;
@@ -47,7 +35,7 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
private statusService: IStatusService; private statusService: IStatusService;
private notificationsService: INotificationsService; private notificationsService: INotificationsService;
private checkService: any; private checkService: any;
private buffer: IBufferService; private buffer: any;
private incidentService: IncidentService; private incidentService: IncidentService;
private maintenanceWindowsRepository: IMaintenanceWindowsRepository; private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
private monitorsRepository: IMonitorsRepository; private monitorsRepository: IMonitorsRepository;
@@ -55,26 +43,36 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
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: ILogger, logger,
networkService: INetworkService, networkService,
statusService: IStatusService, statusService,
notificationsService: INotificationsService, notificationsService,
checkService: any, checkService,
buffer: IBufferService, buffer,
incidentService: IncidentService, incidentService,
maintenanceWindowsRepository: IMaintenanceWindowsRepository, maintenanceWindowsRepository,
monitorsRepository: IMonitorsRepository, monitorsRepository,
teamsRepository: ITeamsRepository, teamsRepository,
monitorStatsRepository: IMonitorStatsRepository, monitorStatsRepository,
checksRepository: IChecksRepository, checksRepository,
incidentsRepository: IIncidentsRepository, incidentsRepository,
geoChecksService: IGeoChecksService, }: {
geoChecksRepository: IGeoChecksRepository logger: any;
) { 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;
@@ -88,8 +86,6 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
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() {
@@ -180,6 +176,98 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
}; };
}; };
getGeoCheckJob = () => {
return async (monitor: Monitor) => {
try {
const monitorId = monitor.id;
const teamId = monitor.teamId;
if (!monitorId) {
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" });
}
// Safety checks
if (!monitor.geoCheckEnabled || monitor.type !== "http") {
this.logger.debug({
message: `Geo check skipped for monitor ${monitorId} - not enabled or not HTTP type`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
return;
}
if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) {
this.logger.warn({
message: `Geo check skipped for monitor ${monitorId} - no locations configured`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
return;
}
// Check for maintenance window
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId);
if (maintenanceWindowActive) {
this.logger.debug({
message: `Geo check skipped for monitor ${monitorId} - in maintenance window`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
return;
}
this.logger.debug({
message: `Starting geo check for monitor ${monitorId}`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
// TODO: Implement GlobalPing API integration
// Step 1: Create measurement via GlobalPing API
// const measurementId = await this.globalPingService.createMeasurement(
// monitor.url,
// monitor.geoCheckLocations
// );
// Step 2: Poll for results (blocks 2-30s, but doesn't affect primary monitor checks)
// const geoResults = await this.globalPingService.pollForResults(measurementId);
// Step 3: Create check document with only geo results
// const check = {
// metadata: {
// monitorId: monitor.id,
// teamId: monitor.teamId,
// type: monitor.type,
// },
// status: true,
// responseTime: 0,
// statusCode: 200,
// message: "Geo check",
// geoResults,
// ack: false,
// };
// Step 4: Save check
// await this.checksRepository.create([check]);
this.logger.info({
message: `Geo check completed for monitor ${monitorId}`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
} catch (error: any) {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "getGeoCheckJob",
details: error,
stack: error.stack,
});
throw error;
}
};
};
getCleanupOrphanedJob = () => { getCleanupOrphanedJob = () => {
return async () => { return async () => {
try { try {
@@ -244,16 +332,6 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
}); });
} }
// 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,
@@ -271,81 +349,6 @@ class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
}; };
}; };
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,39 +1,29 @@
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: ILogger; private logger: any;
private SERVICE_NAME: string; private SERVICE_NAME: string;
private buffer: Check[]; private buffer: any[];
private geoBuffer: GeoCheck[];
private bufferTimer: NodeJS.Timeout | null = null; private bufferTimer: NodeJS.Timeout | null = null;
private checksService: ICheckService; private checksService: any;
private geoChecksService: IGeoChecksService;
constructor(logger: ILogger, checkService: ICheckService, geoChecksService: IGeoChecksService, settingsService: ISettingsService) { constructor({ logger, checkService, settingsService }: { logger: any; checkService: any; settingsService: any }) {
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`,
@@ -49,25 +39,12 @@ class BufferService implements IBufferService {
addToBuffer(check: Check) { addToBuffer(check: Check) {
try { try {
this.buffer.push(check); this.buffer.push(check);
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? error.message : "Unknown error", message: error.message,
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "addToBuffer", method: "addToBuffer",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
});
}
}
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,
}); });
} }
} }
@@ -99,12 +76,12 @@ class BufferService implements IBufferService {
} }
return false; return false;
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? error.message : "Unknown error", message: error.message,
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "removeCheckFromBuffer", method: "removeCheckFromBuffer",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
return false; return false;
} }
@@ -117,13 +94,12 @@ class BufferService implements IBufferService {
this.bufferTimer = setTimeout(async () => { this.bufferTimer = setTimeout(async () => {
try { try {
await this.flushBuffer(); await this.flushBuffer();
await this.flushGeoBuffer(); } catch (error: any) {
} catch (error: unknown) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? error.message : "Unknown error", message: `Error in flush cycle: ${error.message}`,
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "scheduleNextFlush", method: "scheduleNextFlush",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
} finally { } finally {
// Schedule the next flush only after the current one completes // Schedule the next flush only after the current one completes
@@ -134,47 +110,18 @@ 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: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? error.message : "Unknown error", message: error.message,
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "flushBuffer", method: "flushBuffer",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
// Clear buffer even on error to prevent infinite retry loops
this.buffer = [];
} }
}
async flushGeoBuffer() { this.buffer = [];
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,51 +1,27 @@
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;
export interface IEmailService { class EmailService {
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: ISettingsService; private settingsService: any;
private fs: FileSystem; private fs: any;
private path: PathModule; private path: any;
private compile: TemplateCompiler; private compile: any;
private mjml2html: MjmlFn; private mjml2html: any;
private nodemailer: Mailer; private nodemailer: any;
private logger: ILogger; private logger: any;
private transporter: ReturnType<typeof import("nodemailer").createTransport> | null = null; private transporter: any;
private templateLookup: Record<string, ((context: Record<string, unknown>) => string) | undefined>; private templateLookup: Record<string, Function>;
private loadTemplate: (templateName: string) => ((context: Record<string, unknown>) => string) | undefined; private loadTemplate: (templateName: string) => Function;
constructor( constructor(settingsService: any, fs: any, path: any, compile: any, mjml2html: any, nodemailer: any, logger: any) {
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;
@@ -54,7 +30,9 @@ class EmailService implements IEmailService {
this.nodemailer = nodemailer; this.nodemailer = nodemailer;
this.logger = logger; this.logger = logger;
this.templateLookup = {}; this.templateLookup = {};
this.loadTemplate = () => undefined; this.loadTemplate = () => {
return () => {};
};
this.init(); this.init();
} }
@@ -68,12 +46,12 @@ class EmailService implements IEmailService {
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: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? error.message : "Unknown error", message: error.message,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "loadTemplate", method: "loadTemplate",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
} }
}; };
@@ -88,20 +66,17 @@ class EmailService implements IEmailService {
}; };
}; };
buildEmail = async (template: string, context: Record<string, unknown>) => { buildEmail = async (template: string, context: Record<string, any>) => {
try { try {
const mjml = this.templateLookup[template]?.(context); const mjml = this.templateLookup[template]?.(context);
if (!mjml) { const html = await this.mjml2html(mjml);
throw new Error(`Template ${template} not found`);
}
const html = this.mjml2html(mjml);
return html.html; return html.html;
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? error.message : "Unknown error", message: error.message,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "buildEmail", method: "buildEmail",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
} }
}; };
@@ -150,12 +125,11 @@ class EmailService implements IEmailService {
try { try {
await this.transporter.verify(); await this.transporter.verify();
} catch (error: unknown) { } catch (error) {
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;
} }
@@ -168,12 +142,12 @@ class EmailService implements IEmailService {
html: html, html: html,
}); });
return info?.messageId; return info?.messageId;
} catch (error: unknown) { } catch (error: any) {
this.logger.error({ this.logger.error({
message: error instanceof Error ? error.message : "Unknown error", message: error.message,
service: SERVICE_NAME, service: SERVICE_NAME,
method: "sendEmail", method: "sendEmail",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
} }
}; };
@@ -1,210 +0,0 @@
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,25 +1,8 @@
import { HTTPError, RequestError } from "got"; import { HTTPError, RequestError } from "got";
import type { Got, Response } from "got"; import type { Got, Response } from "got";
import type { import type { Monitor, MonitorStatusResponse } from "@/types/index.js";
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">>;
@@ -38,25 +21,8 @@ interface BuildStatusResponseArgs<T> {
export interface INetworkService { export interface INetworkService {
readonly serviceName: string; readonly serviceName: string;
requestStatus( requestStatus(monitor: Monitor): Promise<MonitorStatusResponse>;
monitor: Monitor requestWebhook(type: string, url: string, body: any): Promise<{ type: string; status: boolean; code: number; message: string; payload?: unknown }>;
): 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;
@@ -76,23 +42,20 @@ 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: AxiosStatic; private axios: any;
private got: Got; private got: Got;
private https: typeof import("https"); private https: any;
private jmespath: { search: (data: unknown, expression: string) => unknown }; private jmespath: any;
private GameDig: { query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }> }; private GameDig: any;
private ping: typeof import("ping"); private ping: any;
private logger: ILogger; private logger: any;
private Docker: typeof import("dockerode"); private Docker: any;
private net: typeof import("net"); private net: any;
private settingsService: ISettingsService; private settingsService: any;
private grpc: typeof import("@grpc/grpc-js");
private protoLoader: typeof import("@grpc/proto-loader");
private buildStatusResponse = <T>({ private buildStatusResponse = <T>({
monitor, monitor,
@@ -127,7 +90,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 ?? {}) } as MonitorStatusResponse<T>; return { ...statusResponse, ...(overrides ?? {}) };
} }
return { return {
@@ -145,23 +108,34 @@ class NetworkService implements INetworkService {
expectedValue, expectedValue,
extracted, extracted,
...(overrides ?? {}), ...(overrides ?? {}),
} as MonitorStatusResponse<T>; };
}; };
constructor( constructor({
axios: AxiosStatic, axios,
got: Got, got,
https: typeof import("https"), https,
jmespath: { search: (data: unknown, expression: string) => unknown }, jmespath,
GameDig: { query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }> }, GameDig,
ping: typeof import("ping"), ping,
logger: ILogger, logger,
Docker: typeof import("dockerode"), http,
net: typeof import("net"), Docker,
settingsService: ISettingsService, net,
grpc: typeof import("@grpc/grpc-js"), settingsService,
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";
@@ -169,7 +143,6 @@ 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;
@@ -182,8 +155,6 @@ 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();
@@ -214,26 +185,13 @@ class NetworkService implements INetworkService {
} }
// Main entry point // Main entry point
async requestStatus( async requestStatus(monitor: Monitor): Promise<MonitorStatusResponse> {
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<HttpStatusPayload>(monitor); return await this.requestHttp(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:
@@ -244,14 +202,12 @@ 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<PingStatusPayload>> { private async requestPing(monitor: Monitor): Promise<MonitorStatusResponse> {
try { try {
if (!monitor?.url) { if (!monitor?.url) {
throw new Error("Monitor URL is required"); throw new Error("Monitor URL is required");
@@ -271,33 +227,36 @@ class NetworkService implements INetworkService {
throw new Error("Ping failed - no result returned"); throw new Error("Ping failed - no result returned");
} }
const pingResponse = this.buildStatusResponse<PingStatusPayload>({ const pingResponse = this.buildStatusResponse({
monitor, monitor,
payload: response as PingStatusPayload, payload: response,
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 as PingStatusPayload, payload: response,
} 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: unknown) { } catch (err: any) {
const originalMessage = err instanceof Error ? err.message : String(err); err.service = this.SERVICE_NAME;
throw new AppError({ err.method = "requestPing";
message: originalMessage || "Error performing ping", throw err;
service: this.SERVICE_NAME,
method: "requestPing",
details: { url: monitor.url },
});
} }
} }
private async requestHttp<T = unknown>(monitor: Monitor): Promise<MonitorStatusResponse<T>> { private async requestHttp(monitor: Monitor): Promise<MonitorStatusResponse> {
const { url, secret, ignoreTlsErrors, useAdvancedMatching, jsonPath, matchMethod, expectedValue, basicAuthUser, basicAuthPassword } = monitor; const { url, secret, id, teamId, type, ignoreTlsErrors, useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor;
const httpResponse = this.buildStatusResponse<T>({ const httpResponse = this.buildStatusResponse({
monitor, monitor,
overrides: { overrides: {
status: false, status: false,
@@ -310,15 +269,8 @@ class NetworkService implements INetworkService {
if (!url) { if (!url) {
throw new Error("Monitor URL is required"); throw new Error("Monitor URL is required");
} }
const headers: Record<string, string> = {}; const config: Record<string, any> = {
if (secret) { headers: secret ? { Authorization: `Bearer ${secret}` } : undefined,
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) {
@@ -331,7 +283,7 @@ class NetworkService implements INetworkService {
const response = await this.got(url, config); const response = await this.got(url, config);
let payload: T | string | undefined; let payload: any;
const contentType = response.headers["content-type"]; const contentType = response.headers["content-type"];
if (contentType && contentType.includes("application/json")) { if (contentType && contentType.includes("application/json")) {
@@ -419,27 +371,23 @@ class NetworkService implements INetworkService {
} }
} }
return httpResponse; return httpResponse;
} catch (err: unknown) { } catch (err: any) {
if (err instanceof HTTPError || err instanceof RequestError) { if (err.name === "HTTPError" || err.name === "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 = String(err.response?.statusCode) || err.message; httpResponse.message = 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 || undefined; httpResponse.timings = err?.timings || {};
return httpResponse; return httpResponse;
} }
const originalMessage = err instanceof Error ? err.message : String(err); err.service = this.SERVICE_NAME;
throw new AppError({ err.method = "requestHttp";
message: originalMessage || "Error performing HTTP request", throw err;
service: this.SERVICE_NAME,
method: "requestHttp",
details: { url: monitor.url },
});
} }
} }
private async requestPageSpeed(monitor: Monitor): Promise<MonitorStatusResponse<PageSpeedStatusPayload>> { private async requestPageSpeed(monitor: Monitor): Promise<MonitorStatusResponse> {
try { try {
const url = monitor.url; const url = monitor.url;
if (!url) { if (!url) {
@@ -457,36 +405,28 @@ class NetworkService implements INetworkService {
details: { url }, details: { url },
}); });
} }
return await this.requestHttp<PageSpeedStatusPayload>({ return await this.requestHttp({
...monitor, ...monitor,
url: pageSpeedUrl, url: pageSpeedUrl,
}); });
} catch (err: unknown) { } catch (err: any) {
const originalMessage = err instanceof Error ? err.message : String(err); err.service = this.SERVICE_NAME;
throw new AppError({ err.method = "requestPageSpeed";
message: originalMessage || "Error performing PageSpeed request", throw err;
service: this.SERVICE_NAME,
method: "requestPageSpeed",
details: { url: monitor.url },
});
} }
} }
private async requestHardware(monitor: Monitor): Promise<MonitorStatusResponse<HardwareStatusPayload>> { private async requestHardware(monitor: Monitor): Promise<MonitorStatusResponse> {
try { try {
return await this.requestHttp<HardwareStatusPayload>(monitor); return await this.requestHttp(monitor);
} catch (err: unknown) { } catch (err: any) {
const originalMessage = err instanceof Error ? err.message : String(err); err.service = this.SERVICE_NAME;
throw new AppError({ err.method = "requestHardware";
message: originalMessage || "Error performing hardware request", throw err;
service: this.SERVICE_NAME,
method: "requestHardware",
details: { url: monitor.url },
});
} }
} }
private async requestDocker(monitor: Monitor): Promise<MonitorStatusResponse<DockerStatusPayload>> { private async requestDocker(monitor: Monitor): Promise<MonitorStatusResponse> {
try { try {
if (!monitor.url) { if (!monitor.url) {
throw new Error("Monitor URL is required"); throw new Error("Monitor URL is required");
@@ -494,9 +434,10 @@ 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<DockerStatusPayload>({ const dockerResponse = this.buildStatusResponse({
monitor, monitor,
overrides: { overrides: {
status: false, status: false,
@@ -512,10 +453,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)
const exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput); let exactIdMatch = containers.find((c: any) => c.Id.toLowerCase() === normalizedInput);
// 2. Exact container name match (case-insensitive) // 2. Exact container name match (case-insensitive)
const exactNameMatch = containers.find((c) => let exactNameMatch = containers.find((c: any) =>
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;
@@ -523,10 +464,10 @@ class NetworkService implements INetworkService {
); );
// 3. Partial ID match (fallback for backwards compatibility) // 3. Partial ID match (fallback for backwards compatibility)
const partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput)); let partialIdMatch = containers.find((c: any) => c.Id.toLowerCase().startsWith(normalizedInput));
// Select container based on priority // Select container based on priority
const targetContainer = exactIdMatch || exactNameMatch || partialIdMatch; let targetContainer = exactIdMatch || exactNameMatch || partialIdMatch;
// Return negative response if no container // Return negative response if no container
if (!targetContainer) { if (!targetContainer) {
@@ -562,8 +503,9 @@ class NetworkService implements INetworkService {
} }
const container = docker.getContainer(targetContainer.Id); const container = docker.getContainer(targetContainer.Id);
const { response, responseTime, error }: { response?: { State: { Status: string } } | null; responseTime: number; error: unknown } = const { response, responseTime, error }: { response?: any; responseTime?: number; error?: any } = await this.timeRequest(() =>
await this.timeRequest(() => container.inspect()); container.inspect()
);
dockerResponse.responseTime = responseTime; dockerResponse.responseTime = responseTime;
dockerResponse.status = response?.State?.Status === "running" ? true : false; dockerResponse.status = response?.State?.Status === "running" ? true : false;
@@ -572,34 +514,22 @@ class NetworkService implements INetworkService {
if (error) { if (error) {
dockerResponse.status = false; dockerResponse.status = false;
dockerResponse.code = dockerResponse.code = error.statusCode || this.NETWORK_ERROR;
(error && typeof error === "object" && "statusCode" in error ? (error as { statusCode?: number }).statusCode : undefined) || dockerResponse.message = error.reason || "Failed to fetch Docker container information";
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: unknown) { } catch (err: any) {
const originalMessage = err instanceof Error ? err.message : String(err); err.service = this.SERVICE_NAME;
throw new AppError({ err.method = "requestDocker";
message: originalMessage || "Error performing Docker request", throw err;
service: this.SERVICE_NAME,
method: "requestDocker",
details: { url: monitor.url },
});
} }
} }
private async requestPort(monitor: Monitor): Promise<MonitorStatusResponse<PortStatusPayload>> { private async requestPort(monitor: Monitor): Promise<MonitorStatusResponse> {
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(
@@ -620,14 +550,14 @@ class NetworkService implements INetworkService {
reject(new Error("Connection timeout")); reject(new Error("Connection timeout"));
}); });
socket.on("error", (err: unknown) => { socket.on("error", (err: any) => {
socket.destroy(); socket.destroy();
reject(err); reject(err);
}); });
}); });
}); });
const portResponse = this.buildStatusResponse<PortStatusPayload>({ const portResponse = this.buildStatusResponse({
monitor, monitor,
overrides: { overrides: {
code: 200, code: 200,
@@ -645,22 +575,18 @@ class NetworkService implements INetworkService {
} }
return portResponse; return portResponse;
} catch (err: unknown) { } catch (error: any) {
const originalMessage = err instanceof Error ? err.message : String(err); error.service = this.SERVICE_NAME;
throw new AppError({ error.method = "requestTCP";
message: originalMessage || "Error performing port check", throw error;
service: this.SERVICE_NAME,
method: "requestPort",
details: { url: monitor.url, port: monitor.port },
});
} }
} }
private async requestGame(monitor: Monitor): Promise<MonitorStatusResponse<GameStatusPayload>> { private async requestGame(monitor: Monitor): Promise<MonitorStatusResponse> {
try { try {
const { url, port, gameId } = monitor; const { url, port, gameId } = monitor;
const gameResponse = this.buildStatusResponse<GameStatusPayload>({ const gameResponse = this.buildStatusResponse({
monitor, monitor,
overrides: { overrides: {
code: 200, code: 200,
@@ -670,17 +596,16 @@ class NetworkService implements INetworkService {
}); });
const state = await this.GameDig.query({ const state = await this.GameDig.query({
type: gameId ?? "unknown", type: gameId,
host: url, host: url,
port: port ?? 0, port: port,
}).catch((error: unknown): undefined => { }).catch((error: any) => {
this.logger.warn({ this.logger.warn({
message: error instanceof Error ? error.message : String(error), message: error.message,
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) {
@@ -690,176 +615,15 @@ class NetworkService implements INetworkService {
return gameResponse; return gameResponse;
} }
gameResponse.responseTime = state.ping ?? 0; gameResponse.responseTime = state.ping;
gameResponse.payload = state as GameStatusPayload; gameResponse.payload = state;
return gameResponse; return gameResponse;
} catch (err: unknown) { } catch (error: any) {
const originalMessage = err instanceof Error ? err.message : String(err); error.service = this.SERVICE_NAME;
throw new AppError({ error.method = "requestPing";
message: originalMessage || "Error performing game server check", throw error;
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",
@@ -872,7 +636,7 @@ class NetworkService implements INetworkService {
} }
// Other network requests unrelated to monitoring: // Other network requests unrelated to monitoring:
async requestWebhook(type: string, url: string, body: unknown) { async requestWebhook(type: string, url: string, body: any) {
try { try {
const response = await this.axios.post(url, body, { const response = await this.axios.post(url, body, {
headers: { headers: {
@@ -887,29 +651,19 @@ class NetworkService implements INetworkService {
message: `Successfully sent ${type} notification`, message: `Successfully sent ${type} notification`,
payload: response.data, payload: response.data,
}; };
} catch (err: unknown) { } catch (error: any) {
this.logger.warn({ this.logger.warn({
message: err instanceof Error ? err.message : String(err), message: error.message,
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: this.NETWORK_ERROR, code: error.response?.status || this.NETWORK_ERROR,
message: `Failed to send ${type} notification`, message: `Failed to send ${type} notification`,
payload: error.response?.data,
}; };
} }
} }
@@ -929,17 +683,11 @@ class NetworkService implements INetworkService {
if (response?.data?.status !== "success") return false; if (response?.data?.status !== "success") return false;
return true; return true;
} catch (err: unknown) { } catch (error: any) {
const originalMessage = err instanceof Error ? err.message : String(err); error.details = error.response?.data;
error.service = this.SERVICE_NAME;
throw new AppError({ error.method = "requestPagerDuty";
message: originalMessage || "Error sending PagerDuty notification", throw error;
service: this.SERVICE_NAME,
method: "requestPagerDuty",
details: {
responseData: err && typeof err === "object" && "response" in err ? (err as { response?: { data?: unknown } }).response?.data : undefined,
},
});
} }
} }
@@ -973,35 +721,18 @@ class NetworkService implements INetworkService {
code: response.status, code: response.status,
message: "Successfully sent Matrix notification", message: "Successfully sent Matrix notification",
}; };
} catch (err: unknown) { } catch (error: any) {
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: String(err), message: error.message,
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "requestMatrix", method: "requestMatrix",
}); });
return { return {
status: false, status: false,
code: this.NETWORK_ERROR, code: error.response?.status || 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 { HardwareStatusPayload, Monitor, MonitorStatusResponse } from "@/types/index.js"; import type { 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); const severity = this.determineSeverity(type, monitor.status);
const content = this.buildContent(type, monitor, monitorStatusResponse); const content = this.buildContent(type, monitor, monitorStatusResponse, decision, clientHost);
return { return {
type, type,
@@ -77,7 +77,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
return "monitor_up"; return "monitor_up";
} }
private determineSeverity(type: NotificationType): NotificationSeverity { private determineSeverity(type: NotificationType, monitorStatus: string): NotificationSeverity {
switch (type) { switch (type) {
case "monitor_down": case "monitor_down":
return "critical"; return "critical";
@@ -93,22 +93,28 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
} }
} }
private buildContent(type: NotificationType, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): NotificationContent { private buildContent(
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); return this.buildMonitorDownContent(monitor, monitorStatusResponse, clientHost);
case "monitor_up": case "monitor_up":
return this.buildMonitorUpContent(monitor); return this.buildMonitorUpContent(monitor, clientHost);
case "threshold_breach": case "threshold_breach":
return this.buildThresholdBreachContent(monitor, monitorStatusResponse as MonitorStatusResponse<HardwareStatusPayload>); return this.buildThresholdBreachContent(monitor, monitorStatusResponse, decision, clientHost);
case "threshold_resolved": case "threshold_resolved":
return this.buildThresholdResolvedContent(monitor); return this.buildThresholdResolvedContent(monitor, clientHost);
default: default:
return this.buildDefaultContent(monitor); return this.buildDefaultContent(monitor, clientHost);
} }
} }
private buildMonitorDownContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): NotificationContent { private buildMonitorDownContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, clientHost: string): 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}`];
@@ -131,7 +137,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
}; };
} }
private buildMonitorUpContent(monitor: Monitor): NotificationContent { private buildMonitorUpContent(monitor: Monitor, clientHost: string): 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}`];
@@ -144,7 +150,12 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
}; };
} }
private buildThresholdBreachContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse<HardwareStatusPayload>): NotificationContent { private buildThresholdBreachContent(
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}`];
@@ -160,7 +171,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
}; };
} }
private buildThresholdResolvedContent(monitor: Monitor): NotificationContent { private buildThresholdResolvedContent(monitor: Monitor, clientHost: string): 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}`];
@@ -173,7 +184,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
}; };
} }
private buildDefaultContent(monitor: Monitor): NotificationContent { private buildDefaultContent(monitor: Monitor, clientHost: string): 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}".`,
@@ -182,7 +193,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
}; };
} }
public extractThresholdBreaches(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse<HardwareStatusPayload>): ThresholdBreach[] { public extractThresholdBreaches(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): 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
@@ -191,7 +202,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
} }
// Cast to HardwareStatusPayload type // Cast to HardwareStatusPayload type
const payload = monitorStatusResponse.payload; const payload = monitorStatusResponse.payload as { data?: any };
const hardware = payload.data; const hardware = payload.data;
if (!hardware) { if (!hardware) {
@@ -1,15 +1,14 @@
const SERVICE_NAME = "DiscordProvider"; const SERVICE_NAME = "DiscordProvider";
import type { AlertDiscordPayload, DiscordEmbedField, Notification } from "@/types/index.js"; import type { 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, NotificationSeverity } from "@/types/notificationMessage.js"; import type { NotificationMessage } 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: ILogger; private logger: any;
constructor(logger: ILogger) { constructor(logger: any) {
this.logger = logger; this.logger = logger;
} }
@@ -37,6 +36,9 @@ 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({
@@ -69,17 +71,18 @@ export class DiscordProvider implements INotificationProvider {
} }
} }
private buildDiscordEmbed(message: NotificationMessage): AlertDiscordPayload { private buildDiscordEmbed(message: NotificationMessage): any {
const colorMap: Record<NotificationSeverity, number> = { // Map severity to Discord embed colors
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<DiscordEmbedField> = []; const fields: Array<{ name: string; value: string; inline?: boolean }> = [];
// Add monitor details // Add monitor details
fields.push({ fields.push({
@@ -1,15 +1,15 @@
const SERVICE_NAME = "EmailProvider"; const SERVICE_NAME = "EmailProvider";
import type { Notification } from "@/types/index.js"; import type { Monitor, Notification, MonitorStatusResponse } 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 {
private emailService: IEmailService;
private logger: ILogger;
constructor(emailService: IEmailService, logger: ILogger) { export class EmailProvider implements INotificationProvider {
private emailService: any;
private logger: any;
constructor(emailService: any, logger: any) {
this.emailService = emailService; this.emailService = emailService;
this.logger = logger; this.logger = logger;
} }
@@ -18,24 +18,6 @@ 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({
@@ -56,15 +38,6 @@ 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({
@@ -92,7 +65,7 @@ export class EmailProvider implements INotificationProvider {
} }
} }
private async buildEmailFromMessage(message: NotificationMessage): Promise<string | undefined> { private async buildEmailFromMessage(message: NotificationMessage): Promise<string> {
const context = { const context = {
title: message.content.title, title: message.content.title,
summary: message.content.summary, summary: message.content.summary,
@@ -115,6 +88,16 @@ 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,15 +1,14 @@
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 { AlertMatrixPayload, Notification } from "@/types/index.js"; import type { 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: ILogger; private logger: any;
constructor(logger: ILogger) { constructor(logger: any) {
this.logger = logger; this.logger = logger;
} }
@@ -43,6 +42,9 @@ 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;
@@ -85,7 +87,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): AlertMatrixPayload { private buildMatrixMessage(message: NotificationMessage): { plainText: string; htmlText: string } {
const plainLines: string[] = []; const plainLines: string[] = [];
const htmlLines: string[] = []; const htmlLines: string[] = [];
@@ -1,16 +1,15 @@
const SERVICE_NAME = "PagerDutyProvider"; const SERVICE_NAME = "PagerDutyProvider";
import got from "got"; import got from "got";
import type { Notification } from "@/types/index.js"; import type { Monitor, Notification, MonitorStatusResponse } 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: ILogger; private logger: any;
constructor(logger: ILogger) { constructor(logger: any) {
this.logger = logger; this.logger = logger;
} }
@@ -78,7 +77,7 @@ export class PagerDutyProvider implements INotificationProvider {
} }
} }
private buildPagerDutyPayload(notification: Notification, message: NotificationMessage): AlertPagerDutyPayload { private buildPagerDutyPayload(notification: Notification, message: NotificationMessage): any {
// 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";
@@ -107,7 +106,7 @@ export class PagerDutyProvider implements INotificationProvider {
} }
// Build custom details // Build custom details
const customDetails: Record<string, unknown> = { const customDetails: any = {
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 { Notification } from "@/types/index.js"; import type { Monitor, Notification, MonitorStatusResponse } 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: ILogger; private logger: any;
constructor(logger: ILogger) { constructor(logger: any) {
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: unknown[] = []; const blocks: any[] = [];
// Determine color based on severity // Determine color based on severity
const colorMap = { const colorMap = {
@@ -1,5 +1,6 @@
import { IEmailService } from "@/service/infrastructure/emailService.js"; import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js";
export const buildTestEmail = async (emailService: IEmailService) => { // Test notification helpers - used by all providers for test alerts
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 { Notification } from "@/types/index.js"; import type { Monitor, Alert, Notification, MonitorStatusResponse } 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: ILogger; private logger: any;
constructor(logger: ILogger) { constructor(logger: any) {
this.logger = logger; this.logger = logger;
} }
@@ -46,6 +46,10 @@ 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[] = [];
@@ -1,4 +1,4 @@
import type { Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification, MonitorStatus } from "@/types/index.js";
import type { NotificationMessage } from "@/types/notificationMessage.js"; import type { NotificationMessage } from "@/types/notificationMessage.js";
import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js"; import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js";
import { INotificationProvider } from "./notificationProviders/INotificationProvider.js"; import { INotificationProvider } from "./notificationProviders/INotificationProvider.js";
@@ -1,21 +0,0 @@
syntax = "proto3";
package grpc.health.v1;
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
SERVICE_UNKNOWN = 3;
}
ServingStatus status = 1;
}
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
}
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff. Show more