Compare commits

..
Author SHA1 Message Date
Alexander Holliday e3ecfe6ad8 Merge pull request #3323 from bluewave-labs/develop
develop -> master
2026-02-20 12:38:33 -08:00
Alexander Holliday f4664eec16 Merge pull request #3321 from bluewave-labs/develop
develop->master
2026-02-20 12:20:49 -08:00
Alexander Holliday 56f601e34b Merge pull request #3319 from bluewave-labs/develop
develop -> master
2026-02-20 12:07:29 -08:00
Alexander Holliday 1244ff6d93 Merge pull request #3212 from bluewave-labs/develop
develop -> master
2026-01-27 13:58:48 -08:00
Alexander Holliday c0ea804c98 Merge pull request #3209 from bluewave-labs/develop
develop -> master
2026-01-27 13:40:25 -08:00
Alexander Holliday c21cf29db6 Merge pull request #3207 from bluewave-labs/develop
develop -> master
2026-01-27 13:20:18 -08:00
Alexander Holliday 3d0da70ce6 Merge pull request #3204 from bluewave-labs/develop
develop -> master
2026-01-27 13:09:47 -08:00
Alexander Holliday feb061240b Merge pull request #3042 from bluewave-labs/develop
develp->master
2025-10-27 12:09:00 -07:00
Alexander Holliday a924227805 Merge pull request #2964 from bluewave-labs/develop
develop -> master
2025-09-18 13:40:45 -07:00
Alexander Holliday 2144d3f8c0 Merge pull request #2962 from bluewave-labs/develop
develop -> master
2025-09-18 12:04:51 -07:00
Alexander Holliday fcd6786149 Merge pull request #2936 from bluewave-labs/develop
develop -> master
2025-09-08 20:43:21 -07:00
Alexander Holliday ea968ebf3c Merge pull request #2933 from bluewave-labs/develop
develop -> master
2025-09-08 20:33:35 -07:00
Alexander Holliday 8a7b852974 Merge pull request #2929 from bluewave-labs/develop
develop -> master
2025-09-08 07:24:16 -07:00
Alexander Holliday c8d3e3b70c Merge pull request #2921 from bluewave-labs/develop
develop -> master
2025-09-04 15:46:30 -07:00
Alexander Holliday 7a90a6f9f4 Merge pull request #2915 from bluewave-labs/develop
develop->master
2025-09-01 10:33:09 -07:00
175 changed files with 3924 additions and 8805 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
+15 -8
View File
@@ -42,15 +42,15 @@ 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
Usage instructions can be found [here](https://checkmate.so/docs). Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1). It's still WIP and some of the information there might be outdated as we continuously add features weekly. Rest assured, we are doing our best! :)
## Installation ## Installation
See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs). See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
Alternatively, you can also use [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions. Alternatively, you can also use [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
@@ -157,11 +157,12 @@ Here's how you can contribute:
0. Star this repo :) 0. Star this repo :)
1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag. 1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag.
2. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture. 2. Check [project structure](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
3. Open an issue if you believe you've encountered a bug. 3. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
4. Check for good-first-issue's if you are a newcomer. 4. Open an issue if you believe you've encountered a bug.
5. Make a pull request to add new features/make quality-of-life improvements/fix bugs. 5. Check for good-first-issue's if you are a newcomer.
6. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram). 6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
7. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram).
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors"> <a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" /> <img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
@@ -174,3 +175,9 @@ Here's how you can contribute:
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features). If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features).
Also check other developer and contributor-friendly projects of BlueWave:
- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform.
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners.
+104 -690
View File
File diff suppressed because it is too large. Load diff
+3 -6
View File
@@ -13,7 +13,7 @@
"format-check": "prettier --check ." "format-check": "prettier --check ."
}, },
"lint-staged": { "lint-staged": {
"**/*": "prettier --write" "**/*": "npm run format"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.13.3", "@emotion/react": "^11.13.3",
@@ -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",
@@ -52,7 +49,7 @@
"redux-persist": "6.0.0", "redux-persist": "6.0.0",
"swr": "^2.3.6", "swr": "^2.3.6",
"vite-plugin-svgr": "^4.2.0", "vite-plugin-svgr": "^4.2.0",
"zod": "^4.3.6" "zod": "^4.1.11"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "24.5.2", "@types/node": "24.5.2",
@@ -66,6 +63,6 @@
"lint-staged": "^16.2.7", "lint-staged": "^16.2.7",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"typescript": "5.9.3", "typescript": "5.9.3",
"vite": "6.4.1" "vite": "6.3.6"
} }
} }
+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>
); );
} }
@@ -32,7 +32,7 @@ export const HeatmapResponseTime = ({
if (!checks || checks.length === 0) return null; if (!checks || checks.length === 0) return null;
const latestChecks = checks.slice(-25); const latestChecks = checks.slice(-25).reverse();
let data: Array<CheckSnapshot | PlaceholderCheck>; let data: Array<CheckSnapshot | PlaceholderCheck>;
if (latestChecks.length !== 25) { if (latestChecks.length !== 25) {
@@ -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
+3 -19
View File
@@ -11,26 +11,20 @@ import type { ResponsiveStyleValue } from "@mui/system";
interface SliderInputProps extends SliderProps { interface SliderInputProps extends SliderProps {
sx?: SliderProps["sx"]; sx?: SliderProps["sx"];
showValue?: boolean; showValue?: boolean;
formatDisplayValue?: (value: number) => string;
} }
export const SliderInput = forwardRef<HTMLSpanElement, SliderInputProps>( export const SliderInput = forwardRef<HTMLSpanElement, SliderInputProps>(
({ sx, showValue = false, formatDisplayValue, ...props }, ref) => { ({ sx, showValue = false, ...props }, ref) => {
const theme = useTheme(); const theme = useTheme();
const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : []; const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : [];
const displayValue =
showValue && formatDisplayValue && typeof props.value === "number"
? formatDisplayValue(props.value)
: props.value;
return ( return (
<Stack <Stack
gap={theme.spacing(8)} gap={theme.spacing(8)}
direction={"row"} direction={"row"}
alignItems={"center"} alignItems={"center"}
> >
{showValue && <Typography>{displayValue}</Typography>} {showValue && <Typography>{props.value}</Typography>}
<Slider <Slider
{...props} {...props}
ref={ref} ref={ref}
@@ -69,21 +63,12 @@ interface SliderWithLabelProps extends SliderProps {
fieldLabel?: string; fieldLabel?: string;
required?: boolean; required?: boolean;
showValue?: boolean; showValue?: boolean;
formatDisplayValue?: (value: number) => string;
sliderMaxWidth?: ResponsiveStyleValue<number | string>; sliderMaxWidth?: ResponsiveStyleValue<number | string>;
} }
export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>( export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>(
( (
{ { fieldLabel, required, showValue = true, value, sliderMaxWidth = "100%", ...props },
fieldLabel,
required,
showValue = true,
formatDisplayValue,
value,
sliderMaxWidth = "100%",
...props
},
ref ref
) => { ) => {
const theme = useTheme(); const theme = useTheme();
@@ -97,7 +82,6 @@ export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>
<SliderInput <SliderInput
{...props} {...props}
showValue={showValue} showValue={showValue}
formatDisplayValue={formatDisplayValue}
value={value} value={value}
ref={ref} ref={ref}
/> />
@@ -1,6 +1,9 @@
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import BackgroundSVG from "@/assets/Images/background.svg";
import type { RootState } from "@/Types/state";
import { OfflineBanner } from "@/Components/design-elements"; import { OfflineBanner } from "@/Components/design-elements";
import { setServerUnreachableCallback, get } from "@/Utils/ApiClient"; import { setServerUnreachableCallback, get } from "@/Utils/ApiClient";
@@ -10,6 +13,7 @@ interface AppLayoutProps {
const AppLayout = ({ children }: AppLayoutProps) => { const AppLayout = ({ children }: AppLayoutProps) => {
const theme = useTheme(); const theme = useTheme();
const mode = useSelector((state: RootState) => state.ui.mode);
const [serverUnreachable, setServerUnreachable] = useState(false); const [serverUnreachable, setServerUnreachable] = useState(false);
const retryIntervalRef = useRef<number | null>(null); const retryIntervalRef = useRef<number | null>(null);
@@ -43,6 +47,10 @@ const AppLayout = ({ children }: AppLayoutProps) => {
sx={{ sx={{
minHeight: "100vh", minHeight: "100vh",
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
backgroundImage: mode === "dark" ? `url("${BackgroundSVG}")` : "none",
backgroundSize: "100% 100%",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}} }}
> >
<OfflineBanner visible={serverUnreachable} /> <OfflineBanner visible={serverUnreachable} />
@@ -5,18 +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", "websocket"]; const types = ["http", "ping", "port", "docker", "game"];
const typeDisplayNames: Record<string, string> = {
http: "HTTP",
ping: "Ping",
port: "Port",
docker: "Docker",
game: "Game",
grpc: "gRPC",
websocket: "WebSocket",
};
const statuses = ["up", "down"]; const statuses = ["up", "down"];
const states = ["active", "paused"]; const states = ["active", "paused"];
@@ -40,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 !== "";
@@ -61,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>
@@ -99,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>
);
};
@@ -12,7 +12,7 @@ import {
import { HistogramPageSpeedScoresTooltip } from "@/Components/monitors"; import { HistogramPageSpeedScoresTooltip } from "@/Components/monitors";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import type { PageSpeedGroupedCheck } from "@/Types/Check"; import type { CheckSnapshot } from "@/Types/Check";
import type { Palette } from "@mui/material/styles"; import type { Palette } from "@mui/material/styles";
type PaletteColorKey = Extract< type PaletteColorKey = Extract<
keyof Palette, keyof Palette,
@@ -52,7 +52,7 @@ export const HistogramPageSpeedDetails = ({
checks, checks,
range, range,
}: { }: {
checks: PageSpeedGroupedCheck[]; checks: CheckSnapshot[];
range: string; range: string;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
@@ -73,7 +73,7 @@ export const HistogramPageSpeedDetails = ({
> >
<AreaChart data={checks}> <AreaChart data={checks}>
<XAxis <XAxis
dataKey={"bucketDate"} dataKey={"createdAt"}
tick={(props) => ( tick={(props) => (
<XTick <XTick
{...props} {...props}
@@ -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);
-21
View File
@@ -14,9 +14,6 @@ const getBaseDefaults = (data?: Monitor | null) => ({
notifications: data?.notifications || [], notifications: data?.notifications || [],
statusWindowSize: data?.statusWindowSize || 5, statusWindowSize: data?.statusWindowSize || 5,
statusWindowThreshold: data?.statusWindowThreshold || 60, statusWindowThreshold: data?.statusWindowThreshold || 60,
geoCheckEnabled: data?.geoCheckEnabled ?? false,
geoCheckLocations: data?.geoCheckLocations || [],
geoCheckInterval: data?.geoCheckInterval || 300000,
}); });
export const useMonitorForm = ({ export const useMonitorForm = ({
@@ -73,16 +70,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,
@@ -103,14 +90,6 @@ export const useMonitorForm = ({
selectedDisks: data?.selectedDisks || [], selectedDisks: data?.selectedDisks || [],
}; };
break; break;
case "websocket":
defaults = {
...base,
type: "websocket",
url: data?.url || "",
ignoreTlsErrors: data?.ignoreTlsErrors || false,
};
break;
default: default:
defaults = { defaults = {
...base, ...base,
+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: "",
+11 -64
View File
@@ -11,17 +11,12 @@ import { ConfigBox, BasePage } from "@/Components/design-elements";
import { UserRoles } from "@/Types/User"; import { UserRoles } from "@/Types/User";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { UserRole, User } from "@/Types/User"; import type { UserRole, User } from "@/Types/User";
import { useParams, useNavigate } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useTheme } from "@mui/material"; import { useTheme } from "@mui/material";
import { useEditUserForm } from "@/Hooks/useEditUserForm"; import { useEditUserForm } from "@/Hooks/useEditUserForm";
import { useGet, usePatch, useDelete } from "@/Hooks/UseApi"; import { useGet, usePatch } from "@/Hooks/UseApi";
import type { EditUserFormData } from "@/Validation/editUser"; import type { EditUserFormData } from "@/Validation/editUser";
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { DialogInput } from "@/Components/inputs/Dialog";
import { useSelector } from "react-redux";
import type { RootState } from "@/Types/state";
import { LAYOUT } from "@/Utils/Theme/constants";
import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin";
interface RoleOption { interface RoleOption {
id: UserRole; id: UserRole;
@@ -34,12 +29,6 @@ const EditUserPage = () => {
const { userId } = useParams<{ userId: string }>(); const { userId } = useParams<{ userId: string }>();
const { resolver, defaults } = useEditUserForm(); const { resolver, defaults } = useEditUserForm();
const { patch, loading: isSaving } = usePatch(); const { patch, loading: isSaving } = usePatch();
const { deleteFn, loading: isDeleting } = useDelete();
const navigate = useNavigate();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const currentUser = useSelector((state: RootState) => state.auth.user);
const isSuperAdmin = useIsSuperAdmin();
const isAdmin = useIsAdmin();
const { data: user, isLoading } = useGet<User>(`/auth/users/${userId}`); const { data: user, isLoading } = useGet<User>(`/auth/users/${userId}`);
@@ -71,20 +60,6 @@ const EditUserPage = () => {
setValue("role", newRoles, { shouldValidate: true }); setValue("role", newRoles, { shouldValidate: true });
}; };
const canDeleteUser =
isAdmin &&
userId !== currentUser?.id &&
!user?.role?.includes("demo") &&
(isSuperAdmin || user?.role?.every((r) => r !== "admin" && r !== "superadmin"));
const handleDeleteUser = async () => {
const result = await deleteFn(`/auth/users/${userId}`);
setShowDeleteDialog(false);
if (result) {
navigate("/account", { state: { tab: "team" } });
}
};
const onSubmit = async (data: EditUserFormData) => { const onSubmit = async (data: EditUserFormData) => {
await patch(`/auth/users/${userId}`, data); await patch(`/auth/users/${userId}`, data);
}; };
@@ -190,45 +165,17 @@ const EditUserPage = () => {
</Stack> </Stack>
} }
/> />
<Stack <Button
gap={LAYOUT.XS} type="submit"
direction="row" variant="contained"
justifyContent={"flex-end"} color="primary"
width="100%" loading={isSaving}
sx={{ alignSelf: "flex-end", minWidth: 100 }}
> >
{canDeleteUser && ( {t("common.buttons.save")}
<Button </Button>
variant="contained"
color="error"
onClick={() => setShowDeleteDialog(true)}
sx={{ minWidth: 100 }}
>
{t("common.buttons.removeUser")}
</Button>
)}
<Button
type="submit"
variant="contained"
color="primary"
loading={isSaving}
sx={{ minWidth: 100 }}
>
{t("common.buttons.save")}
</Button>
</Stack>
</Stack> </Stack>
</form> </form>
<DialogInput
open={showDeleteDialog}
title={t("pages.editUser.dialog.removeUser.title")}
content={t("pages.editUser.dialog.removeUser.content", {
name: `${user?.firstName} ${user?.lastName}`,
})}
onCancel={() => setShowDeleteDialog(false)}
onConfirm={handleDeleteUser}
confirmText={t("common.buttons.removeUser")}
loading={isDeleting}
/>
</BasePage> </BasePage>
); );
}; };
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Table } from "@/Components/design-elements"; import { Table } from "@/Components/design-elements";
import type { Header } from "@/Components/design-elements/Table"; import type { Header } from "@/Components/design-elements/Table";
import { useIsAdmin } from "@/Hooks/useIsAdmin"; import { useIsSuperAdmin } from "@/Hooks/useIsAdmin";
import type { User } from "@/Types/User"; import type { User } from "@/Types/User";
interface TeamTableProps { interface TeamTableProps {
@@ -13,7 +13,7 @@ interface TeamTableProps {
export const TeamTable = ({ users }: TeamTableProps) => { export const TeamTable = ({ users }: TeamTableProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const isAdmin = useIsAdmin(); const isSuperAdmin = useIsSuperAdmin();
const headers: Header<User>[] = [ const headers: Header<User>[] = [
{ {
@@ -45,7 +45,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
]; ];
const handleRowClick = (row: User) => { const handleRowClick = (row: User) => {
if (isAdmin) { if (isSuperAdmin) {
navigate(`/account/team/${row.id}`); navigate(`/account/team/${row.id}`);
} }
}; };
@@ -54,7 +54,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
<Table <Table
headers={headers} headers={headers}
data={users} data={users}
onRowClick={isAdmin ? handleRowClick : undefined} onRowClick={isSuperAdmin ? handleRowClick : undefined}
/> />
); );
}; };
@@ -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
+12 -233
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,15 +27,9 @@ 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 { import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
type Monitor,
type MonitorType,
type GamesMap,
supportsGeoCheck,
} 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";
@@ -49,8 +41,6 @@ interface GeneralSettingsConfig {
showPort: boolean; showPort: boolean;
showGameSelect: boolean; showGameSelect: boolean;
showSecret: boolean; showSecret: boolean;
showGrpcServiceName: boolean;
showIgnoreTls: boolean;
} }
const getGeneralSettingsConfig = ( const getGeneralSettingsConfig = (
@@ -66,8 +56,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"),
@@ -77,8 +65,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"),
@@ -88,8 +74,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"),
@@ -99,8 +83,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"),
@@ -110,19 +92,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"),
@@ -132,8 +101,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"),
@@ -143,19 +110,6 @@ const getGeneralSettingsConfig = (
showPort: false, showPort: false,
showGameSelect: false, showGameSelect: false,
showSecret: true, showSecret: true,
showGrpcServiceName: false,
showIgnoreTls: false,
},
websocket: {
urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"),
urlPlaceholder: t("pages.createMonitor.form.general.option.wsUrl.placeholder"),
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
showUrl: true,
showPort: false,
showGameSelect: false,
showSecret: false,
showGrpcServiceName: false,
showIgnoreTls: true,
}, },
}; };
return configs[type] || configs.http; return configs[type] || configs.http;
@@ -211,7 +165,6 @@ const CreateMonitorPage = () => {
const watchedType = watch("type") as MonitorType; const watchedType = watch("type") as MonitorType;
const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean; const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean;
const watchGeoCheckEnabled = watch("geoCheckEnabled") as boolean;
useEffect(() => { useEffect(() => {
clearErrors(); clearErrors();
@@ -271,7 +224,7 @@ const CreateMonitorPage = () => {
}; };
const onError = (errors: unknown) => { const onError = (errors: unknown) => {
logger.debug("Monitor creation validation errors", errors); console.log(errors);
}; };
return ( return (
@@ -298,7 +251,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"
@@ -335,20 +288,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"
)}
/>
<RadioWithDescription
value="websocket"
label={t("pages.createMonitor.form.type.optionWebSocket")}
description={t(
"pages.createMonitor.form.type.optionWebSocketDescription"
)}
/>
</RadioGroup> </RadioGroup>
</FormControl> </FormControl>
)} )}
@@ -361,7 +300,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
@@ -437,30 +376,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
@@ -580,7 +495,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}
@@ -662,7 +577,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}
@@ -714,7 +629,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}
@@ -765,9 +680,7 @@ const CreateMonitorPage = () => {
} }
/> />
{(watchedType === "http" || {watchedType === "http" && (
watchedType === "grpc" ||
watchedType === "websocket") && (
<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")}
@@ -779,7 +692,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}
@@ -800,7 +713,7 @@ const CreateMonitorPage = () => {
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}
@@ -808,7 +721,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}
@@ -823,7 +736,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}
@@ -910,140 +823,6 @@ const CreateMonitorPage = () => {
/> />
)} )}
{supportsGeoCheck(watchedType) && (
<ConfigBox
title={t("pages.createMonitor.form.geoChecks.title")}
subtitle={t("pages.createMonitor.form.geoChecks.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="geoCheckEnabled"
control={control}
render={({ field }) => (
<Stack
direction="row"
alignItems="center"
spacing={theme.spacing(SPACING.LG)}
>
<Switch
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>
<Typography>
{t("pages.createMonitor.form.geoChecks.option.enabled.label")}
</Typography>
</Stack>
)}
/>
{watchGeoCheckEnabled && (
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="geoCheckLocations"
control={control}
render={({ field }) => {
// Map continents to have 'name' property for Autocomplete
const locationOptions = GeoContinents.map((continent) => ({
id: continent,
name: t(
`pages.createMonitor.form.geoChecks.option.locations.options.${continent}`
),
}));
const selectedLocations = locationOptions.filter((loc) =>
(field.value ?? []).includes(loc.id)
);
return (
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Autocomplete
multiple
options={locationOptions}
value={selectedLocations}
getOptionLabel={(option) => option.name}
onChange={(_: unknown, newValue: typeof locationOptions) => {
field.onChange(newValue.map((loc) => loc.id));
}}
isOptionEqualToValue={(option, value) =>
option.id === value.id
}
fieldLabel={t(
"pages.createMonitor.form.geoChecks.option.locations.label"
)}
/>
{selectedLocations.length > 0 && (
<Stack
flex={1}
width="100%"
>
{selectedLocations.map((location, index) => (
<Stack
direction="row"
alignItems="center"
key={location.id}
width="100%"
>
<Typography flexGrow={1}>{location.name}</Typography>
<IconButton
size="small"
onClick={() => {
field.onChange(
(field.value ?? []).filter(
(id: string) => id !== location.id
)
);
}}
aria-label="Remove location"
>
<Trash2 size={16} />
</IconButton>
{index < selectedLocations.length - 1 && <Divider />}
</Stack>
))}
</Stack>
)}
</Stack>
);
}}
/>
<Controller
name="geoCheckInterval"
control={control}
render={({ field }) => (
<Select
{...field}
value={field.value ?? 300000}
fieldLabel={t(
"pages.createMonitor.form.geoChecks.option.interval.label"
)}
>
<MenuItem value={300000}>
{t(
"pages.createMonitor.form.geoChecks.option.interval.value.fiveMinutes"
)}
</MenuItem>
<MenuItem value={600000}>
{t(
"pages.createMonitor.form.geoChecks.option.interval.value.tenMinutes"
)}
</MenuItem>
<MenuItem value={900000}>
{t(
"pages.createMonitor.form.geoChecks.option.interval.value.fifteenMinutes"
)}
</MenuItem>
<MenuItem value={1800000}>
{t(
"pages.createMonitor.form.geoChecks.option.interval.value.thirtyMinutes"
)}
</MenuItem>
</Select>
)}
/>
</Stack>
)}
</Stack>
}
/>
)}
<Stack <Stack
direction="row" direction="row"
justifyContent="flex-end" justifyContent="flex-end"
@@ -3,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}
+4 -24
View File
@@ -1,5 +1,4 @@
import { BasePage } from "@/Components/design-elements"; import { BasePage } from "@/Components/design-elements";
import { HeaderTimeRange } from "@/Components/common";
import type { PageSpeedDetailsResponse } from "@/Types/Monitor"; import type { PageSpeedDetailsResponse } from "@/Types/Monitor";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import { import {
@@ -14,36 +13,23 @@ import { useIsAdmin } from "@/Hooks/useIsAdmin";
import { useGet } from "@/Hooks/UseApi"; import { useGet } from "@/Hooks/UseApi";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useTheme } from "@mui/material"; import { useTheme } from "@mui/material";
import { useState, useMemo } from "react";
const PageSpeedDetails = () => { const PageSpeedDetails = () => {
const { monitorId } = useParams(); const { monitorId } = useParams();
const isAdmin = useIsAdmin(); const isAdmin = useIsAdmin();
const theme = useTheme(); const theme = useTheme();
const [dateRange, setDateRange] = useState<string>("day");
const monitorDetailsUrl = useMemo(() => {
if (!monitorId) {
return null;
}
const params = new URLSearchParams();
params.append("dateRange", dateRange);
return `/monitors/pagespeed/details/${monitorId}?${params.toString()}`;
}, [monitorId, dateRange]);
const { const {
data: monitorData, data: monitorData,
isLoading, isLoading,
error, error,
refetch, refetch,
} = useGet<PageSpeedDetailsResponse>( } = useGet<PageSpeedDetailsResponse>(
monitorDetailsUrl, monitorId ? `/monitors/pagespeed/details/${monitorId}?dateRange=day` : null,
{}, {},
{ keepPreviousData: true, refreshInterval: 30000 } { keepPreviousData: true, refreshInterval: 30000 }
); );
const monitor = monitorData?.monitorData?.monitor; const monitor = monitorData?.monitor;
const groupedChecks = monitorData?.monitorData?.groupedChecks || [];
const monitorStats = monitorData?.monitorStats || null; const monitorStats = monitorData?.monitorStats || null;
return ( return (
@@ -61,15 +47,9 @@ const PageSpeedDetails = () => {
monitor={monitor} monitor={monitor}
monitorStats={monitorStats} monitorStats={monitorStats}
/> />
<HeaderTimeRange
isLoading={isLoading}
hasDateRange={true}
dateRange={dateRange}
setDateRange={setDateRange}
/>
<HistogramPageSpeedDetails <HistogramPageSpeedDetails
checks={groupedChecks} checks={monitor?.recentChecks || []}
range={dateRange} range="day"
/> />
<Stack <Stack
direction={{ xs: "column", md: "row" }} direction={{ xs: "column", md: "row" }}
+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;
+83 -181
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";
@@ -17,13 +8,12 @@ import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux"; import { useSelector, useDispatch } from "react-redux";
import DummyChart from "@/Pages/Settings/DummyChart"; import DummyChart from "@/Pages/Settings/DummyChart";
import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi"; import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi";
import { useToast } from "@/Hooks/UseToast";
import { useSettingsForm } from "@/Hooks/useSettingsForm"; import { useSettingsForm } from "@/Hooks/useSettingsForm";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import type { SettingsFormData } from "@/Validation/settings"; import type { SettingsFormData } from "@/Validation/settings";
import { useState } from "react"; import { useState } from "react";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import { TextField, Button, FieldLabel, 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";
@@ -37,7 +27,6 @@ import {
} from "@/Features/UI/uiSlice.js"; } from "@/Features/UI/uiSlice.js";
import timezones from "@/Utils/timezones.json"; import timezones from "@/Utils/timezones.json";
import type { RootState } from "@/Types/state"; import type { RootState } from "@/Types/state";
import { CHECK_TTL_SENTINEL } from "@/Types/Check";
interface Timezone { interface Timezone {
id: string; id: string;
@@ -55,13 +44,10 @@ export const SettingsPage = () => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const isAdmin = useIsAdmin(); const isAdmin = useIsAdmin();
const { toastError } = useToast();
// Local state for demo monitors dialog // Local state for demo monitors dialog
const [isDemoMonitorsDialogOpen, setIsDemoMonitorsDialogOpen] = useState(false); const [isDemoMonitorsDialogOpen, setIsDemoMonitorsDialogOpen] = useState(false);
const { post: postDemoMonitors, loading: isPostingDemoMonitors } = usePost(); const { post: postDemoMonitors, loading: isPostingDemoMonitors } = usePost();
const { deleteFn: deleteAllMonitors, loading: isDeletingAllMonitors } = useDelete(); const { deleteFn: deleteAllMonitors, loading: isDeletingAllMonitors } = useDelete();
// Import monitors functionality
const { post: importMonitors, loading: isImportingMonitors } = usePost();
// Fetch settings data from API // Fetch settings data from API
const { data: fetchedSettings } = useGet<SettingsResponse>("/settings"); const { data: fetchedSettings } = useGet<SettingsResponse>("/settings");
@@ -216,36 +202,6 @@ export const SettingsPage = () => {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}; };
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
return;
}
if (file.type !== "application/json") {
toastError("Please select a valid JSON file");
event.target.value = "";
}
try {
const text = await file.text();
const monitors = JSON.parse(text);
if (!Array.isArray(monitors)) {
toastError("Invalid file format: expected an array of monitors");
event.target.value = "";
return;
}
await importMonitors("/monitors/import/json", { monitors });
event.target.value = "";
} catch (error) {
toastError("Error parsing JSON file. Please check the file format.");
event.target.value = "";
}
};
const onSubmit = async (data: SettingsFormData) => { const onSubmit = async (data: SettingsFormData) => {
// Don't send pagespeedApiKey if it's already set and user hasn't clicked reset // Don't send pagespeedApiKey if it's already set and user hasn't clicked reset
const dataToSend = { ...data }; const dataToSend = { ...data };
@@ -267,7 +223,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,
}), }),
{} {}
); );
@@ -291,7 +248,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 || {});
@@ -301,7 +258,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")}
@@ -324,7 +281,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}
@@ -463,43 +420,6 @@ export const SettingsPage = () => {
/> />
)} )}
{/* Check Retention */}
{isAdmin && (
<ConfigBox
title={t("pages.settings.form.retention.title")}
subtitle={t("pages.settings.form.retention.description")}
rightContent={
<Controller
name="checkTTL"
control={form.control}
defaultValue={defaults.checkTTL}
render={({ field }) => (
<SliderWithLabel
{...field}
fieldLabel={t("pages.settings.form.retention.option.days.label")}
min={1}
max={CHECK_TTL_SENTINEL}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
value={field.value || 30}
onChange={(_, value) => field.onChange(value)}
valueLabelDisplay="auto"
valueLabelFormat={(value: number) =>
value >= CHECK_TTL_SENTINEL
? t("pages.settings.form.retention.option.days.unlimited")
: `${value}`
}
formatDisplayValue={(value: number) =>
value >= CHECK_TTL_SENTINEL
? t("pages.settings.form.retention.option.days.unlimited")
: `${value}`
}
/>
)}
/>
}
/>
)}
{/* Global Thresholds */} {/* Global Thresholds */}
{isAdmin && ( {isAdmin && (
<ConfigBox <ConfigBox
@@ -511,16 +431,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}
/> />
)} )}
/> />
@@ -528,16 +456,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}
/> />
)} )}
/> />
@@ -545,16 +481,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}
/> />
)} )}
/> />
@@ -562,18 +506,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}
/> />
)} )}
/> />
@@ -589,7 +541,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")}
@@ -637,7 +589,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"
@@ -724,7 +676,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"
@@ -799,7 +751,7 @@ export const SettingsPage = () => {
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: theme.spacing(LAYOUT.XS), gap: theme.spacing(4),
}} }}
> >
{[ {[
@@ -914,34 +866,17 @@ export const SettingsPage = () => {
{/* Export Monitors - Admin Only */} {/* Export Monitors - Admin Only */}
{isAdmin && ( {isAdmin && (
<ConfigBox <ConfigBox
title={t("pages.settings.form.importExportMonitors.title")} title={t("pages.settings.form.exportMonitors.title")}
subtitle={t("pages.settings.form.importExportMonitors.description")} subtitle={t("pages.settings.form.exportMonitors.description")}
rightContent={ rightContent={
<Stack <Box>
gap={theme.spacing(LAYOUT.MD)}
direction={"row"}
>
<input
id="monitor-import-input"
type="file"
accept=".json"
style={{ display: "none" }}
onChange={handleFileSelect}
/>
<Button
variant="contained"
onClick={() => document.getElementById("monitor-import-input")?.click()}
disabled={isImportingMonitors}
>
{t("common.buttons.importFromJSON")}
</Button>
<Button <Button
variant="contained" variant="contained"
onClick={handleExportMonitors} onClick={handleExportMonitors}
> >
{t("common.buttons.exportToJSON")} {t("common.buttons.exportToJSON")}
</Button> </Button>
</Stack> </Box>
} }
/> />
)} )}
@@ -1004,46 +939,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>
);
};
+4 -95
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";
@@ -20,17 +17,11 @@ import { useState, useMemo } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useGet } from "@/Hooks/UseApi"; import { useGet } from "@/Hooks/UseApi";
import { type MonitorDetailsResponse, supportsGeoCheck } 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 || !supportsGeoCheck(monitor?.type) || !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 || !supportsGeoCheck(monitor?.type) || !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", "websocket"];
// Build URL for monitors with checks // Build URL for monitors with checks
const monitorsWithChecksUrl = useMemo(() => { const monitorsWithChecksUrl = useMemo(() => {
+8 -12
View File
@@ -1,5 +1,3 @@
export const CHECK_TTL_SENTINEL = 366;
export interface CheckMetadata { export interface CheckMetadata {
monitorId: string; monitorId: string;
teamId: string; teamId: string;
@@ -127,6 +125,9 @@ export interface Check {
timings?: CheckTimings; timings?: CheckTimings;
statusCode: number; statusCode: number;
message: string; message: string;
ack: boolean;
ackAt?: string | null;
expiry: string;
cpu?: CheckCpuInfo; cpu?: CheckCpuInfo;
memory?: CheckMemoryInfo; memory?: CheckMemoryInfo;
disk?: CheckDiskInfo[]; disk?: CheckDiskInfo[];
@@ -139,6 +140,7 @@ export interface Check {
seo?: number; seo?: number;
performance?: number; performance?: number;
audits?: CheckAudits; audits?: CheckAudits;
__v: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} }
@@ -149,15 +151,6 @@ export interface GroupedCheck {
totalChecks: number; totalChecks: number;
} }
export interface PageSpeedGroupedCheck {
bucketDate: string;
performance: number;
accessibility: number;
bestPractices: number;
seo: number;
totalChecks: number;
}
export interface LatestCheck { export interface LatestCheck {
status: boolean; status: boolean;
responseTime: number; responseTime: number;
@@ -241,7 +234,10 @@ export interface ChecksSummary {
downChecks: number; downChecks: number;
} }
export type CheckSnapshot = Omit<Check, "metadata" | "__v" | "updatedAt"> & { export type CheckSnapshot = Omit<
Check,
"metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt"
> & {
originalResponseTime: number; originalResponseTime: number;
}; };
-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;
}
+1 -22
View File
@@ -1,7 +1,4 @@
import type { GroupedCheck, CheckSnapshot } from "@/Types/Check"; import type { GroupedCheck, CheckSnapshot } from "@/Types/Check";
import type { PageSpeedGroupedCheck } from "@/Types/Check";
import type { GeoContinent } from "@/Types/GeoCheck";
export type { GeoContinent } from "@/Types/GeoCheck";
export const MonitorTypes = [ export const MonitorTypes = [
"http", "http",
@@ -11,21 +8,10 @@ export const MonitorTypes = [
"docker", "docker",
"port", "port",
"game", "game",
"grpc",
"websocket",
"unknown", "unknown",
] as const; ] as const;
export type MonitorType = (typeof MonitorTypes)[number]; export type MonitorType = (typeof MonitorTypes)[number];
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
export const supportsGeoCheck = (type: MonitorType | undefined): boolean => {
if (!type) {
return false;
}
return GeoCheckSupportedTypes.includes(type);
};
export const MonitorStatuses = [ export const MonitorStatuses = [
"up", "up",
"down", "down",
@@ -71,11 +57,7 @@ export interface Monitor {
tempAlertCounter: number; tempAlertCounter: number;
selectedDisks: string[]; selectedDisks: string[];
gameId?: string; gameId?: string;
grpcServiceName?: string;
group: string | null; group: string | null;
geoCheckEnabled?: boolean;
geoCheckLocations?: GeoContinent[];
geoCheckInterval?: number;
recentChecks: CheckSnapshot[]; recentChecks: CheckSnapshot[];
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -130,10 +112,7 @@ export interface MonitorDetailsResponse {
} }
export interface PageSpeedDetailsResponse { export interface PageSpeedDetailsResponse {
monitorData: { monitor: MonitorWithChecks;
monitor: Monitor;
groupedChecks: PageSpeedGroupedCheck[];
};
monitorStats: MonitorStats | null; monitorStats: MonitorStats | null;
} }
-1
View File
@@ -5,7 +5,6 @@ export const NotificationChannels = [
"webhook", "webhook",
"pager_duty", "pager_duty",
"matrix", "matrix",
"teams",
] as const; ] as const;
export type NotificationChannel = (typeof NotificationChannels)[number]; export type NotificationChannel = (typeof NotificationChannels)[number];
-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;
}
-2
View File
@@ -8,8 +8,6 @@ export const getMonitorPath = (type: MonitorType): string => {
port: "uptime", port: "uptime",
ping: "uptime", ping: "uptime",
game: "uptime", game: "uptime",
grpc: "uptime",
websocket: "uptime",
unknown: "uptime", unknown: "uptime",
docker: "uptime", docker: "uptime",
hardware: "infrastructure", hardware: "infrastructure",
-6
View File
@@ -12,8 +12,6 @@ export const typographyLevels = {
export const colors = { export const colors = {
gray200: "#EFEFEF", gray200: "#EFEFEF",
gray700: "#313131", gray700: "#313131",
gray900: "#151518",
gray850: "#1c1c21",
blueBlueWave: "#1570EF", blueBlueWave: "#1570EF",
}; };
@@ -33,8 +31,4 @@ export const darkPalette = {
secondary: { secondary: {
main: colors.gray700, main: colors.gray700,
}, },
background: {
default: colors.gray900,
paper: colors.gray850,
},
}; };
-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,
};
-30
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" });
@@ -21,12 +20,6 @@ const baseSchema = z.object({
.number({ message: "Threshold percentage is required" }) .number({ message: "Threshold percentage is required" })
.min(1, "Incident percentage must be at least 1") .min(1, "Incident percentage must be at least 1")
.max(100, "Incident percentage must be at most 100"), .max(100, "Incident percentage must be at most 100"),
geoCheckEnabled: z.boolean().optional(),
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
geoCheckInterval: z
.number()
.min(300000, "Interval must be at least 5 minutes")
.optional(),
}); });
// HTTP monitor schema // HTTP monitor schema
@@ -73,18 +66,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"),
@@ -115,13 +96,6 @@ const hardwareSchema = baseSchema.extend({
selectedDisks: z.array(z.string()), selectedDisks: z.array(z.string()),
}); });
// WebSocket monitor schema
const websocketSchema = baseSchema.extend({
type: z.literal("websocket"),
url: z.string().min(1, "WebSocket URL is required"),
ignoreTlsErrors: z.boolean(),
});
// Discriminated union of all monitor types // Discriminated union of all monitor types
export const monitorSchema = z.discriminatedUnion("type", [ export const monitorSchema = z.discriminatedUnion("type", [
httpSchema, httpSchema,
@@ -129,10 +103,8 @@ export const monitorSchema = z.discriminatedUnion("type", [
portSchema, portSchema,
dockerSchema, dockerSchema,
gameSchema, gameSchema,
grpcSchema,
pagespeedSchema, pagespeedSchema,
hardwareSchema, hardwareSchema,
websocketSchema,
]); ]);
export type MonitorFormData = z.infer<typeof monitorSchema>; export type MonitorFormData = z.infer<typeof monitorSchema>;
@@ -144,8 +116,6 @@ export {
portSchema, portSchema,
dockerSchema, dockerSchema,
gameSchema, gameSchema,
grpcSchema,
pagespeedSchema, pagespeedSchema,
hardwareSchema, hardwareSchema,
websocketSchema,
}; };
-6
View File
@@ -45,11 +45,6 @@ const matrixSchema = baseSchema.extend({
accessToken: z.string().min(1, "Access token is required"), accessToken: z.string().min(1, "Access token is required"),
}); });
const teamsSchema = baseSchema.extend({
type: z.literal("teams"),
address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"),
});
export const notificationSchema = z.discriminatedUnion("type", [ export const notificationSchema = z.discriminatedUnion("type", [
emailSchema, emailSchema,
slackSchema, slackSchema,
@@ -57,7 +52,6 @@ export const notificationSchema = z.discriminatedUnion("type", [
webhookSchema, webhookSchema,
pagerDutySchema, pagerDutySchema,
matrixSchema, matrixSchema,
teamsSchema,
]); ]);
export type NotificationFormData = z.infer<typeof notificationSchema>; export type NotificationFormData = z.infer<typeof notificationSchema>;
+29 -12
View File
@@ -1,4 +1,3 @@
import { CHECK_TTL_SENTINEL } from "@/Types/Check";
import { z } from "zod"; import { z } from "zod";
export const settingsSchema = z.object({ export const settingsSchema = z.object({
@@ -12,11 +11,7 @@ export const settingsSchema = z.object({
systemEmailSecure: z.boolean().optional(), systemEmailSecure: z.boolean().optional(),
systemEmailPool: z.boolean().optional(), systemEmailPool: z.boolean().optional(),
showURL: z.boolean().optional(), showURL: z.boolean().optional(),
checkTTL: z checkTTL: z.number().int().min(1, "Please enter a value").max(365, "Maximum 365 days"),
.number()
.int()
.min(1, "Please enter a value")
.max(CHECK_TTL_SENTINEL, `Maximum ${CHECK_TTL_SENTINEL}`),
pagespeedApiKey: z pagespeedApiKey: z
.string() .string()
.transform((val) => (val.trim() === "" ? undefined : val.trim())) .transform((val) => (val.trim() === "" ? undefined : val.trim()))
@@ -45,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>;
+8 -77
View File
@@ -44,10 +44,7 @@
"addDemo": "Add demo monitors", "addDemo": "Add demo monitors",
"removeMonitors": "Remove monitors", "removeMonitors": "Remove monitors",
"sendTestEmail": "Send test email", "sendTestEmail": "Send test email",
"exportToJSON": "Export to JSON", "exportToJSON": "Export to JSON"
"importFromJSON": "Import from JSON",
"clearFilters": "Clear filters",
"removeUser": "Remove user"
}, },
"charts": { "charts": {
"labels": { "labels": {
@@ -117,8 +114,7 @@
"type": "Type", "type": "Type",
"url": "Url", "url": "Url",
"interval": "Interval", "interval": "Interval",
"active": "Active", "active": "Active"
"responseTime": "Response time"
} }
} }
}, },
@@ -375,8 +371,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"
} }
} }
}, },
@@ -484,14 +480,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)"
},
"wsUrl": {
"label": "WebSocket URL",
"placeholder": "wss://example.com/socket"
} }
}, },
"title": "General settings", "title": "General settings",
@@ -502,8 +490,6 @@
"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.",
"websocket": "Enter the WebSocket URL to monitor (e.g., wss://example.com/socket) 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."
} }
}, },
@@ -538,10 +524,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.",
"optionWebSocket": "WebSocket",
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
"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",
@@ -568,36 +550,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.",
@@ -627,12 +579,6 @@
"title": "Roles", "title": "Roles",
"description": "Assign roles to the user. Multiple roles can be selected." "description": "Assign roles to the user. Multiple roles can be selected."
} }
},
"dialog": {
"removeUser": {
"title": "Remove user",
"content": "Are you sure you want to remove {{name}} from your team? This action cannot be undone."
}
} }
}, },
"incidents": { "incidents": {
@@ -673,6 +619,7 @@
}, },
"filters": { "filters": {
"allMonitors": "All monitors", "allMonitors": "All monitors",
"clearFilters": "Clear filters",
"monitor": "Monitor", "monitor": "Monitor",
"resolutionType": "Resolution type", "resolutionType": "Resolution type",
"resolutionTypes": { "resolutionTypes": {
@@ -1041,7 +988,7 @@
}, },
"stats": { "stats": {
"title": "Monitor history", "title": "Monitor history",
"description": "Clear all monitoring history and statistics for your team. This action is irreversible.", "description": "Define how long you want to retain historical data. You can also clear all existing data.",
"option": { "option": {
"clear": { "clear": {
"label": "Clear all stats. This is irreversible." "label": "Clear all stats. This is irreversible."
@@ -1052,16 +999,6 @@
"description": "This cannot be undone" "description": "This cannot be undone"
} }
}, },
"retention": {
"title": "Check retention",
"description": "Set how long check data is retained before being automatically cleaned up.",
"option": {
"days": {
"label": "Retention period (days)",
"unlimited": "Unlimited"
}
}
},
"thresholds": { "thresholds": {
"title": "Global Thresholds", "title": "Global Thresholds",
"description": "Set global CPU, Memory, Disk, and Temperature thresholds for infrastructure monitoring. These apply to all hardware monitors unless overridden.", "description": "Set global CPU, Memory, Disk, and Temperature thresholds for infrastructure monitoring. These apply to all hardware monitors unless overridden.",
@@ -1149,18 +1086,12 @@
} }
}, },
"exportMonitors": { "exportMonitors": {
"title": "Export monitors" "title": "Export monitors",
}, "description": "Export your monitors data as a JSON file for backup or transfer."
"importExportMonitors": {
"title": "Import / Export monitors",
"description": "Import or export your monitors data as a JSON file for backup or transfer."
}, },
"about": { "about": {
"title": "About", "title": "About",
"developedBy": "Developed by Bluewave Labs" "developedBy": "Developed by Bluewave Labs"
},
"validation": {
"errorMessage": "Please fix the following validation errors:"
} }
} }
}, },
+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.1"; 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",
+848 -1138
View File
File diff suppressed because it is too large. Load diff
+6 -13
View File
@@ -15,7 +15,7 @@
"format-check": "prettier --check ." "format-check": "prettier --check ."
}, },
"lint-staged": { "lint-staged": {
"**/*": "prettier --write" "**/*": "npm run format"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@@ -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",
@@ -38,28 +36,27 @@
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"express-rate-limit": "8.0.1", "express-rate-limit": "8.0.1",
"gamedig": "5.2.0", "gamedig": "^5.3.1",
"got": "14.4.7", "got": "14.4.7",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"helmet": "^8.0.0", "helmet": "^8.0.0",
"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",
"mjml": "^5.0.0-alpha.4", "mjml": "^5.0.0-alpha.4",
"mongoose": "^8.3.3", "mongoose": "^8.3.3",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nodemailer": "8.0.1", "nodemailer": "^6.9.14",
"ping": "0.4.4", "ping": "0.4.4",
"sharp": "0.33.5", "sharp": "0.33.5",
"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"
"ws": "^8.19.0",
"zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.17.0",
@@ -79,9 +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",
"@types/ws": "^8.18.1",
"@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",
@@ -95,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);
} }
+2 -16
View File
@@ -74,22 +74,8 @@ export const createApp = ({
}, },
}) })
); );
// Swagger UI — dynamically set server URL from request // Swagger UI
app.use("/api-docs", swaggerUi.serve, (req: express.Request, res: express.Response, next: express.NextFunction) => { app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec));
const protocol = req.protocol;
const host = req.get("host");
const dynamicSpec = {
...openApiSpec,
servers: [
{
url: `${protocol}://${host}/api/v1`,
description: "Current Server",
},
...openApiSpec.servers,
],
};
swaggerUi.setup(dynamicSpec)(req, res, next);
});
app.use("/api/v1/health", (req, res) => { app.use("/api/v1/health", (req, res) => {
res.json({ res.json({
-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());
+67 -122
View File
@@ -1,70 +1,34 @@
import MongoDB from "../db/MongoDB.js"; import MongoDB from "../db/MongoDB.js";
import { IDb } from "@/db/IDb.js"; import NetworkService from "../service/infrastructure/networkService.js";
import EmailService from "../service/infrastructure/emailService.js";
import BufferService from "../service/infrastructure/bufferService.js";
import { import {
// Service classes
NetworkService,
EmailService,
BufferService,
GlobalPingService,
SuperSimpleQueue,
SuperSimpleQueueHelper,
NotificationsService, NotificationsService,
StatusService, StatusService,
NotificationMessageBuilder,
MonitorService,
StatusPageService,
UserService,
CheckService,
GeoChecksService,
DiagnosticService,
InviteService,
MaintenanceWindowService,
IncidentService,
// Notification providers
WebhookProvider, WebhookProvider,
SlackProvider, SlackProvider,
EmailProvider, EmailProvider,
DiscordProvider, DiscordProvider,
PagerDutyProvider, PagerDutyProvider,
MatrixProvider, MatrixProvider,
TeamsProvider,
// Interfaces
INetworkService,
IEmailService,
IBufferService,
ISuperSimpleQueue,
INotificationsService, INotificationsService,
IStatusService,
IMonitorService,
IUserService,
ICheckService,
IGeoChecksService,
IDiagnosticService,
IInviteService,
IMaintenanceWindowService,
IStatusPageService,
IIncidentService,
INotificationMessageBuilder,
ISettingsService,
EnvConfig,
} from "@/service/index.js"; } from "@/service/index.js";
import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
// Network providers import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
import { PingProvider } from "@/service/infrastructure/network/PingProvider.js"; import UserService from "../service/business/userService.js";
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js"; import CheckService from "../service/business/checkService.js";
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js"; import DiagnosticService from "../service/business/diagnosticService.js";
import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js"; import InviteService from "../service/business/inviteService.js";
import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js"; import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js"; import { MonitorService } from "@/service/index.js";
import { PortProvider } from "@/service/infrastructure/network/PortProvider.js"; import { StatusPageService, IStatusPageService } from "../service/business/statusPageService.js";
import { GameProvider } from "@/service/infrastructure/network/GameProvider.js"; import IncidentService from "../service/business/incidentService.js";
import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js"; import { NotificationMessageBuilder, INotificationMessageBuilder } from "../service/infrastructure/notificationMessageBuilder.js";
import { WebSocketProvider } from "@/service/infrastructure/network/WebSocketProvider.js";
// Third-party
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 Docker from "dockerode"; import Docker from "dockerode";
import net from "net"; import net from "net";
import fs from "fs"; import fs from "fs";
@@ -77,15 +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";
import WebSocket from "ws";
// Repositories // repositories
import { import {
MongoMonitorsRepository, MongoMonitorsRepository,
MongoChecksRepository, MongoChecksRepository,
MongoGeoChecksRepository,
MongoMonitorStatsRepository, MongoMonitorStatsRepository,
MongoStatusPagesRepository, MongoStatusPagesRepository,
MongoUsersRepository, MongoUsersRepository,
@@ -97,7 +57,6 @@ import {
MongoMaintenanceWindowsRepository, MongoMaintenanceWindowsRepository,
IMonitorsRepository, IMonitorsRepository,
IChecksRepository, IChecksRepository,
IGeoChecksRepository,
IMonitorStatsRepository, IMonitorStatsRepository,
IStatusPagesRepository, IStatusPagesRepository,
IUsersRepository, IUsersRepository,
@@ -110,24 +69,24 @@ import {
IMaintenanceWindowsRepository, IMaintenanceWindowsRepository,
} from "@/repositories/index.js"; } from "@/repositories/index.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
import { EnvConfig } from "@/service/system/settingsService.js";
export type InitializedServices = { export type InitializedServices = {
settingsService: ISettingsService; settingsService: any;
db: IDb; db: any;
networkService: INetworkService; networkService: any;
emailService: IEmailService; emailService: any;
bufferService: IBufferService; bufferService: any;
statusService: IStatusService; statusService: any;
jobQueue: ISuperSimpleQueue; jobQueue: any;
userService: IUserService; userService: any;
checkService: ICheckService; checkService: any;
geoChecksService: IGeoChecksService; diagnosticService: any;
diagnosticService: IDiagnosticService; inviteService: any;
inviteService: IInviteService; maintenanceWindowService: any;
maintenanceWindowService: IMaintenanceWindowService; monitorService: any;
monitorService: IMonitorService; incidentService: any;
incidentService: IIncidentService; logger: any;
logger: ILogger;
notificationsService: INotificationsService; notificationsService: INotificationsService;
statusPageService: IStatusPageService; statusPageService: IStatusPageService;
notificationMessageBuilder: INotificationMessageBuilder; notificationMessageBuilder: INotificationMessageBuilder;
@@ -135,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;
@@ -156,7 +114,7 @@ export const initializeServices = async ({
}: { }: {
logger: ILogger; logger: ILogger;
envSettings: EnvConfig; envSettings: EnvConfig;
settingsService: ISettingsService; settingsService: any;
settingsRepository: ISettingsRepository; settingsRepository: ISettingsRepository;
}): Promise<InitializedServices> => { }): Promise<InitializedServices> => {
// Create DB // Create DB
@@ -168,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();
@@ -179,57 +136,47 @@ export const initializeServices = async ({
const teamsRepository = new MongoTeamsRepository(); const teamsRepository = new MongoTeamsRepository();
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository(); const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
// Network providers const networkService = new NetworkService({
const pingProvider = new PingProvider(ping); axios,
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath)); got,
const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger); https,
const hardwareProvider = new HardwareProvider(httpProvider); jmespath,
const dockerProvider = new DockerProvider(logger, Docker); GameDig,
const portProvider = new PortProvider(net); ping,
const gameProvider = new GameProvider(logger, GameDig); logger,
const grpcProvider = new GrpcProvider(grpc, protoLoader); http,
const webSocketProvider = new WebSocketProvider(WebSocket); Docker,
net,
const networkService = new NetworkService(axios, logger, [ settingsService,
pingProvider, });
httpProvider,
pageSpeedProvider,
hardwareProvider,
dockerProvider,
portProvider,
gameProvider,
grpcProvider,
webSocketProvider,
]);
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);
// Notification providers
const webhookProvider = new WebhookProvider(logger); const webhookProvider = new WebhookProvider(logger);
const slackProvider = new SlackProvider(logger); const slackProvider = new SlackProvider(logger);
const emailProvider = new EmailProvider(emailService, logger); const emailProvider = new EmailProvider(emailService, logger);
const discordProvider = new DiscordProvider(logger); const discordProvider = new DiscordProvider(logger);
const pagerDutyProvider = new PagerDutyProvider(logger); const pagerDutyProvider = new PagerDutyProvider(logger);
const matrixProvider = new MatrixProvider(logger); const matrixProvider = new MatrixProvider(logger);
const teamsProvider = new TeamsProvider(logger);
const notificationsService = new NotificationsService( const notificationsService = new NotificationsService(
notificationsRepository, notificationsRepository,
@@ -240,20 +187,18 @@ export const initializeServices = async ({
discordProvider, discordProvider,
pagerDutyProvider, pagerDutyProvider,
matrixProvider, matrixProvider,
teamsProvider,
settingsService, settingsService,
logger, logger,
notificationMessageBuilder notificationMessageBuilder
); );
const superSimpleQueueHelper = new SuperSimpleQueueHelper( const superSimpleQueueHelper = new SuperSimpleQueueHelper({
logger, logger,
networkService, networkService,
statusService, statusService,
notificationsService, notificationsService,
checkService, checkService,
settingsService, buffer: bufferService,
bufferService,
incidentService, incidentService,
maintenanceWindowsRepository, maintenanceWindowsRepository,
monitorsRepository, monitorsRepository,
@@ -261,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({
@@ -300,7 +247,6 @@ export const initializeServices = async ({
games, games,
monitorsRepository, monitorsRepository,
checksRepository, checksRepository,
geoChecksRepository,
monitorStatsRepository, monitorStatsRepository,
statusPagesRepository, statusPagesRepository,
incidentsRepository, incidentsRepository,
@@ -309,6 +255,7 @@ export const initializeServices = async ({
const statusPageService = new StatusPageService(statusPagesRepository); const statusPageService = new StatusPageService(statusPagesRepository);
const services = { const services = {
//v1
settingsService, settingsService,
db, db,
networkService, networkService,
@@ -318,7 +265,6 @@ export const initializeServices = async ({
jobQueue: superSimpleQueue, jobQueue: superSimpleQueue,
userService, userService,
checkService, checkService,
geoChecksService,
diagnosticService, diagnosticService,
inviteService, inviteService,
maintenanceWindowService, maintenanceWindowService,
@@ -332,7 +278,6 @@ export const initializeServices = async ({
// Repositories // Repositories
monitorsRepository, monitorsRepository,
checksRepository, checksRepository,
geoChecksRepository,
monitorStatsRepository, monitorStatsRepository,
statusPagesRepository, statusPagesRepository,
usersRepository, usersRepository,
+53 -67
View File
@@ -1,34 +1,31 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { requireTeamId, requireUserEmail, requireUserId, requireUserRoles } from "@/controllers/controllerUtils.js"; import { requireTeamId, requireUserRoles } from "./controllerUtils.js";
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";
import { IUserService } from "@/service/index.js";
const SERVICE_NAME = "authController"; const SERVICE_NAME = "authController";
class AuthController { class AuthController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private userService: IUserService; private userService: any;
constructor(userService: IUserService) { constructor(userService: any) {
this.userService = userService; this.userService = userService;
} }
@@ -41,12 +38,11 @@ class AuthController {
const newUser = req.body.user; const newUser = req.body.user;
const newUserToken = req.body.token; const newUserToken = req.body.token;
if (newUser?.email) { if (newUser?.email) {
const newUserEmail = requireUserEmail(newUser.email); newUser.email = newUser.email.toLowerCase();
newUser.email = newUserEmail.toLowerCase();
} }
const validatedBody = registrationBodyValidation.parse(newUser); await registrationBodyValidation.validateAsync(newUser);
const { user, token } = await this.userService.registerUser(validatedBody, newUserToken, req?.file ?? null); const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file);
res.status(200).json({ res.status(200).json({
success: true, success: true,
msg: "User registered successfully", msg: "User registered successfully",
@@ -63,11 +59,11 @@ class AuthController {
if (userData?.email) { if (userData?.email) {
userData.email = userData.email.toLowerCase(); userData.email = userData.email.toLowerCase();
} }
const validatedBody = 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); const actorRoles = requireUserRoles(req.user?.role) as UserRole[];
const newUser = await this.userService.createUser(validatedBody, teamId, actorRoles, req?.file ?? null); const newUser = await this.userService.createUser(userData, teamId, actorRoles, req.file);
res.status(201).json({ res.status(201).json({
success: true, success: true,
msg: "User created successfully", msg: "User created successfully",
@@ -83,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({
@@ -101,10 +97,8 @@ class AuthController {
editUser = async (req: Request, res: Response, next: NextFunction) => { editUser = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = editUserBodyValidation.parse(req.body); await editUserBodyValidation.validateAsync(req.body);
const userId = requireUserId(req.user?.id); const updatedUser = await this.userService.editUser(req.body, req.file, req.user);
const userEmail = requireUserEmail(req.user?.email);
const updatedUser = await this.userService.editUser(validatedBody, req?.file ?? null, userId, userEmail);
res.status(200).json({ res.status(200).json({
success: true, success: true,
@@ -131,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({
@@ -146,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,
@@ -159,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,
@@ -173,15 +167,7 @@ class AuthController {
deleteUser = async (req: Request, res: Response, next: NextFunction) => { deleteUser = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const userId = requireUserId(req.user?.id); await this.userService.deleteUser(req.user);
const teamId = requireTeamId(req.user?.teamId);
const roles = requireUserRoles(req.user?.role);
await this.userService.deleteUser({
userId,
teamId,
roles,
});
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "User deleted successfully", msg: "User deleted successfully",
@@ -191,23 +177,6 @@ class AuthController {
} }
}; };
deleteUserById = async (req: Request, res: Response, next: NextFunction) => {
try {
const validatedParams = getUserByIdParamValidation.parse(req.params);
const targetUserId = validatedParams.userId;
const actorId = requireUserId(req.user?.id);
const actorTeamId = requireTeamId(req.user?.teamId);
const actorRoles = requireUserRoles(req.user?.role);
await this.userService.deleteUserById({ actorId, actorTeamId, actorRoles, targetUserId });
return res.status(200).json({
success: true,
msg: "User removed successfully",
});
} catch (error) {
next(error);
}
};
getAllUsers = async (req: Request, res: Response, next: NextFunction) => { getAllUsers = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const allUsers = await this.userService.getAllUsers(); const allUsers = await this.userService.getAllUsers();
@@ -223,9 +192,19 @@ class AuthController {
getUserById = async (req: Request, res: Response, next: NextFunction) => { getUserById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getUserByIdParamValidation.parse(req.params); await getUserByIdParamValidation.validateAsync(req.params);
const actorRoles = requireUserRoles(req.user?.role); const userId = req?.params?.userId;
const user = await this.userService.getUserById(actorRoles, validatedParams.userId); const roles = req?.user?.role;
if (!userId) {
throw new Error("No user ID in request");
}
if (!roles || roles.length === 0) {
throw new Error("No roles in request");
}
const user = await this.userService.getUserById(roles, userId);
return res.status(200).json({ success: true, msg: "ok", data: user }); return res.status(200).json({ success: true, msg: "ok", data: user });
} catch (error) { } catch (error) {
@@ -235,19 +214,24 @@ class AuthController {
editUserById = async (req: Request, res: Response, next: NextFunction) => { editUserById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const actorRoles = requireUserRoles(req.user?.role); const roles = req?.user?.role;
const actorId = requireUserId(req.user?.id);
if (!actorRoles.includes("superadmin")) { if (!roles || !roles.includes("superadmin")) {
throw new AppError({ message: "Unauthorized", status: 403 }); throw new AppError({ message: "Unauthorized", status: 403 });
} }
const validatedParams = editUserByIdParamValidation.parse(req.params); const userId = req.params.userId as string;
// If this is superadmin self edit, allow "superadmin" role const user = { ...req.body };
const validatedBody =
validatedParams.userId === actorId ? editSuperadminUserByIdBodyValidation.parse(req.body) : editUserByIdBodyValidation.parse(req.body);
await this.userService.editUserById(validatedParams.userId, validatedBody); await editUserByIdParamValidation.validateAsync(req.params);
// If this is superadmin self edit, allow "superadmin" role
if (userId === req.user?.id) {
await editSuperadminUserByIdBodyValidation.validateAsync(req.body);
} else {
await editUserByIdBodyValidation.validateAsync(req.body);
}
await this.userService.editUserById(userId, user);
return res.status(200).json({ success: true, msg: "ok" }); return res.status(200).json({ success: true, msg: "ok" });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -256,14 +240,16 @@ class AuthController {
editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => { editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const actorRoles = requireUserRoles(req.user?.role); const roles = req?.user?.role;
if (!actorRoles.includes("superadmin")) { if (!roles || !roles.includes("superadmin")) {
throw new AppError({ message: "Unauthorized", status: 403 }); throw new AppError({ message: "Unauthorized", status: 403 });
} }
const validatedParams = editUserByIdParamValidation.parse(req.params); const userId = req.params.userId as string;
const validatedBody = editUserPasswordByIdBodyValidation.parse(req.body); await editUserByIdParamValidation.validateAsync(req.params);
await this.userService.setPasswordByUserId(validatedParams.userId, validatedBody.password); await editUserPasswordByIdBodyValidation.validateAsync(req.body);
const updatedPassword = req.body.password;
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" });
} catch (error) { } catch (error) {
next(error); next(error);
+41 -38
View File
@@ -4,19 +4,21 @@ import {
getChecksParamValidation, getChecksParamValidation,
getChecksQueryValidation, getChecksQueryValidation,
getTeamChecksQueryValidation, getTeamChecksQueryValidation,
getChecksSummaryByTeamIdQueryValidation,
deleteChecksParamValidation, deleteChecksParamValidation,
} from "@/validation/checkValidation.js"; deleteChecksByTeamIdParamValidation,
import { ICheckService } from "@/service/index.js"; updateChecksTTLBodyValidation,
import { requireTeamId } from "@/controllers/controllerUtils.js"; ackCheckBodyValidation,
ackAllChecksParamValidation,
ackAllChecksBodyValidation,
} from "@/validation/joi.js";
const SERVICE_NAME = "checkController"; const SERVICE_NAME = "checkController";
class CheckController { class CheckController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private checkService: ICheckService; private checkService: any;
constructor(checkService: ICheckService) { constructor(checkService: any) {
this.checkService = checkService; this.checkService = checkService;
} }
@@ -26,20 +28,13 @@ class CheckController {
getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getChecksParamValidation.parse(req.params); await getChecksParamValidation.validateAsync(req.params);
const validatedQuery = getChecksQueryValidation.parse(req.query); await getChecksQueryValidation.validateAsync(req.query);
const teamId = requireTeamId(req.user?.teamId);
const result = await this.checkService.getChecksByMonitor({ const result = await this.checkService.getChecksByMonitor({
monitorId: validatedParams.monitorId, monitorId: req?.params?.monitorId,
teamId, query: req?.query,
sortOrder: validatedQuery.sortOrder, teamId: req?.user?.teamId,
dateRange: validatedQuery.dateRange,
filter: validatedQuery.filter,
page: validatedQuery.page,
rowsPerPage: validatedQuery.rowsPerPage,
status: validatedQuery.status,
}); });
return res.status(200).json({ return res.status(200).json({
@@ -54,16 +49,10 @@ class CheckController {
getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => { getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getTeamChecksQueryValidation.parse(req.query); await getTeamChecksQueryValidation.validateAsync(req.query);
const teamId = requireTeamId(req.user?.teamId);
const checkData = await this.checkService.getChecksByTeam({ const checkData = await this.checkService.getChecksByTeam({
teamId, teamId: req?.user?.teamId,
sortOrder: validatedQuery.sortOrder, query: req?.query,
dateRange: validatedQuery.dateRange,
page: validatedQuery.page,
rowsPerPage: validatedQuery.rowsPerPage,
filter: validatedQuery.filter,
}); });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -77,11 +66,8 @@ class CheckController {
getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => { getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getChecksSummaryByTeamIdQueryValidation.parse(req.query); const dateRange = req.query?.dateRange || "hour";
const teamId = requireTeamId(req.user?.teamId); const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId, dateRange });
const dateRange = validatedQuery.dateRange ?? "hour";
const summary = await this.checkService.getChecksSummaryByTeamId({ teamId, dateRange });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Checks summary retrieved successfully", msg: "Checks summary retrieved successfully",
@@ -94,12 +80,11 @@ class CheckController {
deleteChecks = async (req: Request, res: Response, next: NextFunction) => { deleteChecks = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = deleteChecksParamValidation.parse(req.params); await deleteChecksParamValidation.validateAsync(req.params);
const teamId = requireTeamId(req.user?.teamId);
const deletedCount = await this.checkService.deleteChecks({ const deletedCount = await this.checkService.deleteChecks({
monitorId: validatedParams.monitorId, monitorId: req.params.monitorId as string,
teamId, teamId: req?.user?.teamId,
}); });
return res.status(200).json({ return res.status(200).json({
@@ -114,9 +99,9 @@ class CheckController {
deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); await deleteChecksByTeamIdParamValidation.validateAsync(req.params);
const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId }); const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -127,6 +112,24 @@ class CheckController {
next(error); next(error);
} }
}; };
updateChecksTTL = async (req: Request, res: Response, next: NextFunction) => {
try {
await updateChecksTTLBodyValidation.validateAsync(req.body);
await this.checkService.updateChecksTTL({
teamId: req?.user?.teamId,
ttl: req?.body?.ttl,
});
return res.status(200).json({
success: true,
msg: "Checks TTL updated successfully",
});
} catch (error) {
next(error);
}
};
} }
export default CheckController; export default CheckController;
+27 -22
View File
@@ -1,25 +1,24 @@
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { Monitor, type MonitorType, MonitorTypes, UserRole } from "@/types/index.js"; import { type MonitorType, MonitorTypes } from "@/types/index.js";
import sslChecker, { SSLDetails } from "ssl-checker";
type SSLCheckerType = typeof sslChecker;
export const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor): Promise<SSLDetails> => { const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise<any> => {
const monitorUrl = new URL(monitor.url); const monitorUrl = new URL(monitor.url);
const hostname = monitorUrl.hostname; const hostname = monitorUrl.hostname;
const cert = await checker(hostname); const cert = await sslChecker(hostname);
// Throw an error if no cert or if cert.validTo is not present
if (cert?.validTo === null || cert?.validTo === undefined) { if (cert?.validTo === null || cert?.validTo === undefined) {
throw new Error("Certificate not found"); throw new Error("Certificate not found");
} }
return cert; return cert;
}; };
export const requireString = (value: unknown, fieldName: string): string => { const requireString = (value: unknown, fieldName: string): string => {
if (typeof value === "string" && value.trim().length > 0) { if (typeof value === "string" && value.trim().length > 0) {
return value; return value;
} }
throw new AppError({ message: `${fieldName} is required`, status: 400 }); throw new AppError({ message: `${fieldName} is required`, status: 400 });
}; };
export const optionalString = (value: unknown, fieldName: string): string | undefined => { const optionalString = (value: unknown, fieldName: string): string | undefined => {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} }
@@ -29,7 +28,7 @@ export const optionalString = (value: unknown, fieldName: string): string | unde
throw new AppError({ message: `${fieldName} must be a string`, status: 400 }); throw new AppError({ message: `${fieldName} must be a string`, status: 400 });
}; };
export const optionalNumber = (value: unknown, fieldName: string): number | undefined => { const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} }
@@ -45,7 +44,7 @@ export const optionalNumber = (value: unknown, fieldName: string): number | unde
throw new AppError({ message: `${fieldName} must be a number`, status: 400 }); throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
}; };
export const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => { const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} }
@@ -63,7 +62,7 @@ export const optionalBoolean = (value: unknown, fieldName: string): boolean | un
throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 }); throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
}; };
export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => { const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
const parseSingle = (input: unknown): MonitorType => { const parseSingle = (input: unknown): MonitorType => {
if (typeof input !== "string") { if (typeof input !== "string") {
throw new AppError({ message: "Monitor type must be a string", status: 400 }); throw new AppError({ message: "Monitor type must be a string", status: 400 });
@@ -83,7 +82,7 @@ export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorTyp
return parseSingle(value); return parseSingle(value);
}; };
export const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => { const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
} }
@@ -93,36 +92,42 @@ export const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 }); throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 });
}; };
export const requireTeamId = (teamId?: string): string => { const requireTeamId = (teamId?: string): string => {
if (!teamId) { if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 }); throw new AppError({ message: "Team ID is required", status: 400 });
} }
return teamId; return teamId;
}; };
export const requireUserId = (userId?: string): string => { const requireUserId = (userId?: string): string => {
if (!userId) { if (!userId) {
throw new AppError({ message: "User ID is required", status: 400 }); throw new AppError({ message: "User ID is required", status: 400 });
} }
return userId; return userId;
}; };
export const requireUserEmail = (userEmail?: string): string => { const requireUserEmail = (userEmail?: string): string => {
if (!userEmail) { if (!userEmail) {
throw new AppError({ message: "User email is required", status: 400 }); throw new AppError({ message: "User email is required", status: 400 });
} }
return userEmail; return userEmail;
}; };
export const requireFirstName = (firstName?: string): string => { export const requireUserRoles = (userRoles?: string[]): string[] => {
if (!firstName) {
throw new AppError({ message: "First name is required", status: 400 });
}
return firstName;
};
export const requireUserRoles = (userRoles?: UserRole[]): UserRole[] => {
if (!userRoles || userRoles.length === 0) { if (!userRoles || userRoles.length === 0) {
throw new AppError({ message: "User roles are required", status: 400 }); throw new AppError({ message: "User roles are required", status: 400 });
} }
return userRoles; return userRoles;
}; };
export {
fetchMonitorCertificate,
requireString,
optionalString,
optionalNumber,
optionalBoolean,
parseMonitorTypeFilter,
parseSortOrder,
requireTeamId,
requireUserId,
requireUserEmail,
};
@@ -1,13 +1,12 @@
import { IDiagnosticService } from "@/service/index.js";
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
const SERVICE_NAME = "diagnosticController"; const SERVICE_NAME = "diagnosticController";
class DiagnosticController { class DiagnosticController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private diagnosticService: IDiagnosticService; private diagnosticService: any;
constructor(diagnosticService: IDiagnosticService) { constructor(diagnosticService: any) {
this.diagnosticService = diagnosticService; this.diagnosticService = diagnosticService;
} }
@@ -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;
+21 -15
View File
@@ -1,30 +1,35 @@
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { requireTeamId, requireUserId, requireUserEmail } from "./controllerUtils.js"; import { requireTeamId, requireUserId, requireUserEmail } from "./controllerUtils.js";
import { IIncidentService } from "@/service/index.js";
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
const SERVICE_NAME = "IncidentController"; const SERVICE_NAME = "incidentController";
class IncidentController { class IncidentController {
private incidentService: IIncidentService; static SERVICE_NAME = SERVICE_NAME;
constructor(incidentService: IIncidentService) {
private incidentService: any;
constructor(incidentService: any) {
this.incidentService = incidentService; this.incidentService = incidentService;
} }
get serviceName() {
return IncidentController.SERVICE_NAME;
}
getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => { getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getIncidentsByTeamQueryValidation.parse(req.query); const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = req.query || {};
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const result = await this.incidentService.getIncidentsByTeam( const result = await this.incidentService.getIncidentsByTeam(
teamId, teamId,
validatedQuery.sortOrder, sortOrder,
validatedQuery.dateRange, dateRange,
validatedQuery.page, page,
validatedQuery.rowsPerPage, rowsPerPage,
validatedQuery.status, status,
validatedQuery.monitorId, monitorId,
validatedQuery.resolutionType resolutionType
); );
return res.status(200).json({ return res.status(200).json({
@@ -40,9 +45,10 @@ class IncidentController {
getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => { getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const validatedQuery = getIncidentSummaryQueryValidation.parse(req.query);
const summary = await this.incidentService.getIncidentSummary(teamId, validatedQuery.limit); const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10;
const summary = await this.incidentService.getIncidentSummary(teamId, limit);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Incident summary retrieved successfully", msg: "Incident summary retrieved successfully",
+10 -12
View File
@@ -1,13 +1,12 @@
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 { requireFirstName, requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js"; import { requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
import { IInviteService } from "@/service/index.js";
const SERVICE_NAME = "inviteController"; const SERVICE_NAME = "inviteController";
class InviteController { class InviteController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private inviteService: IInviteService; private inviteService: any;
constructor(inviteService: IInviteService) { constructor(inviteService: any) {
this.inviteService = inviteService; this.inviteService = inviteService;
} }
@@ -21,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,
@@ -35,17 +34,16 @@ class InviteController {
sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => { sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const userRoles = requireUserRoles(req.user?.role); const userRoles = requireUserRoles(req?.user?.role);
const firstName = requireFirstName(req.user?.firstName);
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,
firstName, firstName: req?.user?.firstName,
userRoles, userRoles,
}); });
return res.status(200).json({ return res.status(200).json({
@@ -60,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,
+3 -4
View File
@@ -1,12 +1,11 @@
import { ILogger } from "@/utils/logger.js";
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
const SERVICE_NAME = "LogController"; const SERVICE_NAME = "LogController";
class LogController { class LogController {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger; private logger: any;
constructor(logger: ILogger) { constructor(logger: any) {
this.logger = logger; this.logger = logger;
} }
get serviceName() { get serviceName() {
@@ -15,7 +14,7 @@ class LogController {
getLogs = async (req: Request, res: Response, next: NextFunction) => { getLogs = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const logs = this.logger.getLogs(); const logs = await this.logger.getLogs();
res.status(200).json({ res.status(200).json({
success: true, success: true,
msg: "Logs fetched successfully", msg: "Logs fetched successfully",
@@ -7,21 +7,28 @@ 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";
import { IMaintenanceWindowService } from "@/service/index.js";
const SERVICE_NAME = "maintenanceWindowController";
class MaintenanceWindowController { class MaintenanceWindowController {
private maintenanceWindowService: IMaintenanceWindowService; static SERVICE_NAME = SERVICE_NAME;
constructor(maintenanceWindowService: IMaintenanceWindowService) { private maintenanceWindowService: any;
constructor(maintenanceWindowService: any) {
this.maintenanceWindowService = maintenanceWindowService; this.maintenanceWindowService = maintenanceWindowService;
} }
get serviceName() {
return MaintenanceWindowController.SERVICE_NAME;
}
createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => { createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = 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: validatedBody });
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -33,11 +40,11 @@ class MaintenanceWindowController {
}; };
getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => { getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMaintenanceWindowByIdParamValidation.parse(req.params); await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: validatedParams.id, teamId }); const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id as string, teamId });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -51,18 +58,11 @@ class MaintenanceWindowController {
getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => { getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query); await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query);
const teamId = requireTeamId(req?.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query });
teamId,
active: validatedQuery.active,
page: validatedQuery.page,
rowsPerPage: validatedQuery.rowsPerPage,
field: validatedQuery.field,
order: validatedQuery.order,
});
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -76,12 +76,12 @@ class MaintenanceWindowController {
getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => { getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params); await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params);
const teamId = requireTeamId(req?.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({
monitorId: validatedParams.monitorId, monitorId: req.params.monitorId as string,
teamId, teamId,
}); });
@@ -96,11 +96,11 @@ class MaintenanceWindowController {
}; };
deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = deleteMaintenanceWindowByIdParamValidation.parse(req.params); await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params);
const teamId = requireTeamId(req?.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: validatedParams.id, teamId }); await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id as string, teamId });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -113,14 +113,14 @@ class MaintenanceWindowController {
editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = editMaintenanceWindowByIdParamValidation.parse(req.params); await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
const validatedBody = editMaintenanceByIdWindowBodyValidation.parse(req.body); await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({
id: validatedParams.id, id: req.params.id as string,
body: validatedBody, body: req.body,
teamId, teamId,
}); });
return res.status(200).json({ return res.status(200).json({
+108 -100
View File
@@ -3,6 +3,7 @@ import { Request, Response, NextFunction } from "express";
import { import {
getMonitorByIdParamValidation, getMonitorByIdParamValidation,
getMonitorByIdQueryValidation, getMonitorByIdQueryValidation,
getMonitorsByTeamIdParamValidation,
getMonitorsByTeamIdQueryValidation, getMonitorsByTeamIdQueryValidation,
getMonitorsWithChecksQueryValidation, getMonitorsWithChecksQueryValidation,
createMonitorBodyValidation, createMonitorBodyValidation,
@@ -11,12 +12,18 @@ import {
getCertificateParamValidation, getCertificateParamValidation,
getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdParamValidation,
getHardwareDetailsByIdQueryValidation, getHardwareDetailsByIdQueryValidation,
getUptimeDetailsByIdParamValidation, } from "@/validation/joi.js";
getUptimeDetailsByIdQueryValidation,
importMonitorsBodyValidation,
} from "@/validation/monitorValidation.js";
import sslChecker from "ssl-checker"; import sslChecker from "ssl-checker";
import { fetchMonitorCertificate, requireTeamId, requireUserId } from "./controllerUtils.js"; import {
fetchMonitorCertificate,
requireString,
optionalString,
optionalNumber,
optionalBoolean,
parseMonitorTypeFilter,
parseSortOrder,
requireTeamId,
} 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";
@@ -36,9 +43,10 @@ class MonitorController {
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getCertificateParamValidation.parse(req.params); await getCertificateParamValidation.validateAsync(req.params);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId }); const monitorId = requireString(req.params?.monitorId, "Monitor ID");
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
const certificate = await fetchMonitorCertificate(sslChecker, monitor); const certificate = await fetchMonitorCertificate(sslChecker, monitor);
return res.status(200).json({ return res.status(200).json({
@@ -55,14 +63,10 @@ class MonitorController {
getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => { getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getUptimeDetailsByIdParamValidation.parse(req.params); const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const validatedQuery = getUptimeDetailsByIdQueryValidation.parse(req.query); const dateRange = requireString(req?.query?.dateRange, "dateRange");
const normalize = optionalBoolean(req?.query?.normalize, "normalize");
const monitorId = validatedParams.monitorId; const teamId = requireTeamId(req?.user?.teamId);
const dateRange = validatedQuery.dateRange;
const normalize = validatedQuery.normalize;
const teamId = requireTeamId(req.user?.teamId);
const data = await this.monitorService.getUptimeDetailsById({ const data = await this.monitorService.getUptimeDetailsById({
teamId, teamId,
@@ -82,12 +86,12 @@ class MonitorController {
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => { getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const dateRange = validatedQuery.dateRange || "recent"; const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent";
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const data = await this.monitorService.getHardwareDetailsById({ const data = await this.monitorService.getHardwareDetailsById({
teamId, teamId,
@@ -106,12 +110,12 @@ class MonitorController {
}; };
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => { getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const dateRange = validatedQuery.dateRange || "recent"; const dateRange = requireString(req?.query?.dateRange, "dateRange");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const data = await this.monitorService.getPageSpeedDetailsById({ const data = await this.monitorService.getPageSpeedDetailsById({
teamId, teamId,
@@ -129,40 +133,13 @@ class MonitorController {
} }
}; };
getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
try {
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
const validatedQuery = getMonitorByIdQueryValidation.parse(req.query);
const monitorId = validatedParams.monitorId;
const dateRange = validatedQuery.dateRange || "recent";
const continentParam = validatedQuery.continent;
const continents = continentParam ? (Array.isArray(continentParam) ? continentParam : [continentParam]) : 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 {
const validatedParams = getMonitorByIdParamValidation.parse(req.params); await getMonitorByIdParamValidation.validateAsync(req.params);
await getMonitorByIdQueryValidation.validateAsync(req.query);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
@@ -178,12 +155,12 @@ class MonitorController {
createMonitor = async (req: Request, res: Response, next: NextFunction) => { createMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = createMonitorBodyValidation.parse(req.body); await createMonitorBodyValidation.validateAsync(req.body);
const userId = requireUserId(req.user?.id); const userId = requireString(req?.user?.id, "User ID");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.createMonitor(teamId, userId, validatedBody); const monitor = await this.monitorService.createMonitor(teamId, userId, req.body);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -195,19 +172,22 @@ class MonitorController {
} }
}; };
importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => { createBulkMonitors = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const userId = req.user?.id;
const userId = requireUserId(req.user?.id); if (!userId) {
const validatedBody = importMonitorsBodyValidation.parse(req.body); throw new AppError({ message: "User ID is required", status: 400 });
const monitors = validatedBody.monitors; }
const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors }); const teamId = requireTeamId(req?.user?.teamId);
const data = req.body;
const monitors = await this.monitorService.createBulkMonitors(data, userId, teamId);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: `Successfully imported ${result.imported} monitor(s)`, msg: "Bulk monitors created successfully",
data: result, data: monitors,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -216,9 +196,9 @@ class MonitorController {
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => { deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMonitorByIdParamValidation.parse(req.params); await getMonitorByIdParamValidation.validateAsync(req.params);
const teamId = requireTeamId(req.user?.teamId); const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const monitorId = validatedParams.monitorId; const teamId = requireTeamId(req?.user?.teamId);
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
@@ -234,7 +214,8 @@ class MonitorController {
deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => { deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); const deletedCount = await this.monitorService.deleteAllMonitors({ teamId });
return res.status(200).json({ return res.status(200).json({
@@ -248,12 +229,12 @@ class MonitorController {
editMonitor = async (req: Request, res: Response, next: NextFunction) => { editMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = getMonitorByIdParamValidation.parse(req.params); await getMonitorByIdParamValidation.validateAsync(req.params);
const validatedBody = editMonitorBodyValidation.parse(req.body); await editMonitorBodyValidation.validateAsync(req.body);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: validatedBody }); const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body });
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -267,10 +248,10 @@ class MonitorController {
pauseMonitor = async (req: Request, res: Response, next: NextFunction) => { pauseMonitor = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedParams = pauseMonitorParamValidation.parse(req.params); await pauseMonitorParamValidation.validateAsync(req.params);
const monitorId = validatedParams.monitorId; const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
@@ -286,8 +267,8 @@ class MonitorController {
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => { addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const id = requireUserId(req.user?.id); const id = requireString(req?.user?.id, "User ID");
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId }); const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId });
return res.status(200).json({ return res.status(200).json({
@@ -300,13 +281,32 @@ class MonitorController {
} }
}; };
sendTestEmail = async (req: Request, res: Response, next: NextFunction) => {
try {
const { to } = req.body;
if (!to || typeof to !== "string") {
throw new AppError({ message: "Invalid 'to' email address", status: 400 });
}
const messageId = await this.monitorService.sendTestEmail({ to });
return res.status(200).json({
success: true,
msg: "Test email sent successfully",
data: { messageId },
});
} catch (error) {
next(error);
}
};
getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => { getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query); await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const type = validatedQuery.type; const type = parseMonitorTypeFilter(req.query?.type);
const filter = validatedQuery.filter; const filter = optionalString(req.query?.filter, "filter");
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter }); const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter });
@@ -322,17 +322,17 @@ class MonitorController {
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query); await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsWithChecksQueryValidation.validateAsync(req.query);
const explain = validatedQuery.explain; const explain = optionalBoolean(req?.query?.explain, "explain");
const limit = validatedQuery.limit; const limit = optionalNumber(req?.query?.limit, "limit");
const page = validatedQuery.page; const page = optionalNumber(req?.query?.page, "page");
const rowsPerPage = validatedQuery.rowsPerPage; const rowsPerPage = optionalNumber(req?.query?.rowsPerPage, "rowsPerPage");
const filter = validatedQuery.filter; const filter = optionalString(req?.query?.filter, "filter");
const field = validatedQuery.field; const field = optionalString(req?.query?.field, "field");
const order = validatedQuery.order; const order = parseSortOrder(req?.query?.order);
const type = validatedQuery.type; const type = parseMonitorTypeFilter(req?.query?.type);
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req?.user?.teamId);
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
teamId, teamId,
@@ -357,7 +357,11 @@ class MonitorController {
exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => { exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const json = await this.monitorService.exportMonitorsToJSON({ teamId }); const json = await this.monitorService.exportMonitorsToJSON({ teamId });
return res.status(200).json({ return res.status(200).json({
@@ -385,7 +389,11 @@ class MonitorController {
getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => { getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const groups = await this.monitorService.getGroupsByTeamId({ teamId }); const groups = await this.monitorService.getGroupsByTeamId({ teamId });
return res.status(200).json({ return res.status(200).json({
@@ -1,21 +1,16 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { import { Notification } from "@/types/index.js";
createNotificationBodyValidation, import { createNotificationBodyValidation } from "@/validation/joi.js";
deleteNotificationParamValidation,
getNotificationByIdParamValidation,
testNotificationBodyValidation,
editNotificationParamValidation,
testAllNotificationsBodyValidation,
} from "@/validation/notificationValidation.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { IMonitorsRepository } from "@/repositories/index.js"; import { IMonitorsRepository } from "@/repositories/index.js";
import { INotificationsService } from "@/service/index.js"; import { INotificationsService } from "@/service/index.js";
import { requireTeamId, requireUserId } from "./controllerUtils.js"; import { requireTeamId } from "./controllerUtils.js";
const SERVICE_NAME = "NotificationController"; const SERVICE_NAME = "NotificationController";
class NotificationController { class NotificationController {
static SERVICE_NAME = SERVICE_NAME;
private notificationsService: INotificationsService; private notificationsService: INotificationsService;
private monitorsRepository: IMonitorsRepository; private monitorsRepository: IMonitorsRepository;
constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) { constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) {
@@ -23,9 +18,13 @@ class NotificationController {
this.monitorsRepository = monitorsRepository; this.monitorsRepository = monitorsRepository;
} }
get serviceName() {
return NotificationController.SERVICE_NAME;
}
testNotification = async (req: Request, res: Response, next: NextFunction) => { testNotification = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const notification = testNotificationBodyValidation.parse(req.body); const notification: Notification = req.body;
const success = await this.notificationsService.sendTestNotification(notification); const success = await this.notificationsService.sendTestNotification(notification);
if (!success) { if (!success) {
@@ -35,7 +34,6 @@ class NotificationController {
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Notification sent successfully", msg: "Notification sent successfully",
details: { service: SERVICE_NAME },
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -44,12 +42,25 @@ class NotificationController {
createNotification = async (req: Request, res: Response, next: NextFunction) => { createNotification = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = createNotificationBodyValidation.parse(req.body); await createNotificationBodyValidation.validateAsync(req.body, {
abortEarly: false,
});
const teamId = requireTeamId(req.user?.teamId); const body = req.body;
const userId = requireUserId(req.user?.id);
const notification = await this.notificationsService.createNotification(validatedBody, userId, teamId); const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const userId = req?.user?.id;
if (!userId) {
throw new AppError({ message: "User ID is required", status: 400 });
}
body.userId = userId;
body.teamId = teamId;
const notification = await this.notificationsService.createNotification(body);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Notification created successfully", msg: "Notification created successfully",
@@ -62,7 +73,11 @@ class NotificationController {
getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => { getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const notifications = await this.notificationsService.findNotificationsByTeamId(teamId); const notifications = await this.notificationsService.findNotificationsByTeamId(teamId);
return res.status(200).json({ return res.status(200).json({
@@ -77,10 +92,17 @@ class NotificationController {
deleteNotification = async (req: Request, res: Response, next: NextFunction) => { deleteNotification = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = req?.user?.teamId;
const validatedParams = deleteNotificationParamValidation.parse(req.params); if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
await this.notificationsService.deleteById(validatedParams.id, teamId); const notificationId = req.params.id as string;
if (!notificationId) {
throw new AppError({ message: "Notification ID is required", status: 400 });
}
await this.notificationsService.deleteById(notificationId, teamId);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Notification deleted successfully", msg: "Notification deleted successfully",
@@ -93,9 +115,12 @@ class NotificationController {
getNotificationById = async (req: Request, res: Response, next: NextFunction) => { getNotificationById = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const validatedParams = getNotificationByIdParamValidation.parse(req.params); const notificationId = req.params.id as string;
if (!notificationId) {
throw new AppError({ message: "Notification ID is required", status: 400 });
}
const notification = await this.notificationsService.findById(validatedParams.id, teamId); const notification = await this.notificationsService.findById(notificationId, teamId);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
@@ -109,13 +134,16 @@ class NotificationController {
editNotification = async (req: Request, res: Response, next: NextFunction) => { editNotification = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = createNotificationBodyValidation.parse(req.body); await createNotificationBodyValidation.validateAsync(req.body, {
const validatedParams = editNotificationParamValidation.parse(req.params); abortEarly: false,
});
const teamId = requireTeamId(req.user?.teamId); const teamId = requireTeamId(req.user?.teamId);
const notificationId = validatedParams.id; const notificationId = req.params.id as string;
if (!notificationId) {
const editedNotification = await this.notificationsService.updateById(notificationId, teamId, validatedBody); throw new AppError({ message: "Notification ID is required", status: 400 });
}
const editedNotification = await this.notificationsService.updateById(notificationId, teamId, req.body);
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
msg: "Notification updated successfully", msg: "Notification updated successfully",
@@ -128,11 +156,13 @@ class NotificationController {
testAllNotifications = async (req: Request, res: Response, next: NextFunction) => { testAllNotifications = async (req: Request, res: Response, next: NextFunction) => {
try { try {
const validatedBody = testAllNotificationsBodyValidation.parse(req.body); const monitorId = req.body.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const teamId = requireTeamId(req.user?.teamId); const monitor = await this.monitorsRepository.findById(monitorId, teamId);
const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId);
const notifications = monitor.notifications; const notifications = monitor.notifications;
if (notifications.length === 0) { if (notifications.length === 0) {
+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 };
} }
-4
View File
@@ -1,4 +0,0 @@
export interface IDb {
connect(): Promise<void>;
disconnect(): Promise<void>;
}
+9 -11
View File
@@ -1,12 +1,10 @@
import mongoose from "mongoose"; import mongoose from "mongoose";
import AppSettings from "@/db/models/AppSettings.js"; import AppSettings from "./models/AppSettings.js";
import { runMigrations } from "@/db/migration/index.js"; import { runMigrations } from "./migration/index.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
import { EnvConfig } from "@/service/system/settingsService.js"; import { EnvConfig } from "@/service/system/settingsService.js";
const SERVICE_NAME = "MongoDB"; const SERVICE_NAME = "MongoDB";
import { IDb } from "@/db/IDb.js"; class MongoDB {
class MongoDB implements IDb {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger; private logger: ILogger;
@@ -48,12 +46,12 @@ class MongoDB implements IDb {
}); });
await runMigrations(); await runMigrations();
} 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: "connect", method: "connect",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
throw error; throw error;
} }
@@ -65,12 +63,12 @@ class MongoDB implements IDb {
await mongoose.disconnect(); await mongoose.disconnect();
this.logger.info({ message: "Disconnected from MongoDB", service: SERVICE_NAME, method: "disconnect" }); this.logger.info({ message: "Disconnected from MongoDB", service: SERVICE_NAME, method: "disconnect" });
return; return;
} 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: "disconnect", method: "disconnect",
stack: error instanceof Error ? error.stack : undefined, stack: error.stack,
}); });
} }
}; };
@@ -1,60 +0,0 @@
import { MonitorModel } from "../models/Monitor.js";
import { logger } from "@/utils/logger.js";
/**
* Fix infrastructure monitors that had thresholds incorrectly set to 0 or 5
* due to the old default value. Updates them to 100 (disabled).
*/
export async function fixInfrastructureThresholds(): Promise<void> {
const SERVICE_NAME = "Migration:FixInfrastructureThresholds";
try {
logger.info({ service: SERVICE_NAME, message: "Starting infrastructure threshold fix" });
const thresholdFields = ["cpuAlertThreshold", "memoryAlertThreshold", "diskAlertThreshold", "tempAlertThreshold"];
// Find hardware monitors with any threshold set to 0 or 5
const filter = {
type: "hardware",
$or: thresholdFields.flatMap((field) => [{ [field]: 0 }, { [field]: 5 }]),
};
const monitors = await MonitorModel.find(filter);
if (monitors.length === 0) {
logger.info({ service: SERVICE_NAME, message: "No monitors with threshold value of 0 or 5 found" });
return;
}
logger.info({
service: SERVICE_NAME,
message: `Found ${monitors.length} monitors with threshold value of 0 or 5`,
});
let updatedCount = 0;
for (const monitor of monitors) {
const update: Record<string, number> = {};
for (const field of thresholdFields) {
if ((monitor as any)[field] === 0 || (monitor as any)[field] === 5) {
update[field] = 100;
}
}
if (Object.keys(update).length > 0) {
await MonitorModel.updateOne({ _id: monitor._id }, { $set: update });
updatedCount++;
}
}
logger.info({
service: SERVICE_NAME,
message: `Fixed ${updatedCount} monitors — thresholds updated from 0/5 to 100`,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error({ service: SERVICE_NAME, message: `Error fixing thresholds: ${errorMessage}` });
throw error;
}
}
-2
View File
@@ -1,7 +1,6 @@
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js"; import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js"; import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js"; import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
import MigrationModel from "../models/Migration.js"; import MigrationModel from "../models/Migration.js";
type MigrationEntry = { type MigrationEntry = {
@@ -13,7 +12,6 @@ const migrations: MigrationEntry[] = [
{ name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold }, { name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold },
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries }, { name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats }, { name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
]; ];
const runMigrations = async (logger?: { info: Function; error: Function }) => { const runMigrations = async (logger?: { info: Function; error: Function }) => {
+16 -2
View File
@@ -21,10 +21,13 @@ type CheckMetadataDocument = Omit<CheckMetadata, "monitorId" | "teamId"> & {
type: MonitorType; type: MonitorType;
}; };
type CheckDocumentBase = Omit<Check, "id" | "metadata" | "createdAt" | "updatedAt"> & { type CheckDocumentBase = Omit<Check, "id" | "metadata" | "expiry" | "ackAt" | "createdAt" | "updatedAt"> & {
metadata: CheckMetadataDocument; metadata: CheckMetadataDocument;
expiry: Date;
ackAt?: Date | null;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
__v: number;
}; };
interface CheckDocument extends CheckDocumentBase { interface CheckDocument extends CheckDocumentBase {
@@ -222,7 +225,18 @@ const CheckSchema = new Schema<CheckDocument>(
message: { message: {
type: String, type: String,
}, },
expiry: {
type: Date,
default: Date.now,
},
ack: {
type: Boolean,
default: false,
},
ackAt: {
type: Date,
default: undefined,
},
cpu: { cpu: {
type: cpuSchema, type: cpuSchema,
default: () => ({}), default: () => ({}),
-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;
-16
View File
@@ -160,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,
@@ -173,18 +169,6 @@ const MonitorSchema = new Schema<MonitorDocument>(
return value && value.trim() ? value.trim() : null; return value && value.trim() ? value.trim() : null;
}, },
}, },
geoCheckEnabled: {
type: Boolean,
default: false,
},
geoCheckLocations: {
type: [String],
default: [],
},
geoCheckInterval: {
type: Number,
default: 300000,
},
recentChecks: { recentChecks: {
type: [checkSnapshotSchema], type: [checkSnapshotSchema],
default: [], default: [],
+1 -1
View File
@@ -25,7 +25,7 @@ const NotificationSchema = new Schema<NotificationDocument>(
}, },
type: { type: {
type: String, type: String,
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as NotificationChannel[], enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as NotificationChannel[],
required: true, required: true,
}, },
notificationName: { notificationName: {
-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";
+5 -8
View File
@@ -2,23 +2,19 @@ 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";
import { runMigrations } from "./db/migration/index.js"; import { runMigrations } from "./db/migration/index.js";
import Logger, { ILogger } from "@/utils/logger.js"; import Logger, { ILogger } from "@/utils/logger.js";
import { SettingsService } from "@/service/index.js"; import SettingsService from "@/service/system/settingsService.js";
import { MongoSettingsRepository } from "./repositories/index.js"; import { MongoSettingsRepository } from "./repositories/index.js";
const SERVICE_NAME = "Server"; 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,8 +46,9 @@ const startApp = async () => {
openApiSpec, openApiSpec,
}); });
const server = app.listen(env.PORT, () => { const port = envSettings.port || 52345;
logger.info({ message: `Server started on port:${env.PORT}` }); const server = app.listen(port, () => {
logger.info({ message: `Server started on port:${port}` });
}); });
initShutdownListener(server, services); initShutdownListener(server, services);
@@ -20,19 +20,12 @@ export interface IChecksRepository {
monitorId: string, monitorId: string,
sortOrder: string, sortOrder: string,
dateRange: string, dateRange: string,
filter: string | undefined, filter: string,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
status: boolean | undefined status: boolean | undefined
): Promise<ChecksQueryResult>; ): Promise<ChecksQueryResult>;
findByTeamId( findByTeamId(sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string): Promise<ChecksQueryResult>;
sortOrder: string,
dateRange: string,
filter: string | undefined,
page: number,
rowsPerPage: number,
teamId: string
): Promise<ChecksQueryResult>;
findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>; findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
findByDateRangeAndMonitorId( findByDateRangeAndMonitorId(
monitorId: string, monitorId: string,
@@ -47,5 +40,4 @@ export interface IChecksRepository {
deleteByMonitorId(monitorId: string): Promise<number>; deleteByMonitorId(monitorId: string): Promise<number>;
deleteByTeamId(teamId: string): Promise<number>; deleteByTeamId(teamId: string): Promise<number>;
deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>; deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>;
deleteOlderThan(date: Date): Promise<number>;
} }
@@ -13,13 +13,20 @@ import type {
GotTimings, GotTimings,
MonitorType, MonitorType,
} from "@/types/index.js"; } from "@/types/index.js";
import { CheckModel, 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";
import { ILogger } from "@/utils/logger.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 };
@@ -27,8 +34,8 @@ type HardwareUpChecks = { totalChecks: number };
class MongoChecksRepository implements IChecksRepository { class MongoChecksRepository implements IChecksRepository {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger; private logger: any;
constructor(logger: ILogger) { constructor(logger: any) {
this.logger = logger; this.logger = logger;
} }
@@ -47,6 +54,13 @@ class MongoChecksRepository implements IChecksRepository {
return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
}; };
const toOptionalDateString = (value?: Date | string | null): string | undefined => {
if (!value) {
return undefined;
}
return toDateString(value);
};
const mapTimings = (timings?: GotTimings): GotTimings => { const mapTimings = (timings?: GotTimings): GotTimings => {
const phases = timings?.phases ?? { const phases = timings?.phases ?? {
wait: 0, wait: 0,
@@ -165,6 +179,9 @@ class MongoChecksRepository implements IChecksRepository {
timings: mapTimings(doc.timings), timings: mapTimings(doc.timings),
statusCode: doc.statusCode ?? 0, statusCode: doc.statusCode ?? 0,
message: doc.message ?? "", message: doc.message ?? "",
ack: doc.ack ?? false,
ackAt: toOptionalDateString(doc.ackAt),
expiry: toDateString(doc.expiry),
cpu: mapCpu(doc.cpu), cpu: mapCpu(doc.cpu),
memory: mapMemory(doc.memory), memory: mapMemory(doc.memory),
disk: mapDisks(doc.disk), disk: mapDisks(doc.disk),
@@ -177,6 +194,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),
__v: doc.__v ?? 0,
createdAt: toDateString(doc.createdAt), createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt), updatedAt: toDateString(doc.updatedAt),
}; };
@@ -221,18 +239,18 @@ class MongoChecksRepository implements IChecksRepository {
monitorId: string, monitorId: string,
sortOrder: string, sortOrder: string,
dateRange: string, dateRange: string,
filter: string | undefined, filter: string,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
status: boolean | undefined status: boolean | undefined
) => { ) => {
// Match // Match
const matchStage: Record<string, unknown> = { 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],
}, },
}), }),
}; };
@@ -279,11 +297,11 @@ 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, unknown> = { 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],
}, },
}), }),
}; };
@@ -348,7 +366,7 @@ class MongoChecksRepository implements IChecksRepository {
); );
const mapped = results.reduce<LatestChecksMap>((acc, { monitorId, docs }) => { const mapped = results.reduce<LatestChecksMap>((acc, { monitorId, docs }) => {
acc[monitorId] = docs.map((doc: CheckDocument) => this.toEntity(doc)); acc[monitorId] = docs.map((doc: any) => this.toEntity(doc as CheckDocument));
return acc; return acc;
}, {}); }, {});
return mapped; return mapped;
@@ -360,7 +378,7 @@ class MongoChecksRepository implements IChecksRepository {
return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString); return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
} }
if (options?.type === "pagespeed") { if (options?.type === "pagespeed") {
return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate, dateString); return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate);
} }
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString); return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString);
}; };
@@ -368,9 +386,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],
}, },
}), }),
}; };
@@ -402,31 +420,6 @@ class MongoChecksRepository implements IChecksRepository {
return result.deletedCount ?? 0; return result.deletedCount ?? 0;
}; };
deleteOlderThan = async (cutoffDate: Date, batchDays: number = 30): Promise<number> => {
// Find the oldest check that is older than the cutoff
const oldest = await CheckModel.findOne({ createdAt: { $lt: cutoffDate } })
.sort({ createdAt: 1 })
.select({ createdAt: 1 });
if (!oldest?.createdAt) return 0;
let totalDeleted = 0;
let batchStart = new Date(oldest.createdAt);
const batchMs = batchDays * 24 * 60 * 60 * 1000;
// Delete in 30 day chunks until we reach the cutoff
while (batchStart < cutoffDate) {
// Advance startTime by batchMs to get to the end of last batch
const nextStart = batchStart.getTime() + batchMs;
const batchEnd = new Date(Math.min(nextStart, cutoffDate.getTime()));
const result = await CheckModel.deleteMany({
createdAt: { $gte: batchStart, $lt: batchEnd },
});
totalDeleted += result.deletedCount ?? 0;
batchStart = batchEnd;
}
return totalDeleted;
};
private findUptimeDateRangeChecks = async ( private findUptimeDateRangeChecks = async (
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">, monitorType: Exclude<MonitorType, "hardware" | "pagespeed">,
monitorObjectId: mongoose.Types.ObjectId, monitorObjectId: mongoose.Types.ObjectId,
@@ -579,50 +572,16 @@ class MongoChecksRepository implements IChecksRepository {
}; };
}; };
private findPageSpeedDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date, dateString: string) => { private findPageSpeedDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date) => {
const matchStage = { const matchStage = {
"metadata.monitorId": monitorObjectId, "metadata.monitorId": monitorObjectId,
createdAt: { $gte: startDate, $lte: endDate }, createdAt: { $gte: startDate, $lte: endDate },
}; };
const [result] = await CheckModel.aggregate([ const checks = await CheckModel.find(matchStage).sort({ createdAt: -1 }).limit(25).lean();
{ $match: matchStage },
{ $sort: { createdAt: 1 } },
{
$facet: {
groupedChecks: [
{
$group: {
_id: {
$dateToString: { format: dateString, date: "$createdAt" },
},
avgPerformance: { $avg: "$performance" },
avgAccessibility: { $avg: "$accessibility" },
avgBestPractices: { $avg: "$bestPractices" },
avgSeo: { $avg: "$seo" },
totalChecks: { $sum: 1 },
},
},
{ $sort: { _id: 1 } },
{
$project: {
bucketDate: "$_id",
performance: "$avgPerformance",
accessibility: "$avgAccessibility",
bestPractices: "$avgBestPractices",
seo: "$avgSeo",
totalChecks: 1,
_id: 0,
},
},
],
},
},
]);
return { return {
monitorType: "pagespeed" as const, monitorType: "pagespeed" as const,
groupedChecks: result?.groupedChecks ?? [], checks: this.mapDocuments(checks),
}; };
}; };
@@ -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,387 +0,0 @@
import { IGeoChecksRepository } from "./IGeoChecksRepository.js";
import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js";
import type { FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
import mongoose from "mongoose";
import { getDateForRange } from "@/utils/dataUtils.js";
import { ILogger } from "@/utils/logger.js";
const SERVICE_NAME = "GeoChecksRepository";
class MongoGeoChecksRepository implements IGeoChecksRepository {
static SERVICE_NAME = SERVICE_NAME;
private logger: ILogger;
constructor(logger: ILogger) {
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);
}; };
@@ -5,12 +5,12 @@ export interface IMaintenanceWindowsRepository {
// fetch // fetch
findById(id: string, teamId: string): Promise<MaintenanceWindow>; findById(id: string, teamId: string): Promise<MaintenanceWindow>;
findByMonitorId(monitorId: string, teamId: string): Promise<MaintenanceWindow[]>; findByMonitorId(monitorId: string, teamId: string): Promise<MaintenanceWindow[]>;
findByTeamId(teamId: string, page: number, rowsPerPage: number, field?: string, order?: string, active?: boolean): Promise<MaintenanceWindow[]>; findByTeamId(teamId: string, active: boolean, page: number, rowsPerPage: number, field: string, order: string): Promise<MaintenanceWindow[]>;
// update // update
updateById(id: string, teamId: string, data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow>; updateById(id: string, teamId: string, data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow>;
// delete // delete
deleteById(id: string, teamId: string): Promise<MaintenanceWindow>; deleteById(id: string, teamId: string): Promise<MaintenanceWindow>;
// other // other
countByTeamId(teamId: string, active?: boolean): Promise<number>; countByTeamId(teamId: string, active: boolean): Promise<number>;
} }
@@ -75,11 +75,11 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
findByTeamId = async ( findByTeamId = async (
teamId: string, teamId: string,
active: boolean,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
field?: string, field: string,
order?: string, order: string
active?: boolean
): Promise<MaintenanceWindow[]> => { ): Promise<MaintenanceWindow[]> => {
const maintenanceQuery: Record<string, any> = { teamId }; const maintenanceQuery: Record<string, any> = { teamId };
@@ -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;
} }
@@ -126,7 +126,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
} }
return this.toEntity(deleted); return this.toEntity(deleted);
}; };
countByTeamId = async (teamId: string, active?: boolean) => { countByTeamId = async (teamId: string, active: boolean) => {
const maintenanceQuery: Record<string, any> = { teamId }; const maintenanceQuery: Record<string, any> = { teamId };
if (active !== undefined) maintenanceQuery.active = active; if (active !== undefined) maintenanceQuery.active = active;
@@ -17,7 +17,7 @@ export interface SummaryConfig {
export interface IMonitorsRepository { export interface IMonitorsRepository {
// create // create
create(monitor: Monitor, teamId: string, userId: string): Promise<Monitor | null>; create(monitor: Monitor, teamId: string, userId: string): Promise<Monitor | null>;
createMonitors(monitors: Monitor[]): Promise<Monitor[]>; createBulkMonitors(monitors: Monitor[]): Promise<Monitor[]>;
// single fetch // single fetch
findById(monitorId: string, teamId: string): Promise<Monitor>; findById(monitorId: string, teamId: string): Promise<Monitor>;
@@ -12,23 +12,13 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return this.toEntity(saved); return this.toEntity(saved);
}; };
createMonitors = async (monitors: Monitor[]): Promise<Monitor[]> => { createBulkMonitors = async (monitors: Monitor[]): Promise<Monitor[]> => {
if (!monitors.length) { if (!monitors.length) {
return []; return [];
} }
const payload = monitors.map((monitor) => ({ ...monitor, notifications: undefined })); const payload = monitors.map((monitor) => ({ ...monitor, notifications: undefined }));
try { const inserted = await MonitorModel.insertMany(payload, { ordered: false });
const inserted = await MonitorModel.insertMany(payload, { ordered: false }); return this.mapDocuments(inserted);
return this.mapDocuments(inserted);
} catch (error: any) {
if (error.name === "MongoBulkWriteError" || error.name === "BulkWriteError") {
const insertedDocs = error.insertedDocs || [];
if (insertedDocs.length > 0) {
return this.mapDocuments(insertedDocs);
}
}
throw error;
}
}; };
findById = async (monitorId: string, teamId: string): Promise<Monitor> => { findById = async (monitorId: string, teamId: string): Promise<Monitor> => {
@@ -352,12 +342,8 @@ class MongoMonitorsRepository implements IMonitorsRepository {
tempAlertCounter: doc.tempAlertCounter, tempAlertCounter: doc.tempAlertCounter,
selectedDisks: doc.selectedDisks ?? [], selectedDisks: doc.selectedDisks ?? [],
gameId: doc.gameId ?? undefined, gameId: doc.gameId ?? undefined,
grpcServiceName: doc.grpcServiceName ?? undefined,
group: doc.group ?? null, group: doc.group ?? null,
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)), recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [],
geoCheckInterval: doc.geoCheckInterval ?? 300000,
createdAt: toDateString(doc.createdAt), createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt), updatedAt: toDateString(doc.updatedAt),
}; };
@@ -411,12 +397,8 @@ class MongoMonitorsRepository implements IMonitorsRepository {
tempAlertCounter: doc.tempAlertCounter, tempAlertCounter: doc.tempAlertCounter,
selectedDisks: doc.selectedDisks ?? [], selectedDisks: doc.selectedDisks ?? [],
gameId: doc.gameId ?? undefined, gameId: doc.gameId ?? undefined,
grpcServiceName: doc.grpcServiceName ?? undefined,
group: doc.group ?? null, group: doc.group ?? null,
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)), recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
geoCheckEnabled: doc.geoCheckEnabled ?? false,
geoCheckLocations: doc.geoCheckLocations ?? [],
geoCheckInterval: doc.geoCheckInterval ?? 300000,
createdAt: toDateString(doc.createdAt), createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt), updatedAt: toDateString(doc.updatedAt),
}; };
@@ -3,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[] => {
Loaded 100 of 175 files, more files were not shown because too many files have changed in this diff. Show more