diff --git a/.gitignore b/.gitignore index 2299e795a..f6e292bc9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ .VSCodeCounter *.sh mongo -node_modules/ \ No newline at end of file +node_modules/ +docs/architecture \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 84fc4c3a6..038814543 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2472,11 +2472,13 @@ } }, "node_modules/axios": { - "version": "1.13.4", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, diff --git a/client/src/App.tsx b/client/src/App.tsx index 0a6b364f8..a8a25669e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, type CSSProperties } from "react"; +import { type CSSProperties } from "react"; import { useSelector } from "react-redux"; import "react-toastify/dist/ReactToastify.css"; import { ToastContainer } from "react-toastify"; @@ -6,7 +6,6 @@ import { ThemeProvider } from "@emotion/react"; import lightTheme from "./Utils/Theme/lightTheme"; import darkTheme from "./Utils/Theme/darkTheme"; import { CssBaseline, GlobalStyles } from "@mui/material"; -import { logger } from "./Utils/Logger"; // Import the logger import { Routes } from "./Routes"; import AppLayout from "@/Components/v2/layout/AppLayout"; import type { RootState } from "@/Types/state"; @@ -14,13 +13,6 @@ import type { RootState } from "@/Types/state"; function App() { const mode = useSelector((state: RootState) => state.ui.mode); - // Cleanup - useEffect(() => { - return () => { - logger.cleanup(); - }; - }, []); - const theme = mode === "light" ? lightTheme : darkTheme; return ( diff --git a/client/src/Components/WalletProvider/index.css b/client/src/Components/WalletProvider/index.css deleted file mode 100644 index f3da31efc..000000000 --- a/client/src/Components/WalletProvider/index.css +++ /dev/null @@ -1,53 +0,0 @@ -.wallet-adapter-dropdown { - width: 100%; - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: center; - gap: var(--env-var-spacing-1); -} - -.wallet-adapter-button { - display: flex; - align-items: center; - justify-content: center; - width: auto; - height: var(--env-var-height-2) !important; - font-size: var(--env-var-font-size-medium-plus) !important; - font-weight: 500 !important; - margin: 0; - padding: calc( - (var(--env-var-height-2) - var(--env-var-font-size-medium-plus) * 1.2) / 2 - ) - var(--env-var-spacing-1); - white-space: nowrap; -} - -.wallet-adapter-modal-title { - font-size: var(--font-size-h5) !important; -} - -/* Separator styling */ -.wallet-adapter-modal-divider { - background-color: var(--border-color) !important; - margin: var(--spacing-md) 0 !important; -} - -/* Responsive fixes */ -@media (max-width: 1200px) { - .wallet-adapter-button { - font-size: var(--env-var-font-size-medium) !important; - padding: calc((var(--env-var-height-2) - var(--env-var-font-size-medium) * 1.2) / 2) - var(--env-var-spacing-1-minus) !important; - } -} - -@media (max-width: 900px) { - .wallet-adapter-modal-wrapper { - flex-direction: column !important; - } - - .wallet-adapter-modal-divider { - margin: var(--spacing-sm) 0 !important; - } -} diff --git a/client/src/Components/WalletProvider/index.jsx b/client/src/Components/WalletProvider/index.jsx deleted file mode 100644 index 13cc83b07..000000000 --- a/client/src/Components/WalletProvider/index.jsx +++ /dev/null @@ -1,50 +0,0 @@ -// import { useMemo } from "react"; -// import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react"; -// import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"; -// import { -// UnsafeBurnerWalletAdapter, -// PhantomWalletAdapter, -// } from "@solana/wallet-adapter-wallets"; - -// import { WalletModalProvider } from "@solana/wallet-adapter-react-ui"; -// import { clusterApiUrl } from "@solana/web3.js"; -// import PropTypes from "prop-types"; -// import "./index.css"; - -// // Default styles that can be overridden by your app -// import "@solana/wallet-adapter-react-ui/styles.css"; - -// export const Wallet = ({ children }) => { -// // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'. -// const network = WalletAdapterNetwork.Mainnet; - -// // You can also provide a custom RPC endpoint. -// const endpoint = useMemo(() => clusterApiUrl(network), [network]); - -// const wallets = useMemo( -// () => [new PhantomWalletAdapter()], -// // eslint-disable-next-line react-hooks/exhaustive-deps -// [network] -// ); - -// return ( -// -// -// {children} -// -// -// ); -// }; - -// Wallet.propTypes = { -// children: PropTypes.node, -// }; - -const Wallet = ({ children }) => { - return children; -}; - -export default Wallet; diff --git a/client/src/Components/v1/Alert/index.css b/client/src/Components/v1/Alert/index.css deleted file mode 100644 index 233b5c466..000000000 --- a/client/src/Components/v1/Alert/index.css +++ /dev/null @@ -1,9 +0,0 @@ -.alert { - margin: 0; - width: fit-content; -} -.alert, -.alert button, -.alert .MuiTypography-root { - font-size: var(--env-var-font-size-medium); -} diff --git a/client/src/Components/v1/Alert/index.jsx b/client/src/Components/v1/Alert/index.jsx deleted file mode 100644 index dd3b2f9f2..000000000 --- a/client/src/Components/v1/Alert/index.jsx +++ /dev/null @@ -1,138 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Button, IconButton, Stack, Typography } from "@mui/material"; -import Icon from "../Icon"; -import "./index.css"; - -/** - * Icons mapping for different alert variants. - * @type {Object} - */ - -const icons = { - info: ( - - ), - error: ( - - ), - warning: ( - - ), -}; - -/** - * @param {Object} props - * @param {'info' | 'error' | 'warning'} props.variant - The type of alert. - * @param {string} [props.title] - The title of the alert. - * @param {string} [props.body] - The body text of the alert. - * @param {boolean} [props.isToast] - Indicates if the alert is used as a toast notification. - * @param {boolean} [props.hasIcon] - Whether to display an icon in the alert. - * @param {function} props.onClick - Toast dismiss function. - * @returns {JSX.Element} - */ - -const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { - const theme = useTheme(); - /* TODO - Do we need other variants for alert? - */ - - const text = theme.palette.secondary.contrastText; - const border = theme.palette.alert.contrastText; - const bg = theme.palette.alert.main; - const icon = icons[variant]; - - return ( - - {hasIcon && {icon}} - - {title && ( - {title} - )} - {body && ( - {body} - )} - {hasIcon && isToast && ( - - )} - - {isToast && ( - - - - )} - - ); -}; - -Alert.propTypes = { - variant: PropTypes.oneOf(["info", "error", "warning"]).isRequired, - title: PropTypes.string, - body: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - isToast: PropTypes.bool, - hasIcon: PropTypes.bool, - onClick: function (props, propName, componentName) { - if (props.isToast && !props[propName]) { - return new Error( - `Prop '${propName}' is required when 'isToast' is true in '${componentName}'.` - ); - } - return null; - }, -}; - -export default Alert; diff --git a/client/src/Components/v1/Breadcrumbs/index.css b/client/src/Components/v1/Breadcrumbs/index.css index abb3c0a28..022db9a83 100644 --- a/client/src/Components/v1/Breadcrumbs/index.css +++ b/client/src/Components/v1/Breadcrumbs/index.css @@ -6,7 +6,6 @@ min-height: 16px; } .MuiBreadcrumbs-root .MuiBreadcrumbs-li a { - font-size: var(--env-var-font-size-medium); font-weight: 400; } .MuiBreadcrumbs-root .MuiBreadcrumbs-li:not(:last-child) { diff --git a/client/src/Components/v1/HOC/withAdminCheck.jsx b/client/src/Components/v1/HOC/withAdminCheck.jsx deleted file mode 100644 index 6343b2e4e..000000000 --- a/client/src/Components/v1/HOC/withAdminCheck.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; - -import { logger } from "../../../Utils/Logger.js"; -import { useLazyGet } from "@/Hooks/UseApi"; - -const withAdminCheck = (WrappedComponent) => { - const WithAdminCheck = (props) => { - const navigate = useNavigate(); - const [superAdminExists, setSuperAdminExists] = useState(false); - const [hasChecked, setHasChecked] = useState(false); - const { get: checkSuperAdmin, loading: isChecking } = useLazyGet(); - - useEffect(() => { - checkSuperAdmin("/auth/users/superadmin") - .then((response) => { - if (response?.data === true) { - navigate("/login"); - } else { - setSuperAdminExists(false); - } - }) - .catch((error) => { - logger.error(error); - }) - .finally(() => { - setHasChecked(true); - }); - }, [navigate, checkSuperAdmin]); - - if (!hasChecked || isChecking) { - return null; - } - - return ( - - ); - }; - const wrappedComponentName = - WrappedComponent.displayName || WrappedComponent.name || "Component"; - WithAdminCheck.displayName = `WithAdminCheck(${wrappedComponentName})`; - - return WithAdminCheck; -}; - -export default withAdminCheck; diff --git a/client/src/Components/v1/Inputs/Select/index.jsx b/client/src/Components/v1/Inputs/Select/index.jsx index 0f96a2ff6..ec2522d17 100644 --- a/client/src/Components/v1/Inputs/Select/index.jsx +++ b/client/src/Components/v1/Inputs/Select/index.jsx @@ -64,7 +64,6 @@ const Select = ({ const theme = useTheme(); const getItemValue = (item) => item?._id ?? item?.id; const itemStyles = { - fontSize: "var(--env-var-font-size-medium)", color: theme.palette.primary.contrastTextTertiary, borderRadius: theme.shape.borderRadius, margin: theme.spacing(2), diff --git a/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx b/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx index 078684405..2b3abab0f 100644 --- a/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx +++ b/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx @@ -18,7 +18,6 @@ export const HttpAdornment = ({ https }) => { > diff --git a/client/src/Components/v1/ProtectedRoute/index.jsx b/client/src/Components/v1/ProtectedRoute/index.jsx deleted file mode 100644 index 313500338..000000000 --- a/client/src/Components/v1/ProtectedRoute/index.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Navigate } from "react-router-dom"; -import { useSelector } from "react-redux"; -import PropTypes from "prop-types"; - -/** - * ProtectedRoute is a wrapper component that ensures only authenticated users - * can access the wrapped content. It checks authentication status (e.g., from Redux or Context). - * If the user is authenticated, it renders the children; otherwise, it redirects to the login page. - * - * @param {Object} props - The props passed to the ProtectedRoute component. - * @param {React.ReactNode} props.children - The children to render if the user is authenticated. - * @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page. - */ - -const ProtectedRoute = ({ children }) => { - const authState = useSelector((state) => state.auth); - - return authState.authToken ? ( - children - ) : ( - - ); -}; - -ProtectedRoute.propTypes = { - children: PropTypes.element.isRequired, -}; - -export default ProtectedRoute; diff --git a/client/src/Components/v1/RoleProtectedRoute/index.jsx b/client/src/Components/v1/RoleProtectedRoute/index.jsx deleted file mode 100644 index c7b7dfc12..000000000 --- a/client/src/Components/v1/RoleProtectedRoute/index.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Navigate } from "react-router-dom"; -import { useSelector } from "react-redux"; -import PropTypes from "prop-types"; - -/** - * ProtectedRoute is a wrapper component that ensures only authenticated users - * can access the wrapped content. It checks authentication status (e.g., from Redux or Context). - * If the user is authenticated, it renders the children; otherwise, it redirects to the login page. - * - * @param {Object} props - The props passed to the ProtectedRoute component. - * @param {React.ReactNode} props.children - The children to render if the user is authenticated. - * @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page. - */ - -const RoleProtectedRoute = ({ roles, children }) => { - const authState = useSelector((state) => state.auth); - const userRoles = authState?.user?.role || []; - const canAccess = userRoles.some((role) => roles.includes(role)); - - return canAccess ? ( - children - ) : ( - - ); -}; - -RoleProtectedRoute.propTypes = { - children: PropTypes.element.isRequired, - roles: PropTypes.array, -}; - -export default RoleProtectedRoute; diff --git a/client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx b/client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx deleted file mode 100644 index 21256e114..000000000 --- a/client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx +++ /dev/null @@ -1,98 +0,0 @@ -import { useTheme } from "@mui/material"; -import "./index.css"; - -const SunAndMoonIcon = () => { - const theme = useTheme(); - - return ( - - ); -}; - -export default SunAndMoonIcon; diff --git a/client/src/Components/v1/ThemeSwitch/index.css b/client/src/Components/v1/ThemeSwitch/index.css deleted file mode 100644 index d5db1c1ca..000000000 --- a/client/src/Components/v1/ThemeSwitch/index.css +++ /dev/null @@ -1,64 +0,0 @@ -.sun-and-moon > :is(.moon, .sun, .sun-beams) { - transform-origin: center; -} - -.theme-toggle .sun-and-moon > .sun-beams { - stroke-width: 2px; -} - -.theme-dark .sun-and-moon > .sun { - transform: scale(1.75); -} - -.theme-dark .sun-and-moon > .sun-beams { - opacity: 0; -} - -.theme-dark .sun-and-moon > .moon > circle { - transform: translateX(-7px); -} - -@supports (cx: 1) { - .theme-dark .sun-and-moon > .moon > circle { - cx: 17; - transform: translateX(0); - } -} - -@media (prefers-reduced-motion: no-preference) { - .sun-and-moon > .sun { - transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55); - } - - .sun-and-moon > .sun-beams { - transition: - transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55), - opacity 0.5s cubic-bezier(0.25, 0.1, 0.25, 1); - } - - .sun-and-moon .moon > circle { - transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); - } - - @supports (cx: 1) { - .sun-and-moon .moon > circle { - transition: cx 0.25s cubic-bezier(0.4, 0, 0.2, 1); - } - } - - .theme-dark .sun-and-moon > .sun { - transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1); - transition-duration: 0.25s; - transform: scale(1.75); - } - - .theme-dark .sun-and-moon > .sun-beams { - transition-duration: 0.15s; - transform: rotateZ(-25deg); - } - - .theme-dark .sun-and-moon > .moon > circle { - transition-duration: 0.5s; - transition-delay: 0.25s; - } -} diff --git a/client/src/Components/v1/ThemeSwitch/index.jsx b/client/src/Components/v1/ThemeSwitch/index.jsx deleted file mode 100644 index 60763da90..000000000 --- a/client/src/Components/v1/ThemeSwitch/index.jsx +++ /dev/null @@ -1,53 +0,0 @@ -/** - * ThemeSwitch Component - * Dark and Light Theme Switch - * Original Code: https://web.dev/patterns/theming/theme-switch - * License: Apache License 2.0 - * Copyright © Google LLC - * - * This code has been adapted for use in this project. - * Apache License: https://www.apache.org/licenses/LICENSE-2.0 - */ - -import { IconButton } from "@mui/material"; -import SunAndMoonIcon from "./SunAndMoonIcon.jsx"; -import { useDispatch, useSelector } from "react-redux"; -import { setMode } from "../../../Features/UI/uiSlice.js"; -import "./index.css"; -import { useTranslation } from "react-i18next"; - -const ThemeSwitch = ({ width = 48, height = 48, color }) => { - const mode = useSelector((state) => state.ui.mode); - const dispatch = useDispatch(); - const { t } = useTranslation(); - - const toggleTheme = () => { - dispatch(setMode(mode === "light" ? "dark" : "light")); - }; - - return ( - :is(circle, g)": { - fill: color, - stroke: color, - }, - }} - > - - - ); -}; - -export default ThemeSwitch; diff --git a/client/src/Components/v2/design-elements/Avatar.tsx b/client/src/Components/v2/design-elements/Avatar.tsx index 74c06f8f3..2dd1160ad 100644 --- a/client/src/Components/v2/design-elements/Avatar.tsx +++ b/client/src/Components/v2/design-elements/Avatar.tsx @@ -31,6 +31,7 @@ export const Avatar = ({ src, small, sx, onClick = () => {} }: AvatarProps) => { alt={`${user?.firstName} ${user?.lastName}`} src={src ? src : user?.avatarImage ? image : undefined} sx={{ + color: theme.palette.primary.contrastText, fontSize: small ? "16px" : "22px", fontWeight: 400, backgroundColor: theme.palette.primary.main, diff --git a/client/src/Components/v2/design-elements/MonitorStatus.tsx b/client/src/Components/v2/design-elements/MonitorStatus.tsx index 3d5e81845..09cd95f0f 100644 --- a/client/src/Components/v2/design-elements/MonitorStatus.tsx +++ b/client/src/Components/v2/design-elements/MonitorStatus.tsx @@ -23,7 +23,6 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => { overflow={"hidden"} textOverflow={"ellipsis"} whiteSpace={"nowrap"} - maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"} > {monitor.name} @@ -40,7 +39,6 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => { overflow={"hidden"} textOverflow={"ellipsis"} whiteSpace={"nowrap"} - maxWidth={isSmall ? "100%" : "calc((100vw - var(--env-var-width-2)) / 2)"} > {formatUrl(monitor?.url)} diff --git a/client/src/Components/v2/design-elements/PulseDot.tsx b/client/src/Components/v2/design-elements/PulseDot.tsx index ab792b628..1f5b3401e 100644 --- a/client/src/Components/v2/design-elements/PulseDot.tsx +++ b/client/src/Components/v2/design-elements/PulseDot.tsx @@ -1,4 +1,17 @@ -import { Box, Stack, useTheme } from "@mui/material"; +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import { useTheme, keyframes } from "@mui/material"; + +const ripple = keyframes` + from { + opacity: 1; + transform: scale(0); + } + to { + opacity: 0; + transform: scale(2); + } +`; interface PulseDotProps { color: string; @@ -28,7 +41,7 @@ export const PulseDot = ({ color }: PulseDotProps) => { height: "100%", backgroundColor: "inherit", borderRadius: "50%", - animation: "ripple 1.8s ease-out infinite", + animation: `${ripple} 1.8s ease-out infinite`, }, "&::after": { content: `""`, diff --git a/client/src/Components/v2/design-elements/StatusBox.tsx b/client/src/Components/v2/design-elements/StatusBox.tsx index bf7979cca..73a881736 100644 --- a/client/src/Components/v2/design-elements/StatusBox.tsx +++ b/client/src/Components/v2/design-elements/StatusBox.tsx @@ -2,7 +2,6 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; import { BaseBox } from "@/Components/v2/design-elements"; -import Background from "@/assets/Images/background-grid.svg?react"; import { useTranslation } from "react-i18next"; import type { SxProps } from "@mui/material"; @@ -18,6 +17,7 @@ export const BGBox = ({ children, sx }: StatusBoxProps) => { return ( { > - - + top={0} + left={0} + right={0} + bottom={0} + sx={{ + pointerEvents: "none", + backgroundImage: ` + linear-gradient(${theme.palette.divider} 1px, transparent 1px), + linear-gradient(90deg, ${theme.palette.divider} 1px, transparent 1px) + `, + backgroundSize: "24px 24px", + maskImage: + "linear-gradient(135deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 30%, rgba(0,0,0,0.4) 100%)", + WebkitMaskImage: + "linear-gradient(135deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 30%, rgba(0,0,0,0.4) 100%)", + }} + /> {children} ); @@ -105,6 +117,40 @@ export const PausedStatusBox = ({ n }: { n: number }) => { /> ); }; +export const MaintenanceStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; +export const InitializingStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; +export const BreachedStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; + export const TotalChecksBox = ({ n }: { n: number }) => { const theme = useTheme(); const { t } = useTranslation(); @@ -138,15 +184,3 @@ export const UpChecksBox = ({ n }: { n: number }) => { /> ); }; - -export const InitializingStatusBox = ({ n }: { n: number }) => { - const theme = useTheme(); - const { t } = useTranslation(); - return ( - - ); -}; diff --git a/client/src/Components/v2/design-elements/StatusLabel.tsx b/client/src/Components/v2/design-elements/StatusLabel.tsx index d7212ddff..19bb8d157 100644 --- a/client/src/Components/v2/design-elements/StatusLabel.tsx +++ b/client/src/Components/v2/design-elements/StatusLabel.tsx @@ -11,33 +11,26 @@ import { useTranslation } from "react-i18next"; export const ValueTypes = ["positive", "negative", "neutral"] as const; export type ValueType = (typeof ValueTypes)[number]; -export const StatusLabel = ({ - status, - isActive, - sx, -}: { - status: MonitorStatus; - isActive?: boolean; - sx?: SxProps; -}) => { +export const StatusLabel = ({ status, sx }: { status: MonitorStatus; sx?: SxProps }) => { const { t } = useTranslation(); const theme = useTheme(); const palette = getStatusPalette(status); - const determineStatus = ( - isActive: boolean | undefined, - status: MonitorStatus - ): string => { - if (isActive === false) { + const determineStatus = (status: MonitorStatus): string => { + if (status === "up") { + return t("pages.common.monitors.status.up"); + } else if (status === "down") { + return t("pages.common.monitors.status.down"); + } else if (status === "breached") { + return t("pages.common.monitors.status.breached"); + } else if (status === "maintenance") { + return t("pages.common.monitors.status.maintenance"); + } else if (status === "paused") { return t("pages.common.monitors.status.paused"); + } else if (status === "initializing") { + return t("pages.common.monitors.status.initializing"); } - if (status === true) { - return t("pages.common.monitors.status.up"); - } - if (status === false) { - return t("pages.common.monitors.status.down"); - } return t("pages.common.monitors.status.initializing"); }; @@ -64,9 +57,7 @@ export const StatusLabel = ({ borderRadius="50%" marginRight="5px" /> - - {determineStatus(isActive, status)} - + {determineStatus(status)} ); }; diff --git a/client/src/Components/v1/I18nLoader/index.jsx b/client/src/Components/v2/i18nLoader/index.tsx similarity index 63% rename from client/src/Components/v1/I18nLoader/index.jsx rename to client/src/Components/v2/i18nLoader/index.tsx index 676e63d7b..f0555afed 100644 --- a/client/src/Components/v1/I18nLoader/index.jsx +++ b/client/src/Components/v2/i18nLoader/index.tsx @@ -1,8 +1,9 @@ -import i18n from "../../../Utils/i18n.js"; +import i18n from "@/Utils/i18n.js"; import { useSelector } from "react-redux"; import { useEffect } from "react"; +import type { RootState } from "@/store"; const I18nLoader = () => { - const language = useSelector((state) => state.ui.language ?? "en"); + const language = useSelector((state: RootState) => state.ui.language ?? "en"); useEffect(() => { if (language && i18n.language !== language) { diff --git a/client/src/Components/v2/inputs/ImageUpload.tsx b/client/src/Components/v2/inputs/ImageUpload.tsx index 6c9fc1783..0d0b77b40 100644 --- a/client/src/Components/v2/inputs/ImageUpload.tsx +++ b/client/src/Components/v2/inputs/ImageUpload.tsx @@ -38,11 +38,11 @@ export const ImageUpload = ({ const isValidType = accept.some((type) => file.type.includes(type)); const isValidSize = file.size <= maxSize; if (!isValidType) { - setLocalError(t("common.errors.invalidFileFormat")); + setLocalError(t("components.imageUpload.errors.invalidFileFormat")); return; } if (!isValidSize) { - setLocalError(t("common.errors.invalidFileSize")); + setLocalError(t("components.imageUpload.errors.invalidFileSize")); return; } setLocalError(null); @@ -137,15 +137,15 @@ export const ImageUpload = ({ color="primary" fontWeight={500} > - {t("common.imageUpload.clickToUpload")} + {t("components.imageUpload.clickToUpload")} {" "} - {t("common.imageUpload.orDragAndDrop")} + {t("components.imageUpload.orDragAndDrop")} - {accept.join(", ").toUpperCase()} • {t("common.imageUpload.maxSize")}{" "} + {accept.join(", ").toUpperCase()} • {t("components.imageUpload.maxSize")}{" "} {Math.round(maxSize / 1024 / 1024)}MB diff --git a/client/src/Components/v2/inputs/Slider.tsx b/client/src/Components/v2/inputs/Slider.tsx index 97a16ac2c..83037ab54 100644 --- a/client/src/Components/v2/inputs/Slider.tsx +++ b/client/src/Components/v2/inputs/Slider.tsx @@ -41,7 +41,10 @@ export const SliderInput = forwardRef( "& .MuiSlider-thumb": { backgroundColor: "#fff", "&:hover, &.Mui-focusVisible": { - boxShadow: `0 0 0 8px ${theme.palette.primary.main}20`, + boxShadow: "none", + }, + "&:active": { + boxShadow: "none", }, }, "& .MuiSlider-valueLabel": { diff --git a/client/src/Components/v2/monitors/HeaderMonitorsSummary.tsx b/client/src/Components/v2/monitors/HeaderMonitorsSummary.tsx new file mode 100644 index 000000000..38a6adfde --- /dev/null +++ b/client/src/Components/v2/monitors/HeaderMonitorsSummary.tsx @@ -0,0 +1,35 @@ +import { + UpStatusBox, + DownStatusBox, + PausedStatusBox, + InitializingStatusBox, + BreachedStatusBox, +} from "@/Components/v2/design-elements"; +import Stack from "@mui/material/Stack"; + +import type { MonitorsSummary } from "@/Types/Monitor"; +import { useTheme } from "@mui/material"; + +interface MonitorsSummaryProps { + summary: MonitorsSummary | null; + showBreached?: boolean; +} + +export const HeaderMonitorsSummary = ({ + summary, + showBreached = false, +}: MonitorsSummaryProps) => { + const theme = useTheme(); + return ( + + + + {showBreached && } + + + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx index 1c25c339e..d3aa62a69 100644 --- a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx +++ b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx @@ -3,7 +3,7 @@ import Typography from "@mui/material/Typography"; import { BaseChart } from "@/Components/v2/design-elements"; import { useTheme } from "@mui/material/styles"; import { useSelector } from "react-redux"; -import { formatDateWithTz } from "@/Utils/timeUtilsLegacy"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; import { useTranslation } from "react-i18next"; import { ResponsiveContainer, BarChart, XAxis, Bar, Cell, Tooltip } from "recharts"; import { getResponseTimeColor } from "@/Utils/MonitorUtils"; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx index e2856d6a9..9496395f6 100644 --- a/client/src/Components/v2/monitors/index.tsx +++ b/client/src/Components/v2/monitors/index.tsx @@ -11,3 +11,4 @@ export * from "./charts/PiePageSpeedLegend"; export * from "./charts/HistogramPageSpeedDetails"; export * from "./charts/HistogramPageSpeedDetailsTooltip"; export * from "./charts/HistogramInfrastructure"; +export * from "./HeaderMonitorsSummary"; diff --git a/client/src/Components/v2/routing/RouteProtected.tsx b/client/src/Components/v2/routing/RouteProtected.tsx new file mode 100644 index 000000000..7064c26a4 --- /dev/null +++ b/client/src/Components/v2/routing/RouteProtected.tsx @@ -0,0 +1,41 @@ +import { Navigate } from "react-router-dom"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; +import type { UserRole } from "@/Types/User"; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +export const ProtectedRoute = ({ children }: ProtectedRouteProps) => { + const authState = useSelector((state: RootState) => state.auth); + + return authState.authToken ? ( + children + ) : ( + + ); +}; + +interface RoleProtectedRouteProps { + roles: UserRole[]; + children: React.ReactNode; +} + +export const RoleProtectedRoute = ({ roles, children }: RoleProtectedRouteProps) => { + const authState = useSelector((state: RootState) => state.auth); + const userRoles = authState?.user?.role || []; + const canAccess = userRoles.some((role) => roles.includes(role)); + + return canAccess ? ( + children + ) : ( + + ); +}; diff --git a/client/src/Components/v2/sidebar/Authfooter.tsx b/client/src/Components/v2/sidebar/Authfooter.tsx index 62ea5610b..4724ed97d 100644 --- a/client/src/Components/v2/sidebar/Authfooter.tsx +++ b/client/src/Components/v2/sidebar/Authfooter.tsx @@ -76,7 +76,7 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) => alignItems="center" py={theme.spacing(4)} px={theme.spacing(8)} - gap={theme.spacing(2)} + gap={theme.spacing(4)} > { }; return ( - t.zIndex.drawer : "auto", - }} - > - + dispatch(setCollapsed({ collapsed: true }))} + sx={{ zIndex: 999 }} + /> + - - {menu.map((item) => { - const selected = location.pathname.startsWith(`/${item.path}`); - return ( - handleNavClick(item.path)} - /> - ); - })} - - - - {bottomMenu.map((item) => { - const selected = location.pathname.startsWith(`/${item.path}`); - return ( - handleNavClick(item.path)} - /> - ); - })} - - + + + {menu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + return ( + handleNavClick(item.path)} + /> + ); + })} + + + + {bottomMenu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + return ( + handleNavClick(item.path)} + /> + ); + })} + + - - + + + ); }; diff --git a/client/src/Features/UI/uiSlice.js b/client/src/Features/UI/uiSlice.js deleted file mode 100644 index 0e8ca8608..000000000 --- a/client/src/Features/UI/uiSlice.js +++ /dev/null @@ -1,98 +0,0 @@ -import { createSlice } from "@reduxjs/toolkit"; - -const initialMode = window?.matchMedia?.("(prefers-color-scheme: dark)")?.matches - ? "dark" - : "light"; - -const initialState = { - monitors: { - rowsPerPage: 10, - }, - team: { - rowsPerPage: 5, - }, - maintenance: { - rowsPerPage: 5, - }, - infrastructure: { - rowsPerPage: 5, - }, - logs: { - rowsPerPage: 15, - }, - sidebar: { - collapsed: false, - }, - mode: initialMode, - showURL: false, - greeting: { index: 0, lastUpdate: null }, - timezone: "America/Toronto", - distributedUptimeEnabled: false, - language: "en", - starPromptOpen: true, - chartType: "histogram", -}; - -const uiSlice = createSlice({ - name: "ui", - initialState, - reducers: { - setDistributedUptimeEnabled: (state, action) => { - state.distributedUptimeEnabled = action.payload; - }, - setRowsPerPage: (state, action) => { - const { table, value } = action.payload; - if (!state[table]) { - state[table] = {}; - } - state[table].rowsPerPage = value; - }, - toggleSidebar: (state) => { - state.sidebar.collapsed = !state.sidebar.collapsed; - }, - setCollapsed: (state, action) => { - const { collapsed } = action.payload; - state.sidebar.collapsed = collapsed; - }, - setMode: (state, action) => { - state.mode = action.payload; - }, - setShowURL: (state, action) => { - state.showURL = action.payload; - }, - setGreeting(state, action) { - if (!state.greeting) { - state.greeting = { index: 0, lastUpdate: null }; - } - state.greeting.index = action.payload.index; - state.greeting.lastUpdate = action.payload.lastUpdate; - }, - setTimezone(state, action) { - state.timezone = action.payload.timezone; - }, - setLanguage: (state, action) => { - state.language = action.payload; - }, - setStarPromptOpen: (state, action) => { - state.starPromptOpen = action.payload; - }, - setChartType: (state, action) => { - state.chartType = action.payload; - }, - }, -}); - -export default uiSlice.reducer; -export const { - setRowsPerPage, - toggleSidebar, - setCollapsed, - setMode, - setShowURL, - setGreeting, - setTimezone, - setDistributedUptimeEnabled, - setLanguage, - setStarPromptOpen, - setChartType, -} = uiSlice.actions; diff --git a/client/src/Features/UI/uiSlice.ts b/client/src/Features/UI/uiSlice.ts new file mode 100644 index 000000000..84e8248db --- /dev/null +++ b/client/src/Features/UI/uiSlice.ts @@ -0,0 +1,119 @@ +import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; + +type ThemeMode = "light" | "dark"; +type ChartType = "histogram" | "line"; +type TableName = "monitors" | "team" | "maintenance" | "infrastructure" | "logs"; + +interface TableState { + rowsPerPage: number; +} + +interface SidebarState { + collapsed: boolean; +} + +interface UIState { + monitors: TableState; + team: TableState; + maintenance: TableState; + infrastructure: TableState; + logs: TableState; + sidebar: SidebarState; + mode: ThemeMode; + showURL: boolean; + timezone: string; + distributedUptimeEnabled: boolean; + language: string; + starPromptOpen: boolean; + chartType: ChartType; +} + +const initialMode: ThemeMode = window?.matchMedia?.("(prefers-color-scheme: dark)") + ?.matches + ? "dark" + : "light"; + +const initialState: UIState = { + monitors: { + rowsPerPage: 10, + }, + team: { + rowsPerPage: 5, + }, + maintenance: { + rowsPerPage: 5, + }, + infrastructure: { + rowsPerPage: 5, + }, + logs: { + rowsPerPage: 15, + }, + sidebar: { + collapsed: false, + }, + mode: initialMode, + showURL: false, + timezone: "America/Toronto", + distributedUptimeEnabled: false, + language: "en", + starPromptOpen: true, + chartType: "histogram", +}; + +const uiSlice = createSlice({ + name: "ui", + initialState, + reducers: { + setDistributedUptimeEnabled: (state, action: PayloadAction) => { + state.distributedUptimeEnabled = action.payload; + }, + setRowsPerPage: ( + state, + action: PayloadAction<{ table: TableName; value: number }> + ) => { + const { table, value } = action.payload; + state[table].rowsPerPage = value; + }, + toggleSidebar: (state) => { + state.sidebar.collapsed = !state.sidebar.collapsed; + }, + setCollapsed: (state, action: PayloadAction<{ collapsed: boolean }>) => { + state.sidebar.collapsed = action.payload.collapsed; + }, + setMode: (state, action: PayloadAction) => { + state.mode = action.payload; + }, + setShowURL: (state, action: PayloadAction) => { + state.showURL = action.payload; + }, + + setTimezone: (state, action: PayloadAction<{ timezone: string }>) => { + state.timezone = action.payload.timezone; + }, + setLanguage: (state, action: PayloadAction) => { + state.language = action.payload; + }, + setStarPromptOpen: (state, action: PayloadAction) => { + state.starPromptOpen = action.payload; + }, + setChartType: (state, action: PayloadAction) => { + state.chartType = action.payload; + }, + }, +}); + +export type { UIState, ThemeMode, ChartType, TableName }; +export default uiSlice.reducer; +export const { + setRowsPerPage, + toggleSidebar, + setCollapsed, + setMode, + setShowURL, + setTimezone, + setDistributedUptimeEnabled, + setLanguage, + setStarPromptOpen, + setChartType, +} = uiSlice.actions; diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index 153cf9f19..633fcb5c9 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -83,10 +83,10 @@ export const useMonitorForm = ({ type: "hardware", url: data?.url || "", secret: data?.secret || "", - cpuAlertThreshold: data?.cpuAlertThreshold ?? 80, - memoryAlertThreshold: data?.memoryAlertThreshold ?? 80, - diskAlertThreshold: data?.diskAlertThreshold ?? 80, - tempAlertThreshold: data?.tempAlertThreshold ?? 80, + cpuAlertThreshold: data?.cpuAlertThreshold ?? 100, + memoryAlertThreshold: data?.memoryAlertThreshold ?? 100, + diskAlertThreshold: data?.diskAlertThreshold ?? 100, + tempAlertThreshold: data?.tempAlertThreshold ?? 100, selectedDisks: data?.selectedDisks || [], }; break; diff --git a/client/src/Pages/Auth/Register/index.tsx b/client/src/Pages/Auth/Register/index.tsx index 75d27649f..3c8a4e4bc 100644 --- a/client/src/Pages/Auth/Register/index.tsx +++ b/client/src/Pages/Auth/Register/index.tsx @@ -5,7 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js"; import { useRegisterForm } from "@/Hooks/useRegisterForm"; import type { RegisterFormData } from "@/Validation/register"; import { useTranslation } from "react-i18next"; -import { usePost } from "@/Hooks/UseApi"; +import { usePost, useGet } from "@/Hooks/UseApi"; import { setAuthState } from "@/Features/Auth/authSlice"; import { useDispatch } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; @@ -32,11 +32,21 @@ const RegisterPage = () => { const { post: verifyToken } = usePost<{ token: string }, InviteVerifyResponse>(); const hasVerified = useRef(false); + const { data: superAdminExists, isLoading: isCheckingAdmin } = useGet( + token ? null : "/auth/users/superadmin" + ); + const { control, handleSubmit, setError, reset } = useForm({ resolver: zodResolver(schema), defaultValues: defaults, }); + useEffect(() => { + if (superAdminExists === true) { + navigate("/login", { replace: true }); + } + }, [superAdminExists, navigate]); + useEffect(() => { if (!token || hasVerified.current) return; hasVerified.current = true; @@ -53,6 +63,8 @@ const RegisterPage = () => { }); }, [token]); + if (isCheckingAdmin) return null; + const onSubmit = async (data: RegisterFormData) => { if (loading) return; diff --git a/client/src/Pages/Auth/SetNewPassword/index.tsx b/client/src/Pages/Auth/SetNewPassword/index.tsx index 09bd565f1..2e99be254 100644 --- a/client/src/Pages/Auth/SetNewPassword/index.tsx +++ b/client/src/Pages/Auth/SetNewPassword/index.tsx @@ -97,27 +97,27 @@ const SetNewPasswordPage = () => { /> @@ -127,7 +127,7 @@ const SetNewPasswordPage = () => { fullWidth loading={loading} > - {t("auth.forgotPassword.buttons.resetPassword")} + {t("common.buttons.resetPassword")} { - return ; + return ; }, }, { diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 3972b8ebb..1c111e23d 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -329,11 +329,16 @@ const CreateMonitorPage = () => { render={({ field, fieldState }) => ( field.onChange(Number(e.target.value) || 0)} + value={field.value === 0 ? "" : field.value} + onChange={(e) => { + const val = e.target.value; + field.onChange(val === "" ? 0 : Number(val)); + }} type="number" - fieldLabel={t("portToMonitor")} - placeholder="5173" + fieldLabel={t("pages.createMonitor.form.general.option.port.label")} + placeholder={t( + "pages.createMonitor.form.general.option.port.placeholder" + )} fullWidth error={!!fieldState.error} helperText={fieldState.error?.message ?? ""} @@ -351,10 +356,12 @@ const CreateMonitorPage = () => { )} /> } /> + {/* Alert Thresholds - only for hardware type */} + {generalSettingsConfig.showSecret && ( + + ( + `${value}%`} + /> + )} + /> + ( + `${value}%`} + /> + )} + /> + ( + `${value}%`} + /> + )} + /> + ( + `${value}°C`} + /> + )} + /> + + } + /> + )} + { "pages.createMonitor.form.advanced.option.matchMethod.label" )} > - {t("matchMethodOptions.equal")} - - {t("matchMethodOptions.include")} + + {t( + "pages.createMonitor.form.advanced.option.matchMethod.equal" + )} + + + {t( + "pages.createMonitor.form.advanced.option.matchMethod.include" + )} + + + {t( + "pages.createMonitor.form.advanced.option.matchMethod.regex" + )} - {t("matchMethodOptions.regex")} )} /> diff --git a/client/src/Pages/Incidents/Components/DialogResolution.tsx b/client/src/Pages/Incidents/Components/DialogResolution.tsx index 648af52a9..a102b1de3 100644 --- a/client/src/Pages/Incidents/Components/DialogResolution.tsx +++ b/client/src/Pages/Incidents/Components/DialogResolution.tsx @@ -46,10 +46,9 @@ export const DialogResolution = ({ return ( setComment(e.target.value)} fullWidth diff --git a/client/src/Pages/Incidents/Components/IncidentTable.tsx b/client/src/Pages/Incidents/Components/IncidentTable.tsx index 121f33123..a9b4bc104 100644 --- a/client/src/Pages/Incidents/Components/IncidentTable.tsx +++ b/client/src/Pages/Incidents/Components/IncidentTable.tsx @@ -9,13 +9,13 @@ import type { Monitor } from "@/Types/Monitor"; import type { ActionMenuItem } from "@/Components/v2/actions-menu"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import { TypeToPathMap } from "@/Utils/monitorUtilsLegacy.js"; import { formatDateWithTz } from "@/Utils/TimeUtils"; import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; import Typography from "@mui/material/Typography"; import { useTheme } from "@mui/material/styles"; import Box from "@mui/material/Box"; +import { getMonitorPath } from "@/Utils/MonitorUtils"; interface IncidentsTableProps { title?: string; @@ -62,7 +62,7 @@ export const IncidentsTable = ({ label: t("pages.incidents.table.actions.goToMonitor"), action: () => { if (monitor) { - const path = TypeToPathMap[monitor.type as keyof typeof TypeToPathMap]; + const path = getMonitorPath(monitor.type); if (path && monitor.id) { navigate(`/${path}/${monitor.id}`); } diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx index ca236f45f..b2bcadc1a 100644 --- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx +++ b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable.tsx @@ -195,12 +195,7 @@ export const InfraMonitorsTable = ({ ), render: (row) => { - return ( - - ); + return ; }, }, { diff --git a/client/src/Pages/Infrastructure/Monitors/index.tsx b/client/src/Pages/Infrastructure/Monitors/index.tsx index 1d38dbdd3..8d1ef1a3a 100644 --- a/client/src/Pages/Infrastructure/Monitors/index.tsx +++ b/client/src/Pages/Infrastructure/Monitors/index.tsx @@ -1,13 +1,8 @@ import Stack from "@mui/material/Stack"; import useMediaQuery from "@mui/material/useMediaQuery"; -import { - MonitorBasePageWithStates, - UpStatusBox, - DownStatusBox, - PausedStatusBox, -} from "@/Components/v2/design-elements"; +import { MonitorBasePageWithStates } from "@/Components/v2/design-elements"; import { HeaderCreate } from "@/Components/v2/common"; -import { ControlsFilter } from "@/Components/v2/monitors"; +import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/v2/monitors"; import { TextField, Dialog } from "@/Components/v2/inputs"; import { useGet, useDelete } from "@/Hooks/UseApi"; @@ -99,7 +94,7 @@ const InfrastructureMonitors = () => { { refreshInterval: 5000, keepPreviousData: true } ); - const { summary, count } = monitorsWithChecksData ?? {}; + const { summary, count } = monitorsWithChecksData ?? { summary: null, count: 0 }; const isLoading = monitorsWithChecksLoading; // Check if any filters are active @@ -138,14 +133,10 @@ const InfrastructureMonitors = () => { isLoading={isLoading} isAdmin={isAdmin} /> - - - - - + { - const theme = useTheme(); - const { t } = useTranslation(); - - return ( - - - {icon} - - {header} - - {info} - - - - - - ); -}; - -// PropTypes for IntegrationsComponent -IntegrationsComponent.propTypes = { - icon: PropTypes.object.isRequired, - header: PropTypes.string.isRequired, - info: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, -}; - -/** - * Integrations Page Component - * @returns {JSX.Element} The JSX representation of the Integrations page. - */ - -const Integrations = () => { - const theme = useTheme(); - const { t } = useTranslation(); - - const integrations = [ - { - icon: ( - - ), - header: t("integrationsSlack"), - info: t("integrationsSlackInfo"), - onClick: () => {}, - }, - { - icon: ( - - ), - header: t("integrationsDiscord"), - info: t("integrationsDiscordInfo"), - onClick: () => {}, - }, - { - icon: ( - - ), - header: t("integrationsZapier"), - info: t("integrationsZapierInfo"), - onClick: () => {}, - }, - // Add more integrations as needed - ]; - - return ( - - {t("integrations")} - {t("integrationsPrism")} - - {integrations.map((integration, index) => ( - - ))} - - - ); -}; - -export default Integrations; diff --git a/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx b/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx index 42378bb67..876969d3d 100644 --- a/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx +++ b/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx @@ -196,13 +196,13 @@ export const MaintenanceWindowTable = ({ /> { setDeleteDialogOpen(false); setSelectedWindow(null); }} onConfirm={handleDelete} - confirmText={t("delete")} loading={deleteLoading} /> diff --git a/client/src/Pages/NotFound/index.tsx b/client/src/Pages/NotFound/index.tsx index 709f808be..868a1573a 100644 --- a/client/src/Pages/NotFound/index.tsx +++ b/client/src/Pages/NotFound/index.tsx @@ -9,8 +9,8 @@ import { useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; interface NotFoundProps { - title: string; - desc: string; + title?: string; + desc?: string; } const NotFoundPage = ({ title, desc }: NotFoundProps) => { diff --git a/client/src/Pages/PageSpeed/Monitors/index.tsx b/client/src/Pages/PageSpeed/Monitors/index.tsx index f6701a988..f958b5619 100644 --- a/client/src/Pages/PageSpeed/Monitors/index.tsx +++ b/client/src/Pages/PageSpeed/Monitors/index.tsx @@ -1,27 +1,22 @@ import { MonitorBasePageWithStates, - UpStatusBox, - DownStatusBox, - PausedStatusBox, PageSpeedKeyPriorityFallback, } from "@/Components/v2/design-elements"; import { Dialog } from "@/Components/v2/inputs"; import { HeaderCreate } from "@/Components/v2/common"; -import Stack from "@mui/material/Stack"; import { PageSpeedMonitorsTable } from "@/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable"; import type { Monitor } from "@/Types/Monitor"; -import { useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useState } from "react"; import { useIsAdmin } from "@/Hooks/useIsAdmin"; import { useGet, useDelete } from "@/Hooks/UseApi"; import type { MonitorsWithChecksResponse } from "@/Types/Monitor"; import type { AppSettingsResponse } from "@/Types/Settings"; +import { HeaderMonitorsSummary } from "@/Components/v2/monitors"; const PageSpeedMonitorsPage = () => { const { t } = useTranslation(); - const theme = useTheme(); const isAdmin = useIsAdmin(); const { deleteFn, loading: isDeleting } = useDelete(); @@ -48,8 +43,8 @@ const PageSpeedMonitorsPage = () => { const [rowsPerPage, setRowsPerPage] = useState(10); const monitors = monitorsData?.monitors; - const monitorsCount = monitorsData?.count; - const summary = monitorsData?.summary; + const monitorsCount = monitorsData?.count ?? 0; + const summary = monitorsData?.summary ?? null; const isLoading = monitorsIsLoading || settingsIsLoading; @@ -80,14 +75,7 @@ const PageSpeedMonitorsPage = () => { isLoading={isLoading} isAdmin={isAdmin} /> - - - - - + { } }, [fetchedSettings]); - const { put: saveSettingsFn, loading: isSaving } = usePut(); + const { patch: saveSettingsFn, loading: isSaving } = usePatch(); const saveSettings = async (settings) => { - const response = await saveSettingsFn("/settings", { settings }); + const response = await saveSettingsFn("/settings", settings); if (response?.data) { const data = response.data; setIsApiKeySet(data.pagespeedKeySet); diff --git a/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx b/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx index d738671ac..0e8f444c9 100644 --- a/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx +++ b/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx @@ -64,7 +64,7 @@ export const HeaderStatusPageControls = ({ }, }} > - {t("publicLink")} + {t("components.headerStatusPageControls.publicLink")} diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx index 04060673a..35aad7a26 100644 --- a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx +++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx @@ -7,7 +7,6 @@ import { StatusLabel, BaseBox } from "@/Components/v2/design-elements"; import { SwitchComponent } from "@/Components/v2/inputs"; import { useTheme } from "@mui/material/styles"; -import { determineState } from "@/Utils/MonitorUtils"; import { useSelector } from "react-redux"; import { useState } from "react"; import type { Monitor } from "@/Types/Monitor"; @@ -54,7 +53,6 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { )} {monitors?.map((monitor) => { - const status = determineState(monitor); return ( { )} - + {statusPage.showCharts !== false && ( diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx b/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx index 8b3343249..b5c52b32b 100644 --- a/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx +++ b/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx @@ -12,16 +12,16 @@ const getMonitorStatus = (monitors: Monitor[], theme: Theme, t: Function) => { icon: , }; - if (monitors.every((monitor) => monitor.status === true)) { + if (monitors.every((monitor) => monitor.status === "up")) { monitorsStatus.msg = t("pages.statusPages.statusBar.allUp"); monitorsStatus.color = theme.palette.success.main; monitorsStatus.icon = ; return monitorsStatus; - } else if (monitors.every((monitor) => monitor.status === false)) { + } else if (monitors.every((monitor) => monitor.status === "down")) { monitorsStatus.msg = t("pages.statusPages.statusBar.allDown"); monitorsStatus.color = theme.palette.error.main; return monitorsStatus; - } else if (monitors.some((monitor) => monitor.status === false)) { + } else if (monitors.some((monitor) => monitor.status === "down")) { monitorsStatus.msg = t("pages.statusPages.statusBar.degraded"); monitorsStatus.color = theme.palette.warning.main; return monitorsStatus; diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index 1b509507e..b1826b867 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -96,7 +96,6 @@ const StatusPageView = () => { }} /> )} - {t("statusPageStatusServiceStatus")} { id: "status", content: t("common.table.headers.status"), render: (row) => { - return ; + return ; }, }, { diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx index 94c619672..75738e6ad 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx @@ -186,12 +186,7 @@ export const MonitorTable = ({ ), render: (row) => { - return ( - - ); + return ; }, }, { diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 702bbd928..9e36ccc86 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -1,10 +1,5 @@ -import { ControlsFilter } from "@/Components/v2/monitors"; -import { - MonitorBasePageWithStates, - UpStatusBox, - DownStatusBox, - PausedStatusBox, -} from "@/Components/v2/design-elements"; +import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/v2/monitors"; +import { MonitorBasePageWithStates } from "@/Components/v2/design-elements"; import { TextField, Dialog } from "@/Components/v2/inputs"; import Stack from "@mui/material/Stack"; import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable"; @@ -99,7 +94,11 @@ const UptimeMonitorsPage = () => { { refreshInterval: 5000, keepPreviousData: true } ); - const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {}; + const { + monitors: monitorsWithChecks, + summary, + count, + } = monitorsWithChecksData ?? { monitors: null, summary: null, count: 0 }; // Delete hook const { deleteFn, loading: isDeleting } = useDelete(); @@ -150,14 +149,8 @@ const UptimeMonitorsPage = () => { isLoading={isLoading} isAdmin={isAdmin} /> - - - - - + + { - const mode = useSelector((state) => state.ui.mode); - const AdminCheckedRegister = withAdminCheck(AuthRegister); + const mode = useSelector((state: RootState) => state.ui.mode); const v2theme = mode === "light" ? lightTheme : darkTheme; return ( @@ -380,19 +384,18 @@ const Routes = () => { element={ <> - + } /> - + } diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 2a0272fdc..043076704 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -1,5 +1,4 @@ import type { GroupedCheck, CheckSnapshot } from "@/Types/Check"; -export type MonitorStatus = boolean | undefined; export const MonitorTypes = [ "http", @@ -13,12 +12,15 @@ export const MonitorTypes = [ ] as const; export type MonitorType = (typeof MonitorTypes)[number]; -export interface MonitorThresholds { - usage_cpu?: number; - usage_memory?: number; - usage_disk?: number; - usage_temperature?: number; -} +export const MonitorStatuses = [ + "up", + "down", + "paused", + "initializing", + "maintenance", + "breached", +] as const; +export type MonitorStatus = (typeof MonitorStatuses)[number]; export type MonitorMatchMethod = "equal" | "include" | "regex" | ""; @@ -28,7 +30,7 @@ export interface Monitor { teamId: string; name: string; description?: string; - status?: boolean; + status: MonitorStatus; statusWindow: boolean[]; statusWindowSize: number; statusWindowThreshold: number; @@ -45,12 +47,14 @@ export interface Monitor { uptimePercentage?: number; notifications: string[]; secret?: string; - thresholds?: MonitorThresholds; - alertThreshold: number; cpuAlertThreshold: number; + cpuAlertCounter: number; memoryAlertThreshold: number; + memoryAlertCounter: number; diskAlertThreshold: number; + diskAlertCounter: number; tempAlertThreshold: number; + tempAlertCounter: number; selectedDisks: string[]; gameId?: string; group: string | null; @@ -66,6 +70,9 @@ export interface MonitorsSummary { upMonitors: number; downMonitors: number; pausedMonitors: number; + initializingMonitors: number; + maintenanceMonitors: number; + breachedMonitors: number; } export interface MonitorsWithChecksResponse { diff --git a/client/src/Utils/Logger.js b/client/src/Utils/Logger.js deleted file mode 100644 index 849e10854..000000000 --- a/client/src/Utils/Logger.js +++ /dev/null @@ -1,74 +0,0 @@ -const LOG_LEVEL = import.meta.env.VITE_APP_LOG_LEVEL || "debug"; -const NO_OP = () => {}; - -class Logger { - constructor() { - let logLevel = LOG_LEVEL; - this.updateLogLevel(logLevel); - // Defer store subscription to avoid circular dependency during HMR - setTimeout(() => { - try { - import("../store").then(({ default: store }) => { - if (store) { - this.unsubscribe = store.subscribe(() => { - logLevel = "debug"; - this.updateLogLevel(logLevel); - }); - } - }); - } catch (e) { - // Store not ready yet, ignore - } - }, 0); - } - - updateLogLevel(logLevel) { - if (logLevel === "none") { - this.info = NO_OP; - this.error = NO_OP; - this.warn = NO_OP; - this.log = NO_OP; - return; - } - - if (logLevel === "error") { - this.error = console.error.bind(console); - this.info = NO_OP; - this.warn = NO_OP; - this.log = NO_OP; - return; - } - - if (logLevel === "warn") { - this.error = console.error.bind(console); - this.warn = console.warn.bind(console); - this.info = NO_OP; - this.log = NO_OP; - return; - } - - if (logLevel === "info") { - this.error = console.error.bind(console); - this.warn = console.warn.bind(console); - this.info = console.info.bind(console); - this.log = NO_OP; - return; - } - - if (logLevel === "debug") { - this.error = console.error.bind(console); - this.warn = console.warn.bind(console); - this.info = console.info.bind(console); - this.log = console.log.bind(console); - return; - } - } - - cleanup() { - if (this.unsubscribe) { - this.unsubscribe(); - } - } -} - -export const logger = new Logger(); diff --git a/client/src/Utils/MonitorUtils.ts b/client/src/Utils/MonitorUtils.ts index 6a30b3fc4..b73fc5bc4 100644 --- a/client/src/Utils/MonitorUtils.ts +++ b/client/src/Utils/MonitorUtils.ts @@ -1,33 +1,29 @@ -import type { Monitor, MonitorStatus, MonitorType } from "@/Types/Monitor"; +import type { MonitorStatus, MonitorType } from "@/Types/Monitor"; import type { PaletteKey } from "@/Utils/Theme/v2Theme"; import type { ValueType } from "@/Components/v2/design-elements/StatusLabel"; -export const determineState = (monitor: Monitor) => { - if (typeof monitor === "undefined") return "pending"; - if (monitor?.isActive === false) return "paused"; - if (monitor?.status === undefined) return "pending"; - return monitor?.status == true ? "up" : "down"; -}; - export const getMonitorPath = (type: MonitorType): string => { const pathMap: Record = { http: "uptime", port: "uptime", ping: "uptime", - hardware: "hardware", - pagespeed: "pagespeed", - docker: "docker", - game: "game-servers", + game: "uptime", unknown: "uptime", + docker: "uptime", + hardware: "infrastructure", + pagespeed: "pagespeed", }; return pathMap[type]; }; export const getStatusPalette = (status: MonitorStatus): PaletteKey => { - if (status === true) { + if (status === "up") { return "success"; } - if (status === false) { + if (status === "down") { + return "error"; + } + if (status === "breached") { return "error"; } return "warning"; @@ -43,11 +39,11 @@ export const getValuePalette = (value: ValueType): PaletteKey => { }; export const getStatusColor = (status: MonitorStatus, theme: any): string => { - if (status === true) { + if (status === "up") { return theme.palette.success.light; } - if (status === false) { + if (status === "down") { return theme.palette.error.light; } @@ -88,40 +84,3 @@ export const formatUrl = (url: string, maxLength: number = 55) => { ? `${strippedUrl.slice(0, maxLength)}…` : strippedUrl; }; - -export interface IStatusPageHeaderConfig { - paletteKey: PaletteKey; - message: string; -} -export const getStatusPageHeaderConfig = ( - monitors: Monitor[], - t: any -): IStatusPageHeaderConfig => { - if (!monitors || monitors.length === 0) { - return { paletteKey: "error", message: "No monitors available" }; - } - - const allUp = monitors.every((monitor) => monitor.status === true); - const anyDown = monitors.some((monitor) => monitor.status === false); - const allDown = monitors.every((monitor) => monitor.status === false); - - if (allUp) - return { - paletteKey: "success", - message: t("statusPage.details.statusHeader.allUp"), - }; - if (allDown) - return { - paletteKey: "error", - message: t("statusPage.details.statusHeader.allDown"), - }; - if (anyDown) - return { - paletteKey: "warning", - message: t("statusPage.details.statusHeader.anyDown"), - }; - return { - paletteKey: "warning", - message: t("statusPage.details.statusHeader.anyDown"), - }; -}; diff --git a/client/src/Utils/ReadMe.md b/client/src/Utils/ReadMe.md deleted file mode 100644 index 6488752d1..000000000 --- a/client/src/Utils/ReadMe.md +++ /dev/null @@ -1 +0,0 @@ -#Utils folder diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index 37143d730..98f3479be 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -38,7 +38,6 @@ const baseTheme = (palette) => ({ fontWeight: 400, }, label: { - fontSize: "var(--env-var-font-size-medium)", color: palette.primary.contrastTextSecondary, fontWeight: 500, }, @@ -332,8 +331,6 @@ const baseTheme = (palette) => ({ "& .MuiInputBase-input": { padding: ".75em", - minHeight: "var(--env-var-height-2)", - fontSize: "var(--env-var-font-size-medium)", fontWeight: 400, color: palette.primary.contrastTextSecondary, "&.Mui-disabled": { @@ -368,13 +365,12 @@ const baseTheme = (palette) => ({ "& .MuiFormHelperText-root": { color: palette.error.main, opacity: 0.8, - fontSize: "var(--env-var-font-size-medium)", + fontSize: "var()", marginLeft: 0, }, "& .MuiFormHelperText-root.Mui-error": { opacity: 0.8, - fontSize: "var(--env-var-font-size-medium)", color: palette.error.main, whiteSpace: "nowrap", }, @@ -762,7 +758,6 @@ const baseTheme = (palette) => ({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", - maxWidth: "calc((100vw - var(--env-var-width-2)) / 2)", }, }, { @@ -775,7 +770,6 @@ const baseTheme = (palette) => ({ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", - maxWidth: "calc((100vw - var(--env-var-width-2)) / 2)", }, }, ], diff --git a/client/src/Utils/Theme/v2Theme.ts b/client/src/Utils/Theme/v2Theme.ts index 57ca76669..39ede6c7e 100644 --- a/client/src/Utils/Theme/v2Theme.ts +++ b/client/src/Utils/Theme/v2Theme.ts @@ -49,6 +49,13 @@ export const theme = (mode: string, palette: any) => }, components: { + MuiTouchRipple: { + styleOverrides: { + root: { + display: "none", + }, + }, + }, MuiButtonBase: { defaultProps: { disableRipple: true, diff --git a/client/src/Utils/fileUtils.js b/client/src/Utils/fileUtils.js deleted file mode 100644 index 1c35738dd..000000000 --- a/client/src/Utils/fileUtils.js +++ /dev/null @@ -1,11 +0,0 @@ -export const formatBytes = (bytes) => { - if (bytes === 0) return "0 Bytes"; - const megabytes = bytes / (1024 * 1024); - return megabytes.toFixed(2) + " MB"; -}; - -export const checkImage = (url) => { - const img = new Image(); - img.src = url; - return img.naturalWidth !== 0; -}; diff --git a/client/src/Utils/greeting.jsx b/client/src/Utils/greeting.jsx deleted file mode 100644 index c4afbf88c..000000000 --- a/client/src/Utils/greeting.jsx +++ /dev/null @@ -1,197 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Typography } from "@mui/material"; -import { useDispatch, useSelector } from "react-redux"; -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { setGreeting } from "../Features/UI/uiSlice"; - -const early = [ - { - prepend: "Rise and shine", - append: "If you’re up this early, you might as well be a legend!", - emoji: "☕", - }, - { - prepend: "Good morning", - append: "The world’s still asleep, but you’re already awesome!", - emoji: "🦉", - }, - { - prepend: "Good morning", - append: "Are you a wizard? Only magical people are up at this hour!", - emoji: "🌄", - }, - { - prepend: "Up before the roosters", - append: "Ready to tackle the day before it even starts?", - emoji: "🐓", - }, - { - prepend: "Early bird special", - append: "Let’s get things done while everyone else is snoozing!", - emoji: "🌟", - }, -]; - -const morning = [ - { - prepend: "Good morning", - append: "Is it coffee o’clock yet, or should we start with high fives?", - emoji: "☕", - }, - { - prepend: "Morning", - append: "The sun is up, and so are you—time to be amazing!", - emoji: "🌞", - }, - { - prepend: "Good morning", - append: "Time to make today the best thing since sliced bread!", - emoji: "🥐", - }, - { - prepend: "Morning", - append: "Let’s kick off the day with more energy than a double espresso!", - emoji: "🚀", - }, - { - prepend: "Rise and shine", - append: "You’re about to make today so great, even Monday will be jealous!", - emoji: "🌟", - }, -]; - -const afternoon = [ - { - prepend: "Good afternoon", - append: "How about a break to celebrate how awesome you’re doing?", - emoji: "🥪", - }, - { - prepend: "Afternoon", - append: "If you’re still going strong, you’re officially a rockstar!", - emoji: "🌞", - }, - { - prepend: "Hey there", - append: "The afternoon is your playground—let’s make it epic!", - emoji: "🍕", - }, - { - prepend: "Good afternoon", - append: "Time to crush the rest of the day like a pro!", - emoji: "🏆", - }, - { - prepend: "Afternoon", - append: "Time to turn those afternoon slumps into afternoon triumphs!", - emoji: "🎉", - }, -]; - -const evening = [ - { - prepend: "Good evening", - append: "Time to wind down and think about how you crushed today!", - emoji: "🌇", - }, - { - prepend: "Evening", - append: "You’ve earned a break—let’s make the most of these evening vibes!", - emoji: "🍹", - }, - { - prepend: "Hey there", - append: "Time to relax and bask in the glow of your day’s awesomeness!", - emoji: "🌙", - }, - { - prepend: "Good evening", - append: "Ready to trade productivity for chill mode?", - emoji: "🛋️ ", - }, - { - prepend: "Evening", - append: "Let’s call it a day and toast to your success!", - emoji: "🕶️", - }, -]; - -/** - * Greeting component that displays a personalized greeting message - * based on the time of day and the user's first name. - * - * @component - * @example - * return ; - * - * @param {Object} props - * @param {string} props.type - The type of monitor to be displayed in the message - * @returns {JSX.Element} The rendered Greeting component - */ - -const Greeting = ({ type = "" }) => { - const theme = useTheme(); - const dispatch = useDispatch(); - const { t } = useTranslation(); - const { firstName } = useSelector((state) => state.auth.user); - const index = useSelector((state) => state.ui.greeting?.index ?? 0); - const lastUpdate = useSelector((state) => state.ui.greeting?.lastUpdate ?? null); - - const now = new Date(); - const hour = now.getHours(); - - useEffect(() => { - const hourDiff = lastUpdate ? hour - lastUpdate : null; - - if (!lastUpdate || hourDiff >= 1) { - let random = Math.floor(Math.random() * 5); - dispatch(setGreeting({ index: random, lastUpdate: hour })); - } - }, [dispatch, hour, lastUpdate]); - - let greetingArray = - hour < 6 ? early : hour < 12 ? morning : hour < 18 ? afternoon : evening; - const { prepend, append, emoji } = greetingArray[index]; - - return ( - - - - {t("greeting.prepend", { defaultValue: prepend })},{" "} - - - {firstName} {emoji} - - - - {t("greeting.append", { defaultValue: append })} —{" "} - {t("greeting.overview", { type: t(`menu.${type}`) })} - - - ); -}; - -Greeting.propTypes = { - type: PropTypes.string, -}; - -export default Greeting; diff --git a/client/src/Utils/i18n.js b/client/src/Utils/i18n.ts similarity index 55% rename from client/src/Utils/i18n.js rename to client/src/Utils/i18n.ts index 030091d48..db448a065 100644 --- a/client/src/Utils/i18n.js +++ b/client/src/Utils/i18n.ts @@ -1,16 +1,26 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; +import type { Resource } from "i18next"; const primaryLanguage = "en"; -// Load all translation files eagerly -const translations = import.meta.glob("../locales/*.json", { eager: true }); +interface TranslationModule { + default?: Record; + [key: string]: unknown; +} -const resources = {}; +// Load all translation files eagerly +const translations = import.meta.glob("../locales/*.json", { + eager: true, +}); + +const resources: Resource = {}; Object.keys(translations).forEach((path) => { - const langCode = path.match(/\/([^/]+)\.json$/)[1]; + const match = path.match(/\/([^/]+)\.json$/); + if (!match) return; + const langCode = match[1]; resources[langCode] = { - translation: translations[path].default || translations[path], + translation: translations[path].default ?? translations[path], }; }); diff --git a/client/src/Utils/monitorUtilsLegacy.js b/client/src/Utils/monitorUtilsLegacy.js deleted file mode 100644 index e3645b0d0..000000000 --- a/client/src/Utils/monitorUtilsLegacy.js +++ /dev/null @@ -1,47 +0,0 @@ -import { capitalizeFirstLetter } from "./stringUtils"; - -/** - * Helper function to get duration since last check or the last date checked - * @param {Array} checks Array of check objects. - * @param {boolean} duration Whether the function should return the duration since last checked or the date itself - * @returns {number} Timestamp of the most recent check. - */ -export const getLastChecked = (checks, duration = true) => { - if (!checks || checks.length === 0) { - return 0; // Handle case when no checks are available - } - - // Data is sorted newest -> oldest, so newest check is the most recent - if (!duration) { - return new Date(checks[0].createdAt); - } - return new Date() - new Date(checks[0].createdAt); -}; - -export const parseDomainName = (url) => { - url = url.replace(/^https?:\/\//, ""); - // Remove leading/trailing dots - url = url.replace(/^\.+|\.+$/g, ""); - // Split by dots - const parts = url.split("."); - // Remove common prefixes and empty parts and exclude the last element of the array (the last element should be the TLD) - const cleanParts = parts.filter((part) => part !== "www" && part !== "").slice(0, -1); - // If there's more than one part, append the two words and capitalize the first letters (e.g. ["api", "test"] -> "Api Test") - const domainPart = - cleanParts.length > 1 - ? cleanParts.map((part) => capitalizeFirstLetter(part)).join(" ") - : capitalizeFirstLetter(cleanParts[0]); - - if (domainPart) return domainPart; - - return url; -}; - -export const TypeToPathMap = { - http: "uptime", - port: "uptime", - docker: "uptime", - ping: "uptime", - hardware: "infrastructure", - pagespeed: "pagespeed", -}; diff --git a/client/src/Utils/roleUtils.js b/client/src/Utils/roleUtils.js deleted file mode 100644 index f359246b2..000000000 --- a/client/src/Utils/roleUtils.js +++ /dev/null @@ -1,13 +0,0 @@ -export const ROLES = { - SUPERADMIN: "superadmin", - ADMIN: "admin", - USER: "user", - DEMO: "demo", -}; - -export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO]; - -export const EDITABLE_ROLES = [ - { role: ROLES.ADMIN, _id: ROLES.ADMIN }, - { role: ROLES.USER, _id: ROLES.USER }, -]; diff --git a/client/src/Utils/stringUtils.js b/client/src/Utils/stringUtils.js deleted file mode 100644 index bb62d0169..000000000 --- a/client/src/Utils/stringUtils.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Helper function to get first letter capitalized string - * @param {string} str String whose first letter is to be capitalized - * @returns A string with first letter capitalized - */ -export const capitalizeFirstLetter = (str) => { - if (str === null || str === undefined) { - return ""; - } - if (typeof str !== "string") { - throw new TypeError("Input must be a string"); - } - if (str.length === 0) { - return ""; - } - return str.charAt(0).toUpperCase() + str.slice(1); -}; - -/** - * Helper function to get first letter as a lower case string - * @param {string} str String whose first letter is to be lower cased - * @returns A string with first letter lower cased - */ - -export const toLowerCaseFirstLetter = (str) => { - if (str === null || str === undefined) { - return ""; - } - if (typeof str !== "string") { - throw new TypeError("Input must be a string"); - } - if (str.length === 0) { - return ""; - } - return str.charAt(0).toLowerCase() + str.slice(1); -}; - -/** - * Checks if a string is null, undefined, or empty (including strings with only whitespace). - * @param {string} str - The string to check. - * @returns {boolean} - Returns true if the string is null, undefined, or empty. - */ -export const isEmpty = (str) => { - // Check if string is null, undefined, or empty (including whitespace only) - return str === null || typeof str === "undefined" || str.trim().length === 0; -}; diff --git a/client/src/Utils/timeUtilsLegacy.js b/client/src/Utils/timeUtilsLegacy.js deleted file mode 100644 index 365572f6b..000000000 --- a/client/src/Utils/timeUtilsLegacy.js +++ /dev/null @@ -1,167 +0,0 @@ -import dayjs from "dayjs"; -import duration from "dayjs/plugin/duration"; -import utc from "dayjs/plugin/utc"; -import timezone from "dayjs/plugin/timezone"; -import customParseFormat from "dayjs/plugin/customParseFormat"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(customParseFormat); -dayjs.extend(duration); - -export const MS_PER_SECOND = 1000; -export const MS_PER_MINUTE = 60 * MS_PER_SECOND; -export const MS_PER_HOUR = 60 * MS_PER_MINUTE; -export const MS_PER_DAY = 24 * MS_PER_HOUR; -export const MS_PER_WEEK = MS_PER_DAY * 7; - -export const formatDuration = (ms) => { - const seconds = Math.floor(ms / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - let dateStr = ""; - - days && (dateStr += `${days}d `); - hours && (dateStr += `${hours % 24}h `); - minutes && (dateStr += `${minutes % 60}m `); - seconds && (dateStr += `${seconds % 60}s `); - - dateStr === "" && (dateStr = "0s"); - - return dateStr; -}; - -export const formatDurationRounded = (ms) => { - const seconds = Math.floor(ms / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - let time = ""; - if (days > 0) { - time += `${days} day${days !== 1 ? "s" : ""}`; - return time; - } - if (hours > 0) { - time += `${hours} hour${hours !== 1 ? "s" : ""}`; - return time; - } - if (minutes > 0) { - time += `${minutes} minute${minutes !== 1 ? "s" : ""}`; - return time; - } - if (seconds > 0) { - time += `${seconds} second${seconds !== 1 ? "s" : ""}`; - return time; - } - - return time; -}; - -export const formatDurationSplit = (ms) => { - const seconds = Math.floor(ms / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - return days > 0 - ? { time: days, format: days === 1 ? "day" : "days" } - : hours > 0 - ? { time: hours, format: hours === 1 ? "hour" : "hours" } - : minutes > 0 - ? { time: minutes, format: minutes === 1 ? "minute" : "minutes" } - : seconds > 0 - ? { time: seconds, format: seconds === 1 ? "second" : "seconds" } - : { time: 0, format: "seconds" }; -}; - -export const getHumanReadableDuration = (ms) => { - const durationObj = dayjs.duration(ms); - - const parts = { - days: Math.floor(durationObj.asDays()), - hours: durationObj.hours(), - minutes: durationObj.minutes(), - seconds: durationObj.seconds(), - milliseconds: durationObj.milliseconds(), - }; - - const result = []; - - if (parts.days > 0) { - result.push(`${parts.days}d`); - } - if (parts.hours > 0) { - result.push(`${parts.hours}h`); - } - if (result.length < 2 && parts.minutes > 0) { - result.push(`${parts.minutes}m`); - } - if (result.length < 2 && parts.seconds > 0) { - result.push(`${parts.seconds}s`); - } - if (result.length < 2 && parts.milliseconds > 0 && parts.seconds < 1) { - result.push(`${parts.milliseconds.toFixed(2)}ms`); - } - - if (result.length === 0) { - // fallback for durations < 1s - return "0s"; - } - - return result.join(" "); -}; - -export const formatDate = (date, customOptions) => { - const options = { - year: "numeric", - month: "long", - day: "numeric", - hour: "numeric", - minute: "numeric", - hour12: true, - ...customOptions, - }; - - // Return the date using the specified options - return date - .toLocaleString("en-US", options) - .replace(/\b(AM|PM)\b/g, (match) => match.toLowerCase()); -}; - -export const formatDateWithTz = (timestamp, format, timezone) => { - const formattedDate = dayjs(timestamp).tz(timezone).format(format); - return formattedDate; -}; - -export const tickDateFormatLookup = (range) => { - switch (range) { - case "recent": - return "h:mm A"; - case "day": - return "h A"; - case "week": - return "MMM D"; - case "month": - return "MMM D"; - default: - return "MMM D, h A"; - } -}; - -export const tooltipDateFormatLookup = (range) => { - switch (range) { - case "recent": - return "MMM D, h:mm A"; - case "day": - return "MMM D, h:mm A"; - case "week": - return "ddd, MMM D"; - case "month": - return "ddd, MMM D"; - default: - return "MMM D, h:mm A"; - } -}; diff --git a/client/src/Utils/utils.js b/client/src/Utils/utils.js deleted file mode 100644 index 81ec44b06..000000000 --- a/client/src/Utils/utils.js +++ /dev/null @@ -1,15 +0,0 @@ -export const safelyParseFloat = (value) => { - const parsedValue = parseFloat(value); - if (isNaN(parsedValue)) { - return 0; - } - return parsedValue; -}; - -export const formatMonitorUrl = (url, maxLength = 55) => { - if (!url) return ""; - const strippedUrl = url.replace(/^https?:\/\//, ""); - return strippedUrl.length > maxLength - ? `${strippedUrl.slice(0, maxLength)}…` - : strippedUrl; -}; diff --git a/client/src/Validation/error.js b/client/src/Validation/error.js deleted file mode 100644 index 5738edf40..000000000 --- a/client/src/Validation/error.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Update errors if passed id matches the error.details[0].path, otherwise remove - * the error for the id - * @param {*} prev Previous errors * - * @param {*} id ID of the field whose error is to be either updated or removed - * @param {*} error the error object - * @returns the Update Errors with the specific field with id being either removed or updated - */ - -const buildErrors = (prev, id, error) => { - const updatedErrors = { ...prev }; - if (error && id == error.details[0].path) { - updatedErrors[id] = error.details[0].message ?? "Validation error"; - } else { - delete updatedErrors[id]; - } - return updatedErrors; -}; - -/** - * Processes Joi validation errors and returns a filtered object of error messages for fields that have been touched. - * - * @param {Object} validation - The Joi validation result object. - * @param {Object} validation.error - The error property of the validation result containing details of validation failures. - * @param {Object[]} validation.error.details - An array of error details from the Joi validation. Each item contains information about the path and the message. - * @param {Object} touchedErrors - An object representing which fields have been interacted with. Keys are field IDs (field names), and values are booleans indicating whether the field has been touched. - * @returns {Object} - An object where keys are the field IDs (if they exist in `touchedErrors` and are in the error details) and values are their corresponding error messages. - */ -const getTouchedFieldErrors = (validation, touchedErrors) => { - let newErrors = {}; - - if (validation?.error) { - newErrors = validation.error.details.reduce((errors, detail) => { - const fieldId = detail.path[0]; - if (touchedErrors[fieldId] && !(fieldId in errors)) { - errors[fieldId] = detail.message; - } - return errors; - }, {}); - } - - return newErrors; -}; -/** - * - * @param {*} form The form object of the submitted form data - * @param {*} validation The Joi validation rules - * @param {*} setErrors The function used to set the local errors - * @returns true if there is no error or false if there is error after validating the form - * the error will be reset to {} if returns false; otherwise the errors object will be set with - * the new value - */ -const hasValidationErrors = (form, validation, setErrors) => { - const { error } = validation.validate(form, { - abortEarly: false, - }); - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - if ( - ![ - "clientHost", - "refreshTokenSecret", - "dbConnectionString", - "refreshTokenTTL", - "jwtTTL", - "notify-email-list", - "_id", - "__v", - "createdAt", - "updatedAt", - ].includes(err.path[0]) - ) { - newErrors[err.path[0]] = err.message ?? "Validation error"; - } - // Handle conditionally usage number required cases - if (!form.cpu || form.usage_cpu) { - newErrors["usage_cpu"] = null; - } - if (!form.memory || form.usage_memory) { - newErrors["usage_memory"] = null; - } - if (!form.disk || form.usage_disk) { - newErrors["usage_disk"] = null; - } - if (!form.temperature || form.usage_temperature) { - newErrors["usage_temperature"] = null; - } - }); - if (Object.values(newErrors).some((v) => v)) { - setErrors(newErrors); - return true; - } else { - setErrors({}); - return false; - } - } - setErrors({}); - return false; -}; -export { buildErrors, hasValidationErrors, getTouchedFieldErrors }; diff --git a/client/src/Validation/login.ts b/client/src/Validation/login.ts index 03eb8d4e2..f14463734 100644 --- a/client/src/Validation/login.ts +++ b/client/src/Validation/login.ts @@ -2,11 +2,10 @@ import { z } from "zod"; export const loginSchema = z.object({ email: z - .string() - .min(1, "auth.common.inputs.email.errors.empty") - .email("auth.common.inputs.email.errors.invalid") + .email("Please enter a valid email address") + .min(1, "Please enter your email address") .transform((val) => val.toLowerCase().trim()), - password: z.string().min(1, "auth.common.inputs.password.errors.empty"), + password: z.string().min(1, "Please enter your password"), }); export type LoginFormData = z.infer; diff --git a/client/src/Validation/recovery.ts b/client/src/Validation/recovery.ts index 356f28738..4a244ed66 100644 --- a/client/src/Validation/recovery.ts +++ b/client/src/Validation/recovery.ts @@ -2,9 +2,8 @@ import { z } from "zod"; export const recoverySchema = z.object({ email: z - .string() - .min(1, "auth.common.inputs.email.errors.empty") - .email("auth.common.inputs.email.errors.invalid") + .email("Please enter a valid email address") + .min(1, "Please enter your email address") .transform((val) => val.toLowerCase().trim()), }); diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 27d585074..fa92f2e99 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -1,6 +1,18 @@ import joi from "joi"; import dayjs from "dayjs"; -import { ROLES } from "../Utils/roleUtils"; +export const ROLES = { + SUPERADMIN: "superadmin", + ADMIN: "admin", + USER: "user", + DEMO: "demo", +}; + +export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO]; + +export const EDITABLE_ROLES = [ + { role: ROLES.ADMIN, _id: ROLES.ADMIN }, + { role: ROLES.USER, _id: ROLES.USER }, +]; const THRESHOLD_COMMON_BASE_MSG = "Threshold must be a number."; diff --git a/client/src/assets/Images/Google.png b/client/src/assets/Images/Google.png deleted file mode 100644 index 490f4a201..000000000 Binary files a/client/src/assets/Images/Google.png and /dev/null differ diff --git a/client/src/assets/Images/avatar_placeholder.png b/client/src/assets/Images/avatar_placeholder.png deleted file mode 100644 index 38e896811..000000000 Binary files a/client/src/assets/Images/avatar_placeholder.png and /dev/null differ diff --git a/client/src/assets/Images/background-grid.svg b/client/src/assets/Images/background-grid.svg deleted file mode 100644 index 988f1da77..000000000 --- a/client/src/assets/Images/background-grid.svg +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/create-placeholder-dark.svg b/client/src/assets/Images/create-placeholder-dark.svg deleted file mode 100644 index 2dd68750b..000000000 --- a/client/src/assets/Images/create-placeholder-dark.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/create-placeholder.svg b/client/src/assets/Images/create-placeholder.svg deleted file mode 100644 index bdd9ef945..000000000 --- a/client/src/assets/Images/create-placeholder.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/data_placeholder.svg b/client/src/assets/Images/data_placeholder.svg deleted file mode 100644 index bf840b95f..000000000 --- a/client/src/assets/Images/data_placeholder.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/data_placeholder_dark.svg b/client/src/assets/Images/data_placeholder_dark.svg deleted file mode 100644 index 7e2a811d5..000000000 --- a/client/src/assets/Images/data_placeholder_dark.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/jupiter_logo_banner_dark.svg b/client/src/assets/Images/jupiter_logo_banner_dark.svg deleted file mode 100644 index 3d3e23b14..000000000 --- a/client/src/assets/Images/jupiter_logo_banner_dark.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/jupiter_logo_banner_light.svg b/client/src/assets/Images/jupiter_logo_banner_light.svg deleted file mode 100644 index 4744e0bcb..000000000 --- a/client/src/assets/Images/jupiter_logo_banner_light.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/logo_placeholder.svg b/client/src/assets/Images/logo_placeholder.svg deleted file mode 100644 index 82f307b2d..000000000 --- a/client/src/assets/Images/logo_placeholder.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/client/src/assets/Images/solana_logo_banner_dark.svg b/client/src/assets/Images/solana_logo_banner_dark.svg deleted file mode 100644 index 08ec87cb1..000000000 --- a/client/src/assets/Images/solana_logo_banner_dark.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/solana_logo_banner_light.svg b/client/src/assets/Images/solana_logo_banner_light.svg deleted file mode 100644 index 774018a6a..000000000 --- a/client/src/assets/Images/solana_logo_banner_light.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/client/src/assets/icons/discord-icon.svg b/client/src/assets/icons/discord-icon.svg deleted file mode 100644 index c03e8e127..000000000 --- a/client/src/assets/icons/discord-icon.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/client/src/assets/icons/slack-icon.svg b/client/src/assets/icons/slack-icon.svg deleted file mode 100644 index ff256fd4d..000000000 --- a/client/src/assets/icons/slack-icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/client/src/assets/icons/upt_logo.png b/client/src/assets/icons/upt_logo.png deleted file mode 100644 index d443d0897..000000000 Binary files a/client/src/assets/icons/upt_logo.png and /dev/null differ diff --git a/client/src/assets/icons/zapier-icon.svg b/client/src/assets/icons/zapier-icon.svg deleted file mode 100644 index 6ee0b5eca..000000000 --- a/client/src/assets/icons/zapier-icon.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css index 48737d4bb..a662ef475 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -18,70 +18,4 @@ html { text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - - /* Generalized Stylings */ - --env-var-radius-2: 8px; - - --env-var-width-2: 360px; - --env-var-width-4: 100px; - - --env-var-height-2: 34px; - - --env-var-nav-bar-height: 70px; - --env-var-side-bar-width: 250px; - --env-var-side-bar-collapsed-width: 64px; - --env-var-side-bar-auth-footer-height: 50px; - - --env-var-spacing-1: 12px; - --env-var-spacing-1-plus: 16px; - --env-var-spacing-1-minus: 10px; - --env-var-spacing-2: 24px; - - --env-var-font-size-small: 11px; - --env-var-font-size-small-plus: 12px; - --env-var-font-size-medium: 13px; - --env-var-font-size-medium-plus: 14px; - --env-var-font-size-large: 16px; - --env-var-font-size-large-plus: 22px; - --env-var-font-size-xlarge: 30px; -} - -.MuiInputBase-root.Mui-disabled input { - cursor: not-allowed; -} - -/* .Toastify__toast-container { - min-width: 300px; - width: auto; -} -.Toastify__toast-body .alert { - min-width: 150px; - padding: 5px 10px; - align-items: center; -} -.Toastify [class^="Toastify__toast"] { - padding: 0; - margin: 0; -} -.Toastify__toast { - min-height: 0; - border-radius: 4px; -} -.Toastify [class*="Toastify__toast-theme"] { - background-color: transparent; -} */ - -.MuiTouchRipple-root { - display: none; -} - -@keyframes ripple { - from { - opacity: 1; - transform: scale(0); - } - to { - opacity: 0; - transform: scale(2); - } } diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 3b58827ca..14081a445 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1,189 +1,4 @@ { - "aboutus": "About Us", - "access": "Access", - "actions": "Actions", - "add": "Add", - "addMonitors": "Add monitors", - "advancedMatching": "Advanced matching", - "area": "Area", - "auth": { - "common": { - "errors": { - "validation": "Error validating data." - }, - "fields": { - "role": { - "errors": { - "min": "At least one role is required" - } - } - }, - "inputs": { - "email": { - "errors": { - "empty": "To continue, please enter your email address", - "invalid": "Please recheck validity of entered email address" - }, - "label": "Email", - "placeholder": "jordan.ellis@domain.com" - }, - "firstName": { - "errors": { - "empty": "Please enter your name", - "length": "Name must be less than 50 characters", - "pattern": "Name must contain only letters, spaces, apostrophes, or hyphens" - }, - "label": "Name", - "placeholder": "Jordan" - }, - "lastName": { - "errors": { - "empty": "Please enter your surname", - "length": "Surname must be less than 50 characters", - "pattern": "Surname must contain only letters, spaces, apostrophes, or hyphens" - }, - "label": "Surname", - "placeholder": "Ellis" - }, - "password": { - "errors": { - "empty": "Please enter your password", - "length": "Password must be at least 8 characters long", - "lowercase": "Password must contain at least 1 lowercase letter", - "number": "Password must contain at least 1 number", - "special": "Password must contain at least 1 special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)", - "uppercase": "Password must contain at least 1 uppercase letter" - }, - "label": "Password", - "rules": { - "length": { - "beginning": "Must be at least", - "highlighted": "8 characters long" - }, - "lowercase": { - "beginning": "Must contain at least", - "highlighted": "one lower character" - }, - "match": { - "beginning": "Passwords", - "highlighted": "must match" - }, - "number": { - "beginning": "Must contain at least", - "highlighted": "one number" - }, - "special": { - "beginning": "Must contain at least", - "highlighted": "one special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)" - }, - "uppercase": { - "beginning": "Must contain at least", - "highlighted": "one upper character" - } - } - }, - "passwordConfirm": { - "errors": { - "different": "Entered passwords don't match, so one of them is probably mistyped", - "empty": "Please enter your password again for confirmation (helps with typos)" - }, - "label": "Confirm password", - "placeholder": "Re-enter password to confirm" - } - }, - "navigation": { - "continue": "Continue" - }, - "passwordRules": { - "length": "Must be at least 8 characters long", - "special": "Must contain at least one special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)", - "number": "Must contain at least one number", - "uppercase": "Must contain at least one upper character", - "lowercase": "Must contain at least one lower character", - "match": "Passwords must match" - } - }, - "forgotPassword": { - "buttons": { - "openEmail": "Open email app", - "resetPassword": "Reset password" - }, - "heading": "Forgot password?", - "links": { - "login": "Go back to Log In", - "resend": "Didn't receive the email? Click to resend" - }, - "subheadings": { - "stepFour": "Your password has been successfully reset. Click below to log in magically.", - "stepOne": "No worries, we'll send you reset instructions.", - "stepThree": "Your new password must be different from previously used passwords.", - "stepTwo": "We sent a password reset link to " - }, - "toasts": { - "emailNotFound": "Email not found.", - "error": "Unable to reset password. Please try again later or contact support.", - "redirect": "Redirecting in ...", - "sent": "Instructions sent to .", - "success": "Your password was reset successfully." - } - }, - "login": { - "errors": { - "password": { - "incorrect": "The password you provided does not match our records" - } - }, - "heading": "Log in to continue", - "links": { - "forgotPassword": "Forgot password?", - "forgotPasswordLink": "Reset password", - "register": "Do not have an account?", - "registerLink": "Register here" - }, - "toasts": { - "incorrectPassword": "Incorrect password", - "success": "Welcome back! You're successfully logged in." - }, - "welcome": "Welcome back to Checkmate!" - }, - "registration": { - "description": { - "superAdmin": "Create your super admin account to get started", - "user": "Sign up as a user and ask super admin for access to your monitors" - }, - "heading": { - "user": "Sign Up" - }, - "toasts": { - "success": "Welcome! Your account was created successfully." - }, - "welcome": "Welcome to Checkmate!" - } - }, - "avgCpuTemperature": "Average CPU Temperature", - "bar": "Bar", - "basicInformation": "Basic Information", - "bulkImport": { - "fallbackPage": "Import a file to upload a list of servers in bulk", - "invalidFileType": "Invalid file type", - "noFileSelected": "No file selected", - "selectFile": "Select File", - "selectFileDescription": "You can download our or sample", - "selectFileTips": "Select CSV file to upload", - "title": "Bulk Import", - "uploadFailed": "Upload failed", - "uploadSuccess": "Monitors created successfully!" - }, - "bytesSent": "Bytes Sent", - "addMember": "Add member", - "cancel": "Cancel", - "checkHooks": { - "failureResolveOne": "Failed to resolve incident." - }, - "chooseGame": "Choose game", - "city": "CITY", - "ClickUpload": "Click to upload", - "close": "Close", "common": { "auth": { "roles": { @@ -222,7 +37,8 @@ "testNotifications": "Test notifications", "toggleTheme": "Toggles light & dark", "flushQueue": "Flush queue", - "notFound": "Go to the main dashboard" + "notFound": "Go to the main dashboard", + "resetPassword": "Reset password" }, "charts": { "labels": { @@ -294,28 +110,23 @@ "interval": "Interval", "active": "Active" } - }, - "toasts": { - "checkConnection": "Please check your connection", - "networkError": "Network error", - "unknownError": "Unknown error" - }, + } + }, + "components": { "imageUpload": { "clickToUpload": "Click to upload", "dragAndDrop": "or drag and drop", "supportedFormats": "Supported formats", "maxSize": "Max size", - "orDragAndDrop": "or drag and drop" + "orDragAndDrop": "or drag and drop", + "errors": { + "invalidFileSize": "File size is too large!", + "invalidFileFormat": "Unsupported file format!" + } + }, + "headerStatusPageControls": { + "publicLink": "Public link" }, - "errors": { - "invalidFileType": "Invalid file type", - "fileTooLarge": "File too large" - } - }, - "commonSave": "Save", - "commonSaving": "Saving...", - "companyName": "Company name", - "components": { "headerTimeRange": { "labels": { "day": "Day", @@ -380,229 +191,6 @@ "description": "See the latest releases and help grow the community on GitHub" } }, - "configure": "Configure", - "confirmPassword": "Confirm password", - "cores": "Cores", - "cpu": "CPU", - "cpuFrequency": "CPU Frequency", - "cpuLogical": "CPU (Logical)", - "cpuPhysical": "CPU (Physical)", - "cpuTemperature": "CPU Temperature", - "cpuUsage": "CPU usage", - "createA": "Create a", - "createMaintenance": "Create maintenance", - "createMaintenanceWindow": "Create maintenance window", - "createMonitor": "Create monitor", - "createNew": "Create new", - "delete": "Delete", - "generateToken": "Generate token", - "DeleteAccountButton": "Remove account", - "DeleteAccountTitle": "Remove account", - "DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.", - "DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.", - "deleteStatusPage": "Do you want to delete this status page?", - "deleteStatusPageConfirm": "Yes, delete status page", - "deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.", - "DeleteWarningTitle": "Really remove this account?", - "details": "Details", - "device": "Device", - "diagnosticsPage": { - "diagnosticDescription": "System diagnostics", - "gauges": { - "heapAllocationSubtitle": "% of available memory", - "heapAllocationTitle": "Heap allocation", - "heapUsageSubtitle": "% of available memory", - "heapUsageTitle": "Heap usage", - "heapUtilizationSubtitle": "% of allocated", - "heapUtilizationTitle": "Heap utilization", - "instantCpuUsageSubtitle": "% of 1s used by CPU", - "instantCpuUsageTitle": "Instant CPU usage" - }, - "stats": { - "eventLoopDelayTitle": "Event loop delay", - "osMemoryLimitTitle": "OS Memory Limit", - "totalHeapSizeTitle": "Total heap size", - "uptimeTitle": "Uptime", - "usedHeapSizeTitle": "Used heap size" - } - }, - "disk": "Disk", - "diskUsage": "Disk Usage", - "displayName": "Display name", - "download": "Download", - "DragandDrop": "drag and drop", - "duration": "Duration", - "edit": "Edit", - "editing": "Editing...", - "editMaintenance": "Edit maintenance", - "editUserPage": { - "form": { - "email": "Email", - "firstName": "First name", - "lastName": "Last name", - "role": "Roles", - "save": "Save" - }, - "table": { - "actionHeader": "Action", - "roleHeader": "Role" - }, - "title": "Edit user", - "toast": { - "successUserUpdate": "User updated successfully", - "validationErrors": "Validation errors" - } - }, - "EmailDescriptionText": "This is your current email address — it cannot be changed.", - "errorPages": { - "serverUnreachable": { - "alertBox": "Server Connection Error", - "description": "We're unable to connect to the server. Please check your internet connection or verify your deployment configuration if the problem persists.", - "retryButton": { - "default": "Retry connection", - "processing": "Connecting..." - }, - "toasts": { - "reconnected": "Successfully reconnected to the server.", - "stillUnreachable": "Server is still unreachable. Please try again later." - } - } - }, - "errors": "Errors", - "expectedValue": "Expected value", - "failedToSendEmail": "Failed to send email", - "FirstName": "First name", - "frequency": "Frequency", - "friendlyNameInput": "Friendly name", - "friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes", - "gb": "GB", - "general": { - "noOptionsFound": "No {{unit}} found" - }, - "greeting": { - "append": "The afternoon is your playground—let's make it epic!", - "overview": "Here's an overview of your {{type}} monitors.", - "prepend": "Hey there" - }, - "high": "high", - "host": "Host", - "http": "HTTP", - "https": "HTTPS", - "incidentsPage": { - "resolveIncidentDialogCommentLabel": "Comment (optional)", - "resolveIncidentDialogCommentPlaceholder": "Add a comment about the resolution...", - "resolveIncidentDialogConfirm": "Resolve", - "resolveIncidentDialogTitle": "Resolve Incident" - }, - "integrations": "Integrations", - "integrationsDiscord": "Discord", - "integrationsDiscordInfo": "Connect with Discord and view incidents directly in a channel", - "integrationsPrism": "Connect Prism to your favorite service.", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "Connect with Slack and see incidents in a channel", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "Send all incidents to Zapier, and then see them everywhere", - "invalidFileFormat": "Unsupported file format!", - "invalidFileSize": "File size is too large!", - "inviteNoTokenFound": "No invite token found", - "LastName": "Last name", - "logsPage": { - "logLevelSelect": { - "title": "Log level", - "values": { - "all": "All", - "debug": "Debug", - "error": "Error", - "info": "Info", - "warn": "Warn" - } - }, - "noLogs": "No logs found", - "table": { - "level": "Level", - "logs": "logs", - "message": "Message", - "method": "Method", - "service": "Service", - "timestamp": "Timestamp" - }, - "tabs": { - "diagnostics": "Diagnostics", - "logs": "Server logs", - "queue": "Job queue" - }, - "title": "Logs" - }, - "low": "low", - "maintenance": "maintenance", - "maintenanceRepeat": "Maintenance Repeat", - "maintenanceTableActionMenuDialogTitle": "Do you really want to remove this maintenance window?", - "maintenanceWindowDescription": "During maintenance windows, all monitoring is suspended for selected monitors. No network checks will be performed, preventing any status updates or notifications from being triggered. Your monitors will appear frozen at their last known status, and status pages will display a maintenance indicator. Once the maintenance window ends, monitoring automatically resumes, and alerts will trigger if issues are detected. Maintenance periods do not count against uptime calculations.", - "maintenanceWindowName": "Maintenance Window Name", - "matchMethod": "Match Method", - "matchMethodOptions": { - "equal": "Equal", - "include": "Include", - "regex": "Regex" - }, - "MaxSize": "Maximum Size", - "mb": "MB", - "mem": "Mem", - "memory": "Memory", - "memoryUsage": "Memory usage", - "menu": { - "changelog": "Changelog", - "checks": "Checks", - "discussions": "Discussions", - "docs": "Docs", - "incidents": "Incidents", - "inviteMember": "Invite member", - "infrastructure": "Infrastructure", - "logOut": "Log out", - "logs": "Logs", - "maintenance": "Maintenance", - "notifications": "Notifications", - "pagespeed": "Pagespeed", - "password": "Password", - "profile": "Profile", - "settings": "Settings", - "statusPages": "Status pages", - "support": "Support", - "team": "Team", - "uptime": "Uptime" - }, - "message": "Message", - "monitor": "monitor", - "monitorHooks": { - "failureAddDemoMonitors": "Failed to add demo monitors", - "successAddDemoMonitors": "Successfully added demo monitors" - }, - "monitors": "monitors", - "monitorsToApply": "Monitors to apply maintenance window to", - "ms": "ms", - "navControls": "Controls", - "network": "Network", - "nextWindow": "Next window", - "notFoundButton": "Go to the main dashboard", - "notifications": { - "fallback": { - "actionButton": "Create notification channel!", - "checks": [ - "Alert teams about downtime or performance issues", - "Let engineers know when incidents happen", - "Keep administrators informed of system changes" - ], - "title": "A notification channel is used to:" - }, - "fetch": { - "failed": "Failed to fetch notifications" - }, - "test": { - "success": "Test notification sent successfully" - } - }, - "now": "Now", - "os": "OS", "pages": { "notFound": { "title": "Oh no! You dropped your sushi!", @@ -685,7 +273,32 @@ }, "auth": { "common": { - "passwordRules": {}, + "passwordRules": { + "length": { + "beginning": "Must be at least", + "highlighted": "8 characters long" + }, + "lowercase": { + "beginning": "Must contain at least", + "highlighted": "one lower character" + }, + "match": { + "beginning": "Passwords", + "highlighted": "must match" + }, + "number": { + "beginning": "Must contain at least", + "highlighted": "one number" + }, + "special": { + "beginning": "Must contain at least", + "highlighted": "one special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)" + }, + "uppercase": { + "beginning": "Must contain at least", + "highlighted": "one upper character" + } + }, "form": { "option": { "email": { @@ -778,7 +391,9 @@ }, "status": { "down": "down", + "breached": "breached", "initializing": "initializing", + "maintenance": "maintenance", "paused": "paused", "total": "total", "up": "up" @@ -801,7 +416,10 @@ "label": "JSONPath expression" }, "matchMethod": { - "label": "Match method" + "label": "Match method", + "equal": "Equal", + "include": "Include", + "regex": "Regex" } }, "title": "Advanced settings" @@ -810,7 +428,19 @@ "description": "How often do you want to check the status of this monitor?", "option": { "frequency": { - "label": "Check frequency" + "label": "Check frequency", + "value": { + "fifteenMinutes": "15 minutes", + "fifteenSeconds": "15 seconds", + "fiveMinutes": "5 minutes", + "fourMinutes": "4 minutes", + "oneMinute": "1 minute", + "tenMinutes": "10 minutes", + "thirtyMinutes": "30 minutes", + "thirtySeconds": "30 seconds", + "threeMinutes": "3 minutes", + "twoMinutes": "2 minutes" + } } }, "title": "Check frequency" @@ -836,6 +466,14 @@ "url": { "label": "URL", "placeholder": "https://www.google.com" + }, + "game": { + "label": "Choose game", + "placeholder": "Select a game" + }, + "port": { + "label": "Port to monitor", + "placeholder": 80 } }, "title": "General settings", @@ -887,6 +525,24 @@ "optionPort": "Port", "optionPortDescription": "Monitor if a specific port on a server is open.", "title": "Type" + }, + "thresholds": { + "title": "Alert thresholds", + "description": "Define the thresholds at which alerts should be triggered for this hardware monitor.", + "option": { + "cpuThreshold": { + "label": "CPU alert threshold (%)" + }, + "memoryThreshold": { + "label": "Memory alert threshold (%)" + }, + "diskThreshold": { + "label": "Disk alert threshold (%)" + }, + "tempThreshold": { + "label": "Temperature alert threshold (°C)" + } + } } } }, @@ -900,6 +556,15 @@ }, "incidents": { "dialog": { + "resolveIncident": { + "title": "Resolve incident", + "option": { + "comment": { + "label": "Comment (optional)", + "placeholder": "Add a comment about the resolution..." + } + } + }, "details": { "analysis": "Incident analysis", "comment": "Comment:", @@ -1374,272 +1039,5 @@ "title": "An uptime monitor is used to:" } } - }, - "pageSpeedAddApiKey": "to add your API key.", - "pageSpeedLearnMoreLink": "Click here", - "pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key yet. Without it, the PageSpeed monitor won't function.", - "passwordPanel": { - "confirmNewPassword": "Confirm new password", - "currentPassword": "Current password", - "enterCurrentPassword": "Enter your current password", - "enterNewPassword": "Enter your new password", - "newPassword": "New password", - "passwordChangedSuccess": "Your password was changed successfully.", - "passwordInputIncorrect": "Your password input was incorrect.", - "passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)." - }, - "pause": "Pause", - "PhotoDescriptionText": "This photo will be displayed in your profile page.", - "portToMonitor": "Port to monitor", - "publicLink": "Public link", - "queuePage": { - "failedJobTable": { - "failCountHeader": "Fail count", - "failedAtHeader": "Last failed at", - "failReasonHeader": "Fail reason", - "monitorIdHeader": "Monitor ID", - "monitorUrlHeader": "Monitor URL", - "title": "Failed jobs" - }, - "flushButton": "Flush queue", - "jobTable": { - "activeHeader": "Active", - "failCountHeader": "Fail count", - "idHeader": "Monitor ID", - "intervalHeader": "Interval", - "lastFinishedAtHeader": "Last finished at", - "lastRunHeader": "Last run at", - "lastRunTookHeader": "Last run took", - "lockedAtHeader": "Locked at", - "runCountHeader": "Run count", - "title": "Jobs currently in queue", - "typeHeader": "Type", - "urlHeader": "URL" - }, - "metricsTable": { - "metricHeader": "Metric", - "title": "Queue metrics", - "valueHeader": "Value" - }, - "refreshButton": "Refresh" - }, - "rate": "Rate", - "remove": "Remove", - "repeat": "Repeat", - "reset": "Reset", - "response": "RESPONSE", - "responseTime": "Response time", - "resume": "Resume", - "roles": { - "admin": "Admin", - "demoUser": "Demo user", - "superAdmin": "Super admin", - "teamMember": "Team member" - }, - "save": "Save", - "sendInvite": "Send invite", - "selectAll": "Select all", - "settingsFailedToClearStats": "Failed to clear stats", - "settingsFailedToDeleteMonitors": "Failed to delete all monitors", - "settingsFailedToSave": "Failed to save settings", - "settingsGeneralSettings": "General settings", - "settingsMonitorsDeleted": "Successfully deleted all monitors", - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "Developed by Bluewave Labs", - "title": "About" - }, - "demoMonitorsSettings": { - "buttonAddMonitors": "Add demo monitors", - "description": "Add sample monitors for demonstration purposes.", - "title": "Demo monitors" - }, - "emailSettings": { - "buttonSendTestEmail": "Send test e-mail", - "description": "Configure the email settings for your system. This is used to send notifications and alerts.", - "descriptionTransport": "This builds an SMTP transport for NodeMailer", - "labelAddress": "Email address - Used for authentication", - "labelConnectionHost": "Email connection host - Hostname to use in the HELO/EHLO greeting", - "labelHost": "Email host - Hostname or IP address to connect to", - "labelIgnoreTLS": "Disable STARTTLS: Don't use TLS even if the server supports it", - "labelPassword": "Email password - Password for authentication", - "labelPasswordSet": "Password is set. Click Reset to change it.", - "labelPool": "Enable connection pooling: Reuse existing connections to improve performance", - "labelPort": "Email port - Port to connect to", - "labelRejectUnauthorized": "Reject invalid certificates: Reject connections with self-signed or untrusted certificates", - "labelRequireTLS": "Force STARTTLS: Require TLS upgrade, fail if not supported", - "labelSecure": "Use SSL (recommended): Encrypt the connection using SSL/TLS", - "labelTLSServername": "TLS Servername - Optional Hostname for TLS Validation when host is an IP", - "labelUser": "Email user - Username for authentication, overrides email address if specified", - "linkTransport": "See specifications here", - "placeholderUser": "Leave empty if not required", - "title": "Email", - "toastEmailRequiredFieldsError": "Email address, host, port and password are required" - }, - "globalThresholds": { - "description": "Configure global CPU, Memory, Disk, and Temperature thresholds. If a value is provided, it will automatically be enabled for monitoring.", - "title": "Global Thresholds" - }, - "pageSpeedSettings": { - "description": "Enter your Google PageSpeed API key to enable Google PageSpeed monitoring. Click Reset to update the key.", - "labelApiKey": "PageSpeed API key", - "labelApiKeySet": "API key is set. Click Reset to change it.", - "title": "Google PageSpeed API key" - }, - "saveButtonLabel": "Save", - "statsSettings": { - "clearAllStatsButton": "Clear all stats", - "clearAllStatsDescription": "Clear all stats. This is irreversible.", - "clearAllStatsDialogConfirm": "Yes, clear all stats", - "clearAllStatsDialogDescription": "Once removed, the monitoring history and stats cannot be retrieved.", - "clearAllStatsDialogTitle": "Do you want to clear all stats?", - "description": "Define how long you want to retain historical data. You can also clear all existing data.", - "title": "Monitor history" - }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "Remove all monitors", - "description": "Remove all monitors from your system.", - "dialogConfirm": "Yes, remove all monitors", - "dialogTitle": "Do you want to remove all monitors?", - "title": "System reset" - }, - "timezoneSettings": { - "description": "Select the timezone used to display dates and times throughout the application.", - "title": "Display timezone" - }, - "title": "Settings", - "uiSettings": { - "chartTypeHeatmap": "Heatmap", - "chartTypeHistogram": "Histogram", - "description": "Switch between light and dark mode, or change user interface language.", - "labelChartType": "Chart type", - "labelLanguage": "Language", - "labelTheme": "Theme mode", - "title": "Appearance" - }, - "urlSettings": { - "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.", - "label": "Display IP/URL on status page", - "selectDisabled": "Disabled", - "selectEnabled": "Enabled", - "title": "Monitor IP/URL on Status Page" - } - }, - "settingsStatsCleared": "Stats cleared successfully", - "settingsSuccessSaved": "Settings saved successfully", - "settingsTestEmailFailed": "Failed to send test email", - "settingsTestEmailFailedWithReason": "Failed to send test email: {{reason}}", - "settingsTestEmailSuccess": "Test email sent successfully", - "settingsTestEmailUnknownError": "Unknown error", - "shown": "Shown", - "starPromptDescription": "See the latest releases and help grow the community on GitHub", - "starPromptTitle": "Star Checkmate", - "startTime": "Start time", - "state": "State", - "status": "Status", - "statusCode": "Status code", - "statusPage": { - "contents": "Contents", - "deleteFailed": "Failed to delete status page", - "deleteSuccess": "Status page deleted successfully", - "generalSettings": "General settings", - "details": { - "statusHeader": { - "allUp": "All systems operational", - "allDown": "All systems are experiencing issues", - "anyDown": "Some systems are experiencing issues" - } - } - }, - "statusPageCreate": { - "buttonSave": "Save" - }, - "statusPageStatusServiceStatus": "Service status", - "submit": "Submit", - "SupportedFormats": "Supported formats", - "teamPanel": { - "addTeamMember": { - "addButton": "Add Member", - "addMemberMenu": "Add Team Member", - "description": "Create a new user and share the credentials with them. This method gives the member immediate access to all monitors.", - "title": "Register new team member" - }, - "addMember": "Add member", - "cancel": "Cancel", - "changeTeamPassword": { - "changePasswordMenu": "Reset Password", - "description": "Create a new password for this team member. You will need to share the password with them securely.", - "success": "Password successfully reset. Make sure to provide the credentials to the member in a secure way.", - "title": "Reset team member password" - }, - "email": "Email", - "emailToken": "E-mail token", - "filter": { - "all": "All", - "member": "Member" - }, - "getToken": "Get token", - "inviteDescription": "When you add a new team member, they will get access to all monitors.", - "inviteLink": "Invite link", - "inviteNewTeamMember": "Invite new team member", - "inviteTeamMember": "Invite a team member", - "noMembers": "There are no team members with this role", - "register": "Register a team member", - "registerTeamMember": { - "auth": { - "common": { - "inputs": { - "role": { - "errors": { - "empty": "Role is required" - } - } - } - } - } - }, - "registerToast": { - "success": "User created, share credentials with the member securely." - }, - "role": "Role", - "selectRole": "Select role", - "table": { - "created": "Created", - "email": "Email", - "name": "Name", - "role": "Role" - }, - "teamMembers": "Team members" - }, - "time": { - "fifteenMinutes": "15 minutes", - "fifteenSeconds": "15 seconds", - "fiveMinutes": "5 minutes", - "fourMinutes": "4 minutes", - "oneMinute": "1 minute", - "tenMinutes": "10 minutes", - "thirtyMinutes": "30 minutes", - "thirtySeconds": "30 seconds", - "threeMinutes": "3 minutes", - "twoMinutes": "2 minutes" - }, - "timezone": "Timezone", - "timeZoneInfo": "All dates and times are in GMT+0 time zone.", - "title": "Title", - "total": "Total", - "type": "Type", - "update": "Update", - "uptime": "Uptime", - "url": "URL", - "used": "Used", - "window": "window", - "YourPhoto": "Profile photo", - "failedDeleteMonitor": "Failed to delete monitor", - "failedPauseMonitor": "Failed to pause monitor", - "hour": "hour", - "minute": "minute", - "monitorDeleted": "Monitor deleted successfully", - "monitorPaused": "Monitor paused successfully", - "monitorResumed": "Monitor resumed successfully", - "name": "Name" + } } diff --git a/client/src/main.tsx b/client/src/main.tsx index 813a839bd..f075886d3 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -3,9 +3,9 @@ import App from "./App.jsx"; import "./index.css"; import { BrowserRouter as Router } from "react-router-dom"; import { Provider } from "react-redux"; -import { persistor, store } from "./store.js"; +import { persistor, store } from "@/store.js"; import { PersistGate } from "redux-persist/integration/react"; -import I18nLoader from "./Components/v1/I18nLoader/index.jsx"; +import I18nLoader from "./Components/v2/i18nLoader"; import { initApiClient } from "./Utils/ApiClient.js"; initApiClient(store); diff --git a/client/src/store.js b/client/src/store.ts similarity index 59% rename from client/src/store.js rename to client/src/store.ts index 69aa3d50b..9e064b55f 100644 --- a/client/src/store.js +++ b/client/src/store.ts @@ -1,18 +1,21 @@ import { configureStore, combineReducers } from "@reduxjs/toolkit"; - -import authReducer from "./Features/Auth/authSlice"; -import uiReducer from "./Features/UI/uiSlice"; +import authReducer from "@/Features/Auth/authSlice"; +import uiReducer from "@/Features/UI/uiSlice"; import storage from "redux-persist/lib/storage"; -import { persistReducer, persistStore, createTransform } from "redux-persist"; +import { + persistReducer, + persistStore, + createTransform, + PERSIST, + REHYDRATE, +} from "redux-persist"; const authTransform = createTransform( - (inboundState) => { + (inboundState: Record) => { const { profileImage, ...rest } = inboundState; return rest; }, - // No transformation on rehydration - null, - // Only applies to auth + undefined, { whitelist: ["auth"] } ); @@ -28,6 +31,7 @@ const rootReducer = combineReducers({ ui: uiReducer, }); +// @ts-expect-error - redux-persist types don't align perfectly with redux-toolkit const persistedReducer = persistReducer(persistConfig, rootReducer); export const store = configureStore({ @@ -35,10 +39,13 @@ export const store = configureStore({ middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { - ignoredActions: ["persist/PERSIST", "persist/REHYDRATE", "persist/REGISTER"], + ignoredActions: [PERSIST, REHYDRATE, "persist/REGISTER"], }, }), }); +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; + export const persistor = persistStore(store); export default store; diff --git a/scripts/flatten-locales.js b/scripts/flatten-locales.js new file mode 100644 index 000000000..ba8d5d16c --- /dev/null +++ b/scripts/flatten-locales.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +function flattenKeys(obj, prefix = "", result = []) { + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const newKey = prefix ? `${prefix}.${key}` : key; + + if ( + typeof obj[key] === "object" && + obj[key] !== null && + !Array.isArray(obj[key]) + ) { + flattenKeys(obj[key], newKey, result); + } else { + result.push(newKey); + } + } + } + return result; +} + +// Paths +const localesDir = path.join(__dirname, "../client/src/locales"); +const inputPath = path.join(localesDir, "en.json"); +const outputPath = path.join(localesDir, "keys.json"); + +try { + const jsonContent = fs.readFileSync(inputPath, "utf8"); + const jsonData = JSON.parse(jsonContent); + + const flattenedKeys = flattenKeys(jsonData); + flattenedKeys.sort(); + + fs.writeFileSync(outputPath, JSON.stringify(flattenedKeys, null, 2), "utf8"); + + console.log(`Keys written to: ${outputPath}`); +} catch (error) { + console.error(error.message); + process.exit(1); +} diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index 7a20ed93c..8a83a0f63 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -30,7 +30,7 @@ export const initializeControllers = (services: InitializedServices): Initialize return { authController: new AuthController(services.userService), monitorController: new MonitorController(services.monitorService), - settingsController: new SettingsController(services.settingsService, services.emailService, services.db), + settingsController: new SettingsController(services.settingsService, services.emailService), checkController: new CheckController(services.checkService), inviteController: new InviteController(services.inviteService), maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService), diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 4437e9176..67c16ff6a 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -41,18 +41,6 @@ import crypto from "crypto"; import { games, GameDig } from "gamedig"; import jmespath from "jmespath"; -// DB Modules -import { GenerateAvatarImage } from "../utils/imageProcessing.js"; -import { ParseBoolean } from "../utils/utils.js"; - -// Models -import InviteToken from "../db/models/Invite.js"; -import Team from "../db/models/Team.js"; -import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; -import MonitorStats from "../db/models/MonitorStats.js"; -import NotificationModel from "../db/models/Notification.js"; -import RecoveryToken from "../db/models/RecoveryToken.js"; - // repositories import { MongoMonitorsRepository, @@ -176,7 +164,7 @@ export const initializeServices = async ({ const bufferService = new BufferService({ logger, checkService, settingsService }); - const statusService = new StatusService({ logger, buffer: bufferService, monitorsRepository }); + const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository); const webhookProvider = new WebhookProvider(logger); const slackProvider = new SlackProvider(logger); @@ -194,6 +182,7 @@ export const initializeServices = async ({ discordProvider, pagerDutyProvider, matrixProvider, + settingsService, logger ); @@ -206,6 +195,7 @@ export const initializeServices = async ({ buffer: bufferService, incidentService, maintenanceWindowsRepository, + monitorsRepository, }); const superSimpleQueue = await SuperSimpleQueue.create({ diff --git a/server/src/controllers/settingsController.ts b/server/src/controllers/settingsController.ts index 0a01daa0c..3cbfd3f6d 100644 --- a/server/src/controllers/settingsController.ts +++ b/server/src/controllers/settingsController.ts @@ -8,11 +8,9 @@ class SettingsController { static SERVICE_NAME = SERVICE_NAME; private settingsService: any; private emailService: any; - private db: any; - constructor(settingsService: any, emailService: any, db: any) { + constructor(settingsService: any, emailService: any) { this.settingsService = settingsService; this.emailService = emailService; - this.db = db; } get serviceName() { diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts index d4cc9a872..a459cdf3e 100644 --- a/server/src/db/models/Monitor.ts +++ b/server/src/db/models/Monitor.ts @@ -1,9 +1,6 @@ import { Schema, model, Types, type UpdateQuery } from "mongoose"; -import type { Monitor, MonitorMatchMethod, MonitorThresholds, CheckSnapshot } from "@/types/monitor.js"; -import { MonitorTypes } from "@/types/monitor.js"; -import Check from "./Check.js"; -import MonitorStats from "./MonitorStats.js"; -import StatusPage from "./StatusPage.js"; +import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js"; +import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js"; type CheckSnapshotDocument = Omit & { createdAt: Date }; @@ -16,7 +13,6 @@ type MonitorDocumentBase = Omit< notifications: Types.ObjectId[]; selectedDisks: string[]; matchMethod?: MonitorMatchMethod; - thresholds?: MonitorThresholds; }; interface MonitorDocument extends MonitorDocumentBase { @@ -27,16 +23,6 @@ interface MonitorDocument extends MonitorDocumentBase { updatedAt: Date; } -const thresholdsSchema = new Schema( - { - usage_cpu: { type: Number }, - usage_memory: { type: Number }, - usage_disk: { type: Number }, - usage_temperature: { type: Number }, - }, - { _id: false } -); - const checkSnapshotSchema = new Schema( { id: { type: String, required: true }, @@ -68,8 +54,9 @@ const MonitorSchema = new Schema( type: String, }, status: { - type: Boolean, - default: undefined, + type: String, + enum: MonitorStatuses, + default: "initializing", }, statusWindow: { type: [Boolean], @@ -134,36 +121,37 @@ const MonitorSchema = new Schema( secret: { type: String, }, - thresholds: { - type: thresholdsSchema, + cpuAlertThreshold: { + type: Number, + default: 100, }, - alertThreshold: { + cpuAlertCounter: { type: Number, default: 5, }, - cpuAlertThreshold: { - type: Number, - default: function () { - return this.alertThreshold; - }, - }, memoryAlertThreshold: { type: Number, - default: function () { - return this.alertThreshold; - }, + default: 100, + }, + memoryAlertCounter: { + type: Number, + default: 5, }, diskAlertThreshold: { type: Number, - default: function () { - return this.alertThreshold; - }, + default: 100, + }, + diskAlertCounter: { + type: Number, + default: 5, }, tempAlertThreshold: { type: Number, - default: function () { - return this.alertThreshold; - }, + default: 100, + }, + tempAlertCounter: { + type: Number, + default: 5, }, selectedDisks: { type: [String], @@ -191,33 +179,6 @@ const MonitorSchema = new Schema( } ); -MonitorSchema.pre("save", function (next) { - if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) { - this.cpuAlertThreshold = this.alertThreshold; - } - if (!this.memoryAlertThreshold || this.isModified("alertThreshold")) { - this.memoryAlertThreshold = this.alertThreshold; - } - if (!this.diskAlertThreshold || this.isModified("alertThreshold")) { - this.diskAlertThreshold = this.alertThreshold; - } - if (!this.tempAlertThreshold || this.isModified("alertThreshold")) { - this.tempAlertThreshold = this.alertThreshold; - } - next(); -}); - -MonitorSchema.pre("findOneAndUpdate", function (next) { - const update = this.getUpdate() as UpdateQuery | null; - if (update && !Array.isArray(update) && update.alertThreshold !== undefined) { - update.cpuAlertThreshold = update.alertThreshold; - update.memoryAlertThreshold = update.alertThreshold; - update.diskAlertThreshold = update.alertThreshold; - update.tempAlertThreshold = update.alertThreshold; - } - next(); -}); - MonitorSchema.index({ teamId: 1, type: 1 }); const MonitorModel = model("Monitor", MonitorSchema); diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 74def03c2..b75b18f13 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -11,6 +11,7 @@ import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory export interface IChecksRepository { // create + create(check: Check): Promise; createChecks(checks: Check[]): Promise; // single fetch diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index a36788e2c..c9cdbffd6 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -227,6 +227,11 @@ class MongoChecksRepository implements IChecksRepository { } as unknown as CheckDocument; }; + create = async (check: Check) => { + const savedCheck = await CheckModel.create(check); + return this.toEntity(savedCheck); + }; + createChecks = async (checks: Check[]) => { const docs = checks.map((check) => this.toDocument(check)); const inserted = await CheckModel.insertMany(docs); diff --git a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts index 08309b4ee..c718e7eb3 100644 --- a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts +++ b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts @@ -1,6 +1,7 @@ import type { MonitorStats } from "@/types/index.js"; export interface IMonitorStatsRepository { // create + create(data: Omit): Promise; // single fetch findByMonitorId(monitorId: string): Promise; // update diff --git a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts index 52dbe8aa5..7211c7d89 100644 --- a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts +++ b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts @@ -32,6 +32,11 @@ class MongoMonitorStatsRepository implements IMonitorStatsRepository { }; }; + create = async (data: Omit): Promise => { + const created = await MonitorStatsModel.create(data); + return this.toEntity(created); + }; + findByMonitorId = async (monitorId: string): Promise => { const monitorStats = await MonitorStatsModel.findOne({ monitorId: new mongoose.Types.ObjectId(monitorId) }); if (!monitorStats) { diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 9b30e5ac2..c6f3266a6 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -55,7 +55,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { query.isActive = filter === "true"; break; case "status": - query.status = filter === "true"; + query.status = filter; break; case "type": query.type = filter; @@ -181,7 +181,13 @@ class MongoMonitorsRepository implements IMonitorsRepository { { $set: { isActive: { $not: "$isActive" }, - status: "$$REMOVE", + status: { + $cond: { + if: { $eq: ["$status", "paused"] }, + then: "initializing", + else: "paused", + }, + }, }, }, ], @@ -223,17 +229,32 @@ class MongoMonitorsRepository implements IMonitorsRepository { totalMonitors: { $sum: 1 }, upMonitors: { $sum: { - $cond: [{ $eq: ["$status", true] }, 1, 0], + $cond: [{ $eq: ["$status", "up"] }, 1, 0], }, }, downMonitors: { $sum: { - $cond: [{ $eq: ["$status", false] }, 1, 0], + $cond: [{ $eq: ["$status", "down"] }, 1, 0], }, }, pausedMonitors: { $sum: { - $cond: [{ $eq: ["$isActive", false] }, 1, 0], + $cond: [{ $eq: ["$status", "paused"] }, 1, 0], + }, + }, + initializingMonitors: { + $sum: { + $cond: [{ $eq: ["$status", "initializing"] }, 1, 0], + }, + }, + maintenanceMonitors: { + $sum: { + $cond: [{ $eq: ["$status", "maintenance"] }, 1, 0], + }, + }, + breachedMonitors: { + $sum: { + $cond: [{ $eq: ["$status", "breached"] }, 1, 0], }, }, }, @@ -242,7 +263,17 @@ class MongoMonitorsRepository implements IMonitorsRepository { ]; const [summary] = await MonitorModel.aggregate(pipeline); - return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 }; + return ( + summary ?? { + totalMonitors: 0, + upMonitors: 0, + downMonitors: 0, + pausedMonitors: 0, + initializingMonitors: 0, + maintenanceMonitors: 0, + breachedMonitors: 0, + } + ); }; findGroupsByTeamId = async (teamId: string): Promise => { @@ -284,7 +315,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { teamId: toStringId(doc.teamId), name: doc.name, description: doc.description ?? undefined, - status: doc.status ?? undefined, + status: doc.status ?? "initializing", statusWindow: doc.statusWindow ?? [], statusWindowSize: doc.statusWindowSize, statusWindowThreshold: doc.statusWindowThreshold, @@ -301,12 +332,14 @@ class MongoMonitorsRepository implements IMonitorsRepository { uptimePercentage: doc.uptimePercentage ?? undefined, notifications: notificationIds, secret: doc.secret ?? undefined, - thresholds: doc.thresholds ?? undefined, - alertThreshold: doc.alertThreshold, cpuAlertThreshold: doc.cpuAlertThreshold, + cpuAlertCounter: doc.cpuAlertCounter, memoryAlertThreshold: doc.memoryAlertThreshold, + memoryAlertCounter: doc.memoryAlertCounter, diskAlertThreshold: doc.diskAlertThreshold, + diskAlertCounter: doc.diskAlertCounter, tempAlertThreshold: doc.tempAlertThreshold, + tempAlertCounter: doc.tempAlertCounter, selectedDisks: doc.selectedDisks ?? [], gameId: doc.gameId ?? undefined, group: doc.group ?? null, @@ -331,45 +364,13 @@ class MongoMonitorsRepository implements IMonitorsRepository { const notificationIds = (doc.notifications ?? []).map((notification: unknown) => toStringId(notification)); - const checks: Check[] = (doc.checks ?? []).map((check: any) => ({ - id: toStringId(check._id), - metadata: { - monitorId: toStringId(check.metadata?.monitorId), - teamId: toStringId(check.metadata?.teamId), - type: check.metadata?.type, - }, - status: check.status, - responseTime: check.responseTime, - timings: check.timings, - statusCode: check.statusCode, - message: check.message, - ack: check.ack, - ackAt: check.ackAt ?? null, - expiry: toDateString(check.expiry), - cpu: check.cpu, - memory: check.memory, - disk: check.disk, - host: check.host, - errors: check.errors, - capture: check.capture, - net: check.net, - accessibility: check.accessibility, - bestPractices: check.bestPractices, - seo: check.seo, - performance: check.performance, - audits: check.audits, - __v: check.__v, - createdAt: toDateString(check.createdAt), - updatedAt: toDateString(check.updatedAt), - })); - return { id: toStringId(doc._id), userId: toStringId(doc.userId), teamId: toStringId(doc.teamId), name: doc.name, description: doc.description ?? undefined, - status: doc.status ?? undefined, + status: doc.status ?? "initializing", statusWindow: doc.statusWindow ?? [], statusWindowSize: doc.statusWindowSize, statusWindowThreshold: doc.statusWindowThreshold, @@ -386,12 +387,14 @@ class MongoMonitorsRepository implements IMonitorsRepository { uptimePercentage: doc.uptimePercentage ?? undefined, notifications: notificationIds, secret: doc.secret ?? undefined, - thresholds: doc.thresholds ?? undefined, - alertThreshold: doc.alertThreshold, cpuAlertThreshold: doc.cpuAlertThreshold, + cpuAlertCounter: doc.cpuAlertCounter, memoryAlertThreshold: doc.memoryAlertThreshold, + memoryAlertCounter: doc.memoryAlertCounter, diskAlertThreshold: doc.diskAlertThreshold, + diskAlertCounter: doc.diskAlertCounter, tempAlertThreshold: doc.tempAlertThreshold, + tempAlertCounter: doc.tempAlertCounter, selectedDisks: doc.selectedDisks ?? [], gameId: doc.gameId ?? undefined, group: doc.group ?? null, diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 8116c893a..bcb17d3a5 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -4,6 +4,7 @@ import { AppError } from "@/utils/AppError.js"; import { ParseBoolean } from "@/utils/utils.js"; import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js"; import type { Incident } from "@/types/index.js"; +import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; const dateRangeLookup: Record = { recent: new Date(new Date().setHours(new Date().getHours() - 2)), @@ -43,10 +44,14 @@ class IncidentService { return IncidentService.SERVICE_NAME; } - handleIncident = async (monitor: Monitor, code: number): Promise => { + handleIncident = async (monitor: Monitor, code: number, decision: MonitorActionDecision): Promise => { + if (!decision.shouldCreateIncident && !decision.shouldResolveIncident) { + return null; + } + const activeIncident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId); - // Monitor is down, create an incident - if (monitor.status === false) { + + if (decision.shouldCreateIncident) { if (activeIncident) { return activeIncident; } else { @@ -61,14 +66,17 @@ class IncidentService { } } - // Monitor is up, resolve active incidents - if (!activeIncident) { - return null; + if (decision.shouldResolveIncident) { + if (!activeIncident) { + return null; + } + activeIncident.status = false; + activeIncident.endTime = Date.now().toString(); + activeIncident.resolutionType = "automatic"; + return await this.incidentsRepository.updateById(activeIncident.id, activeIncident.teamId, activeIncident); } - activeIncident.status = false; - activeIncident.endTime = Date.now().toString(); - activeIncident.resolutionType = "automatic"; - return await this.incidentsRepository.updateById(activeIncident.id, activeIncident.teamId, activeIncident); + + return null; }; resolveIncident = async (incidentId: string, userId: string, teamId: string, comment?: string, userEmail?: string) => { diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index cf3824d60..274318ad7 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -2,8 +2,23 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js"; +import type { StatusChangeResult, MonitorStatusResponse, HardwareStatusPayload, MonitorStatus } from "@/types/index.js"; import IncidentService from "@/service/business/incidentService.js"; -import { IMaintenanceWindowsRepository } from "@/repositories/index.js"; +import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js"; + +export interface MonitorActionDecision { + shouldCreateIncident: boolean; + shouldResolveIncident: boolean; + shouldSendNotification: boolean; + incidentReason: "status_down" | "threshold_breach" | null; + notificationReason: "status_change" | "threshold_breach" | null; + thresholdBreaches?: { + cpu?: boolean; + memory?: boolean; + disk?: boolean; + temp?: boolean; + }; +} class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; @@ -16,6 +31,7 @@ class SuperSimpleQueueHelper { private buffer: any; private incidentService: IncidentService; private maintenanceWindowsRepository: IMaintenanceWindowsRepository; + private monitorsRepository: IMonitorsRepository; constructor({ logger, @@ -26,6 +42,7 @@ class SuperSimpleQueueHelper { buffer, incidentService, maintenanceWindowsRepository, + monitorsRepository, }: { logger: any; networkService: INetworkService; @@ -35,6 +52,7 @@ class SuperSimpleQueueHelper { buffer: any; incidentService: IncidentService; maintenanceWindowsRepository: IMaintenanceWindowsRepository; + monitorsRepository: IMonitorsRepository; }) { this.logger = logger; this.networkService = networkService; @@ -44,6 +62,7 @@ class SuperSimpleQueueHelper { this.notificationsService = notificationsService; this.incidentService = incidentService; this.maintenanceWindowsRepository = maintenanceWindowsRepository; + this.monitorsRepository = monitorsRepository; } get serviceName() { @@ -59,7 +78,8 @@ class SuperSimpleQueueHelper { throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getMonitorJob" }); } - // Step 1. Check for maintenacne window, if found, skip the check + // Step 1. Check for maintenance window, if found, skip the check + const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId); if (maintenanceWindowActive) { this.logger.debug({ @@ -67,6 +87,9 @@ class SuperSimpleQueueHelper { service: SERVICE_NAME, method: "getMonitorJob", }); + if (monitor.status !== "maintenance") { + await this.monitorsRepository.updateById(monitorId, teamId, { status: "maintenance" }); + } return; } @@ -78,17 +101,26 @@ class SuperSimpleQueueHelper { // Step 3. Build check const check = await this.checkService.buildCheck(status); - + if (!check) { + this.logger.warn({ + message: `No check could be built for monitor ${monitorId}`, + service: SERVICE_NAME, + method: "getMonitorJob", + details: { code: status.code, message: status.message }, + }); + return; + } // Step 4 Add check to buffer this.buffer.addToBuffer({ check }); - // Step 4. Update monitor status const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); - // Step 5 handle notifications (best effort, continue even in event of failure, don't wait) - this.notificationsService - .handleNotifications(statusChangeResult.monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged) - .catch((error: any) => { + // Step 5. Get decisions + const decision = this.evaluateMonitorAction(statusChangeResult); + + // Step 6. Handle notifications (best effort, continue even in event of failure, don't wait) + if (decision.shouldSendNotification) { + this.notificationsService.handleNotifications(statusChangeResult.monitor, status, decision).catch((error: any) => { this.logger.error({ message: error.message, service: SERVICE_NAME, @@ -97,19 +129,18 @@ class SuperSimpleQueueHelper { stack: error.stack, }); }); - - // Step 6. Handle incidents (best effort, don't wait) - if (statusChangeResult.statusChanged) { - this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code).catch((error: any) => { - this.logger.warn({ - message: error.message, - service: SERVICE_NAME, - method: "getMonitorJob", - details: `Error handling incident for job ${monitor.id}: ${error.message}`, - stack: error.stack, - }); - }); } + + // Step 7. Handle incidents (best effort, don't wait) + this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision).catch((error: any) => { + this.logger.warn({ + message: error.message, + service: SERVICE_NAME, + method: "getMonitorJob", + details: `Error handling incident for job ${monitor.id}: ${error.message}`, + stack: error.stack, + }); + }); } catch (error: any) { this.logger.warn({ message: error.message, @@ -151,6 +182,45 @@ class SuperSimpleQueueHelper { }, false); return maintenanceWindowIsActive; } + + private evaluateMonitorAction(statusChangeResult: StatusChangeResult): MonitorActionDecision { + const { monitor, statusChanged, prevStatus } = statusChangeResult; + + // Initialize result + const decision: MonitorActionDecision = { + shouldCreateIncident: false, + shouldResolveIncident: false, + shouldSendNotification: false, + incidentReason: null, + notificationReason: null, + }; + + // Simplified logic: Just check status changes + if (!statusChanged) { + return decision; + } + + if (monitor.status === "down") { + // Monitor went down (unreachable) + decision.shouldCreateIncident = true; + decision.shouldSendNotification = true; + decision.incidentReason = "status_down"; + decision.notificationReason = "status_change"; + } else if (monitor.status === "breached") { + // Hardware monitor exceeded thresholds + decision.shouldCreateIncident = true; + decision.shouldSendNotification = true; + decision.incidentReason = "threshold_breach"; + decision.notificationReason = "threshold_breach"; + } else if (monitor.status === "up" && (prevStatus === "down" || prevStatus === "breached")) { + // Monitor recovered from down or breached state + decision.shouldResolveIncident = true; + decision.shouldSendNotification = true; + decision.notificationReason = "status_change"; + } + + return decision; + } } export default SuperSimpleQueueHelper; diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index 461001800..415c22281 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -2,7 +2,14 @@ import type { Check } from "@/types/index.js"; const SERVICE_NAME = "BufferService"; -class BufferService { +export interface IBufferService { + addToBuffer(check: Check): void; + removeCheckFromBuffer(check: Check): boolean; + scheduleNextFlush(): void; + flushBuffer(): Promise; +} + +class BufferService implements IBufferService { static SERVICE_NAME = SERVICE_NAME; private BUFFER_TIMEOUT: number; private logger: any; @@ -13,7 +20,6 @@ class BufferService { constructor({ logger, checkService, settingsService }: { logger: any; checkService: any; settingsService: any }) { this.BUFFER_TIMEOUT = settingsService.getSettings().nodeEnv === "development" ? 10 : 1000 * 60 * 1; // 1 minute - console.log(this.BUFFER_TIMEOUT); this.logger = logger; this.checksService = checkService; this.SERVICE_NAME = SERVICE_NAME; @@ -30,7 +36,7 @@ class BufferService { return BufferService.SERVICE_NAME; } - addToBuffer({ check }: { check: Check }) { + addToBuffer(check: Check) { try { this.buffer.push(check); } catch (error: any) { diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts index 2d363d6b3..21a221c9c 100644 --- a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -1,6 +1,13 @@ import type { Monitor, Notification, Alert, MonitorStatusResponse } from "@/types/index.js"; +import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; export interface INotificationProvider { - sendAlert: (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Promise; + sendAlert: ( + notification: Notification, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision, + clientHost: string + ) => Promise; sendTestAlert(notification: Notification): Promise; } diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index ad1f2f28a..560c18aae 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -1,6 +1,7 @@ const SERVICE_NAME = "DiscordProvider"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import { buildHardwareAlerts, buildDiscordBody, getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import got from "got"; @@ -10,15 +11,31 @@ export class DiscordProvider implements INotificationProvider { constructor(logger: any) { this.logger = logger; } - private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - const { discordPayload } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + private getHardwareContent = ( + clientHost: string, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision + ) => { + // For status changes (recovery), use standard format + if (decision.notificationReason === "status_change") { + return buildDiscordBody(monitor, monitorStatusResponse); + } + // For threshold breaches, use hardware alert format + const { discordPayload } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse); return discordPayload; }; - sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + sendAlert = async ( + notification: Notification, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision, + clientHost: string + ) => { let body; if (monitor.type === "hardware") { - body = this.getHardwareContent(monitor, monitorStatusResponse); + body = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision); } else { body = buildDiscordBody(monitor, monitorStatusResponse); } diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index 32ccf1859..fd15034d1 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -1,6 +1,7 @@ const SERVICE_NAME = "EmailProvider"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import { buildHardwareAlerts, buildHardwareEmail, buildEmail, buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js"; export class EmailProvider implements INotificationProvider { @@ -12,21 +13,48 @@ export class EmailProvider implements INotificationProvider { this.logger = logger; } - private buildHardwareEmail = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + private buildHardwareEmail = async ( + clientHost: string, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision + ) => { + // For status changes (recovery), use standard email format + if (decision.notificationReason === "status_change") { + return await buildEmail(this.emailService, monitor); + } + // For threshold breaches, use hardware alert format + const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse); const html = buildHardwareEmail(this.emailService, monitor, alertsToSend); return html; }; - async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + async sendAlert( + notification: Notification, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision, + clientHost: string + ): Promise { // For grouped notifications (identified by ":" in name), customize subject to indicate multiple services. // Example: "2 services: Service A, Service B" becomes "Alert: 2 services are down" const isGroupedNotification = monitor.name.includes(":"); - const subject = isGroupedNotification ? `Alert: ${monitor.name} are down` : `Monitor ${monitor.name} is down`; + + // Build subject based on notification reason and monitor status + let subject: string; + if (isGroupedNotification) { + subject = `Alert: ${monitor.name} are down`; + } else if (decision.notificationReason === "threshold_breach") { + subject = `Monitor ${monitor.name} threshold breached`; + } else if (monitor.status === "up") { + subject = `Monitor ${monitor.name} is back up`; + } else { + subject = `Monitor ${monitor.name} is down`; + } let html; if (monitor.type === "hardware") { - html = this.buildHardwareEmail(monitor, monitorStatusResponse); + html = await this.buildHardwareEmail(clientHost, monitor, monitorStatusResponse, decision); } else { html = await buildEmail(this.emailService, monitor); } diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts index 8914dbf80..f2543886d 100644 --- a/server/src/service/infrastructure/notificationProviders/matrix.ts +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -1,10 +1,11 @@ const SERVICE_NAME = "MatrixProvider"; import got from "got"; import type { INotificationProvider } from "@/service/index.js"; +import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js"; import { buildHardwareAlerts, - buildHardwareWebhookBody, + buildHardwareNotificationMessage, buildWebhookBody, getTestMessage, } from "@/service/infrastructure/notificationProviders/utils.js"; @@ -15,9 +16,19 @@ export class MatrixProvider implements INotificationProvider { constructor(logger: any) { this.logger = logger; } - private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); - const body = buildHardwareWebhookBody(alertsToSend, monitor); + private getHardwareContent = ( + clientHost: string, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision + ) => { + // For status changes (recovery), use standard format + if (decision.notificationReason === "status_change") { + return buildWebhookBody(monitor, monitorStatusResponse); + } + // For threshold breaches, use hardware alert format + const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse); + const body = buildHardwareNotificationMessage(clientHost, alertsToSend, monitor); return body; }; @@ -26,12 +37,18 @@ export class MatrixProvider implements INotificationProvider { return body; }; - sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + sendAlert = async ( + notification: Notification, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision, + clientHost: string + ) => { const { homeserverUrl, accessToken, roomId } = notification; let content; if (monitor.type === "hardware") { - content = this.getHardwareContent(monitor, monitorStatusResponse); + content = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision); } else { content = this.getContent(monitor, monitorStatusResponse); } diff --git a/server/src/service/infrastructure/notificationProviders/pagerduty.ts b/server/src/service/infrastructure/notificationProviders/pagerduty.ts index f65e50c68..291cd7de5 100644 --- a/server/src/service/infrastructure/notificationProviders/pagerduty.ts +++ b/server/src/service/infrastructure/notificationProviders/pagerduty.ts @@ -1,10 +1,11 @@ const SERVICE_NAME = "PagerDutyProvider"; import got from "got"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import { INotificationProvider } from "@/service/index.js"; import { buildHardwareAlerts, - buildHardwareWebhookBody, + buildHardwareNotificationMessage, buildWebhookBody, getTestMessage, } from "@/service/infrastructure/notificationProviders/utils.js"; @@ -15,9 +16,19 @@ export class PagerDutyProvider implements INotificationProvider { constructor(logger: any) { this.logger = logger; } - private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); - const body = buildHardwareWebhookBody(alertsToSend, monitor); + private getHardwareContent = ( + clientHost: string, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision + ) => { + // For status changes (recovery), use standard format + if (decision.notificationReason === "status_change") { + return buildWebhookBody(monitor, monitorStatusResponse); + } + // For threshold breaches, use hardware alert format + const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse); + const body = buildHardwareNotificationMessage(clientHost, alertsToSend, monitor); return body; }; @@ -26,10 +37,16 @@ export class PagerDutyProvider implements INotificationProvider { return body; }; - async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + async sendAlert( + notification: Notification, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision, + clientHost: string + ): Promise { let body; if (monitor.type === "hardware") { - body = this.getHardwareContent(monitor, monitorStatusResponse); + body = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision); } else { body = this.getContent(monitor, monitorStatusResponse); } diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index 2aeb442eb..f99af48d4 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -1,9 +1,10 @@ const SERVICE_NAME = "SlackProvider"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import { buildHardwareAlerts, - buildHardwareWebhookBody, + buildHardwareNotificationMessage, buildWebhookBody, getTestMessage, } from "@/service/infrastructure/notificationProviders/utils.js"; @@ -15,9 +16,19 @@ export class SlackProvider implements INotificationProvider { constructor(logger: any) { this.logger = logger; } - private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); - const body = buildHardwareWebhookBody(alertsToSend, monitor); + private getHardwareContent = ( + clientHost: string, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision + ) => { + // For status changes (recovery), use standard format + if (decision.notificationReason === "status_change") { + return buildWebhookBody(monitor, monitorStatusResponse); + } + // For threshold breaches, use hardware alert format + const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse); + const body = buildHardwareNotificationMessage(clientHost, alertsToSend, monitor); return body; }; @@ -26,10 +37,16 @@ export class SlackProvider implements INotificationProvider { return body; }; - async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + async sendAlert( + notification: Notification, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision, + clientHost: string + ): Promise { let body; if (monitor.type === "hardware") { - body = this.getHardwareContent(monitor, monitorStatusResponse); + body = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision); } else { body = this.getContent(monitor, monitorStatusResponse); } diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index f71933425..72797c1d0 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -23,17 +23,26 @@ export const buildHardwareAlerts = ( monitor: Monitor, networkResponse: MonitorStatusResponse ): { alertsToSend: string[]; discordPayload: any } => { - const thresholds = monitor.thresholds || {}; - const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; + // Thresholds are stored as percentages (0-100), convert to decimal (0-1) for comparison + const cpuThreshold = monitor.cpuAlertThreshold !== undefined ? monitor.cpuAlertThreshold / 100 : -1; + const memoryThreshold = monitor.memoryAlertThreshold !== undefined ? monitor.memoryAlertThreshold / 100 : -1; + const diskThreshold = monitor.diskAlertThreshold !== undefined ? monitor.diskAlertThreshold / 100 : -1; + const tempThreshold = monitor.tempAlertThreshold !== undefined ? monitor.tempAlertThreshold : -1; const payload = networkResponse?.payload as HardwareStatusPayload; const metrics = payload.data || {}; - const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; + const { cpu = {}, memory = {}, disk = [] } = metrics; + const cpuUsage = cpu.usage_percent ?? -1; + const memoryUsage = memory.usage_percent ?? -1; + // Get max temperature from CPU temperature sensors array + const temps = cpu.temperature ?? []; + const maxTemp = temps.length > 0 ? Math.max(...temps) : -1; const alerts: Record = { cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false, + temp: tempThreshold !== -1 && maxTemp > tempThreshold ? true : false, }; const alertsToSend = []; @@ -43,6 +52,7 @@ export const buildHardwareAlerts = ( { name: "URL", value: monitor.url, inline: false }, ]; const goToIncidentField = { name: `Go to incident`, value: `${clientHost}/infrastructure/${monitor.id}` }; + const formatDiscordAlert = { cpu: () => ({ title: "CPU alert", @@ -75,78 +85,42 @@ export const buildHardwareAlerts = ( goToIncidentField, ], }), + + temp: () => ({ + title: "Temperature alert", + description: `Your current temperature (${maxTemp.toFixed(0)}°C) is above your threshold (${tempThreshold.toFixed(0)}°C)`, + color: 15548997, + fields: [...monitorInfoFields, goToIncidentField], + footer: { text: "Checkmate" }, + }), }; - const alertTypes = ["cpu", "memory", "disk"] as const; - const alertThresholdKeyMap: Record<(typeof alertTypes)[number], "cpuAlertThreshold" | "memoryAlertThreshold" | "diskAlertThreshold"> = { - cpu: "cpuAlertThreshold", - memory: "memoryAlertThreshold", - disk: "diskAlertThreshold", - }; + + const alertTypes = ["cpu", "memory", "disk", "temp"] as const; + for (const type of alertTypes) { - const thresholdKey = alertThresholdKeyMap[type]; - // Iterate over each alert type to see if any need to be decremented if (alerts[type] === true) { - const nextValue = ((monitor[thresholdKey] ?? monitor.alertThreshold) as number) - 1; - monitor[thresholdKey] = nextValue; // Decrement threshold if an alert is triggered - - if (monitor[thresholdKey] <= 0) { - // If threshold drops below 0, reset and send notification - monitor[thresholdKey] = monitor.alertThreshold; - - const formatAlert = { - cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, - memory: () => - `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, - disk: () => - `Your current disk usage: ${disk - .map((d, idx) => `(Disk${idx}: ${(d?.usage_percent ?? 0 * 100).toFixed(0)}%)`) - .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, - }; - alertsToSend.push(formatAlert[type]()); - discordEmbeds.push(formatDiscordAlert[type]()); - } + const formatAlert = { + cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, + memory: () => + `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, + disk: () => + `Your current disk usage: ${disk.map((d, idx) => `(Disk${idx}: ${(d?.usage_percent ?? 0 * 100).toFixed(0)}%)`).join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, + temp: () => `Your current temperature (${maxTemp.toFixed(0)}°C) is above your threshold (${tempThreshold.toFixed(0)}°C)`, + }; + alertsToSend.push(formatAlert[type]()); + discordEmbeds.push(formatDiscordAlert[type]()); } } + const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null; return { alertsToSend, discordPayload }; }; -export const buildHardwareNotificationMessage = (clientHost: string, alerts: any, monitor: Monitor) => { +export const buildHardwareNotificationMessage = (clientHost: string, alerts: string[], monitor: Monitor): string => { const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`]; const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor.id}`]; const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts, ...alertFooter] : []; - return alertText.map((alert) => alert).join("\n"); -}; - -export const buildHardwareWebhookBody = (alerts: string[], monitor: Monitor): string => { - const content = alerts.map((alert) => alert).join("\n"); - return content; -}; - -export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: MonitorStatusResponse): boolean => { - const thresholds = monitor.thresholds || {}; - const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; - - const payload = networkResponse?.payload as HardwareStatusPayload; - const metrics = payload.data || {}; - const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; - - const cpuBreach = cpuThreshold !== -1 && cpuUsage > cpuThreshold; - if (cpuBreach && ((monitor.cpuAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { - return true; - } - - const memoryBreach = memoryThreshold !== -1 && memoryUsage > memoryThreshold; - if (memoryBreach && ((monitor.memoryAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { - return true; - } - - const diskBreach = disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold); - if (diskBreach && ((monitor.diskAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { - return true; - } - - return false; + return alertText.join("\n"); }; export const buildWebhookBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { @@ -163,7 +137,7 @@ export const buildHardwareEmail = async (emailService: any, monitor: Monitor, al }; export const buildEmail = async (emailService: any, monitor: Monitor): Promise => { - const template = monitor.status === true ? "serverIsUpTemplate" : "serverIsDownTemplate"; + const template = monitor.status === "up" ? "serverIsUpTemplate" : "serverIsDownTemplate"; const context = { monitor: monitor.name, url: monitor.url }; const html = await emailService.buildEmail(template, context); return html; diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index 9e743ae53..17d871950 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -1,9 +1,10 @@ const SERVICE_NAME = "WebhookProvider"; import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import { buildHardwareAlerts, - buildHardwareWebhookBody, + buildHardwareNotificationMessage, buildWebhookBody, getTestMessage, } from "@/service/infrastructure/notificationProviders/utils.js"; @@ -15,9 +16,19 @@ export class WebhookProvider implements INotificationProvider { constructor(logger: any) { this.logger = logger; } - private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); - const body = buildHardwareWebhookBody(alertsToSend, monitor); + private getHardwareContent = ( + clientHost: string, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision + ) => { + // For status changes (recovery), use standard format + if (decision.notificationReason === "status_change") { + return buildWebhookBody(monitor, monitorStatusResponse); + } + // For threshold breaches, use hardware alert format + const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse); + const body = buildHardwareNotificationMessage(clientHost, alertsToSend, monitor); return body; }; @@ -26,10 +37,16 @@ export class WebhookProvider implements INotificationProvider { return body; }; - sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + sendAlert = async ( + notification: Notification, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision, + clientHost: string + ) => { let body; if (monitor.type === "hardware") { - body = this.getHardwareContent(monitor, monitorStatusResponse); + body = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision); } else { body = this.getContent(monitor, monitorStatusResponse); } diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 78b1dc8ab..5f057f0c5 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,19 +1,17 @@ -import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; -import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; +import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification, MonitorStatus } from "@/types/index.js"; import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js"; import { INotificationProvider } from "./notificationProviders/INotificationProvider.js"; +import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import type { ISettingsService } from "@/service/system/settingsService.js"; +import { ILogger } from "@/utils/logger.js"; + export interface INotificationsService { createNotification: (notificationData: Partial) => Promise; findById: (id: string, teamId: string) => Promise; findNotificationsByTeamId: (teamId: string) => Promise; updateById(id: string, teamId: string, updateData: Partial): Promise; deleteById: (id: string, teamId: string) => Promise; - handleNotifications: ( - monitor: Monitor, - monitorStatusResponse: MonitorStatusResponse, - prevStatus: boolean | undefined, - statusChanged: boolean - ) => Promise; + handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => Promise; sendTestNotification: (notification: Notification) => Promise; testAllNotifications: (notificationIds: string[]) => Promise; @@ -32,19 +30,8 @@ export class NotificationsService implements INotificationsService { private discordProvider: INotificationProvider; private pagerDutyProvider: INotificationProvider; private matrixProvider: INotificationProvider; - private logger: any; - - // Email grouping (batching) configuration - private emailGroupingWindowMs: number; - private pendingEmailGroups: Map< - string, - { - monitors: Monitor[]; - statusResponses: MonitorStatusResponse[]; - timer: ReturnType; - createdAt: number; - } - >; + private logger: ILogger; + private settingsService: ISettingsService; constructor( notificationsRepository: INotificationsRepository, @@ -55,6 +42,7 @@ export class NotificationsService implements INotificationsService { discordProvider: INotificationProvider, pagerDutyProvider: INotificationProvider, matrixProvider: INotificationProvider, + settingsService: ISettingsService, logger: any ) { this.notificationsRepository = notificationsRepository; @@ -65,57 +53,42 @@ export class NotificationsService implements INotificationsService { this.discordProvider = discordProvider; this.pagerDutyProvider = pagerDutyProvider; this.matrixProvider = matrixProvider; + this.settingsService = settingsService; this.logger = logger; - - // Configure email grouping window (in milliseconds). - // When > 0, multiple DOWN events for monitors that share the same - // email notification within this window will be batched into a single email. - const rawGroupingWindow = process.env.NOTIFICATION_GROUP_WINDOW_MS ?? process.env.NOTIFICATION_GROUP_WINDOW_SECONDS; - let groupingWindowMs = 0; - if (rawGroupingWindow) { - const parsed = Number(rawGroupingWindow); - if (!Number.isNaN(parsed) && parsed > 0) { - // If value looks like seconds (small number), convert to ms. - // This allows either milliseconds (e.g. 60000) or seconds (e.g. 60). - groupingWindowMs = parsed <= 300 ? parsed * 1000 : parsed; - } - } - this.emailGroupingWindowMs = groupingWindowMs; - this.pendingEmailGroups = new Map(); } - private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise => { + private send = async ( + notification: Notification, + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + decision: MonitorActionDecision + ): Promise => { + const settings = this.settingsService.getSettings(); + const clientHost = settings.clientHost || "Host not defined"; + switch (notification.type) { case "email": - return await this.emailProvider.sendAlert(notification, monitor, monitorStatusResponse); + return await this.emailProvider.sendAlert(notification, monitor, monitorStatusResponse, decision, clientHost); case "slack": - return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse); + return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse, decision, clientHost); case "discord": - return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse); + return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse, decision, clientHost); case "pager_duty": - return await this.pagerDutyProvider.sendAlert(notification, monitor, monitorStatusResponse); + return await this.pagerDutyProvider.sendAlert(notification, monitor, monitorStatusResponse, decision, clientHost); case "matrix": - return await this.matrixProvider.sendAlert(notification, monitor, monitorStatusResponse); + return await this.matrixProvider.sendAlert(notification, monitor, monitorStatusResponse, decision, clientHost); case "webhook": - return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse); + return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse, decision, clientHost); default: return false; } }; - private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => { const notificationIds = monitor.notifications ?? []; const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); - const tasks = notifications.map((notification) => { - // Only group emails, only for DOWN transitions, and only if a window is configured. - if (notification.type === "email" && this.emailGroupingWindowMs > 0 && monitorStatusResponse.status === false) { - return this.queueGroupedEmailNotification(notification, monitor, monitorStatusResponse); - } - - // For all other cases (UP notifications or non-email channels), send immediately. - return this.send(notification, monitor, monitorStatusResponse); - }); + const tasks = notifications.map((notification) => this.send(notification, monitor, monitorStatusResponse, decision)); const outcomes = await Promise.all(tasks); const succeeded = outcomes.filter(Boolean).length; @@ -124,141 +97,21 @@ export class NotificationsService implements INotificationsService { this.logger.warn({ message: `Notification send completed with ${succeeded} success, ${failed} failure(s)`, service: SERVICE_NAME, - method: "getMonitorJob", + method: "sendNotifications", }); } - // Return true if all notificaitons succeeded + // Return true if all notifications succeeded return succeeded === notifications.length; }; - /** - * Queue a DOWN email notification to be potentially grouped with other - * DOWN events for the same email notification within the configured window. - * - * This method returns immediately; the actual email is sent asynchronously - * when the grouping window expires. - */ - private queueGroupedEmailNotification = async ( - notification: Notification, - monitor: Monitor, - monitorStatusResponse: MonitorStatusResponse - ): Promise => { - // If grouping is disabled, fallback to immediate send. - if (this.emailGroupingWindowMs <= 0) { - return await this.send(notification, monitor, monitorStatusResponse); - } - - const key = notification.id; - const now = Date.now(); - const existingGroup = this.pendingEmailGroups.get(key); - - if (!existingGroup) { - // Create a new group and schedule a flush after the window expires. - const timer = setTimeout(async () => { - const group = this.pendingEmailGroups.get(key); - if (!group) return; - - this.pendingEmailGroups.delete(key); - - try { - await this.flushEmailGroup(notification, group.monitors, group.statusResponses); - } catch (error: any) { - this.logger.error({ - message: error?.message, - service: SERVICE_NAME, - method: "flushEmailGroup", - stack: error?.stack, - }); - } - }, this.emailGroupingWindowMs); - - this.pendingEmailGroups.set(key, { - monitors: [monitor], - statusResponses: [monitorStatusResponse], - timer, - createdAt: now, - }); - } else { - // Append to existing group. - existingGroup.monitors.push(monitor); - existingGroup.statusResponses.push(monitorStatusResponse); - } - - // Consider queueing as "succeeded" from the caller's perspective. - return true; - }; - - /** - * Flush a grouped set of DOWN events into a single email. - * - * To avoid changing email templates, we construct a synthetic Monitor - * whose name concisely lists all affected services. The existing - * `serverIsDownTemplate` is then reused. - * - * @param notification The email notification to send to - * @param monitors Array of monitors that went down - * @param statusResponses Array of status responses (parallel to monitors) - * @returns true if email was sent successfully, false otherwise - */ - private flushEmailGroup = async (notification: Notification, monitors: Monitor[], statusResponses: MonitorStatusResponse[]): Promise => { - if (!monitors.length || !statusResponses.length) { + handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => { + // Early return if no notification should be sent + if (!decision.shouldSendNotification) { return false; } - // Build a combined monitor name listing all affected services. - // Example: "Service A, Service B" (2 services) or "Service A" (1 service) - const uniqueNames = Array.from(new Set(monitors.map((m) => m.name))); - const servicesCount = uniqueNames.length; - const servicesList = uniqueNames.join(", "); - - const combinedName = servicesCount === 1 ? servicesList : `${servicesCount} services: ${servicesList}`; - - // Use the first monitor as a base for URL and other fields. - const baseMonitor = monitors[0]!; - const baseStatus = statusResponses[0]!; - - // Create a shallow clone so we don't mutate the original entity. - // This preserves monitor properties while overriding the name for grouped display. - const syntheticMonitor: Monitor = { - ...baseMonitor, - name: combinedName, - }; - - // Reuse existing email provider to send grouped notification. - return await this.emailProvider.sendAlert(notification, syntheticMonitor, baseStatus); - }; - - handleNotifications = async ( - monitor: Monitor, - monitorStatusResponse: MonitorStatusResponse, - prevStatus: boolean | undefined, - statusChanged: boolean - ) => { - const { type } = monitor; - const payload = monitorStatusResponse.payload as HardwareStatusPayload; - // If this is a non-hardeware type monitor and status did not change, we're done - if (type !== "hardware" && statusChanged === false) return false; - // if prevStatus is undefined, monitor is resuming, we're done - if (type !== "hardware" && prevStatus === undefined) return false; - - // Deal with hardware thresholds - if (type === "hardware") { - const thresholds = monitor.thresholds; - - if (thresholds === undefined) return false; // No thresholds set, we're done - const metrics = payload?.data ?? null; - if (metrics === null) return false; // No metrics, we're done - - // We should send a notificaiton - - const shouldSend = shouldSendHardwareAlert(monitor, monitorStatusResponse); - if (shouldSend === false) return false; - - return await this.sendNotifications(monitor, monitorStatusResponse); - } - - // We should send a notification for non-hardware monitor status change - return await this.sendNotifications(monitor, monitorStatusResponse); + // Send notifications based on decision + return await this.sendNotifications(monitor, monitorStatusResponse, decision); }; sendTestNotification = async (notification: Notification) => { diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index 0e1011278..b01131bbe 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -1,21 +1,20 @@ -import { IMonitorsRepository } from "@/repositories/index.js"; -import MonitorStats from "../../db/models/MonitorStats.js"; -import { CheckModel } from "@/db/models/index.js"; +import { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository } from "@/repositories/index.js"; import type { - CheckErrorInfo, Monitor, + MonitorStatus, MonitorStatusResponse, StatusChangeResult, Check, HardwareStatusPayload, PageSpeedStatusPayload, CheckSnapshot, + MonitorStats, } from "@/types/index.js"; +import { ILogger } from "@/utils/logger.js"; const SERVICE_NAME = "StatusService"; export interface IStatusService { updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }): Promise; - getStatusString(status: boolean | undefined): string; handleIncidentForCheck(check: any, monitor: Monitor, action: any, errorContext?: string): Promise; updateMonitorStatus( statusResponse: MonitorStatusResponse, @@ -28,39 +27,60 @@ export class StatusService implements IStatusService { private logger: any; private buffer: any; private monitorsRepository: IMonitorsRepository; + private monitorStatsRepository: IMonitorStatsRepository; + private checksRepository: IChecksRepository; - constructor({ logger, buffer, monitorsRepository }: { logger: any; buffer: any; monitorsRepository: IMonitorsRepository }) { + constructor( + logger: ILogger, + buffer: any, + monitorsRepository: IMonitorsRepository, + monitorStatsRepository: IMonitorStatsRepository, + checksRepository: IChecksRepository + ) { this.logger = logger; this.buffer = buffer; this.monitorsRepository = monitorsRepository; + this.monitorStatsRepository = monitorStatsRepository; + this.checksRepository = checksRepository; } get serviceName() { return StatusService.SERVICE_NAME; } - async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }) { + async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: MonitorStatusResponse }) { try { const monitorId = monitor.id; const { responseTime, status } = networkResponse; - // Get stats - let stats = await MonitorStats.findOne({ monitorId }); + let stats: Omit | null = null; + stats = await this.monitorStatsRepository + .findByMonitorId(monitorId) + .then((result) => result) + .catch(() => { + this.logger.debug({ + service: SERVICE_NAME, + method: "updateRunningStats", + message: `No existing stats found for monitor ${monitorId}, initializing new stats.`, + }); + return null; + }); if (!stats) { - stats = new MonitorStats({ + stats = { monitorId, avgResponseTime: 0, totalChecks: 0, totalUpChecks: 0, totalDownChecks: 0, uptimePercentage: 0, - lastCheck: null, - }); + lastResponseTime: 0, + lastCheckTimestamp: 0, + }; } // Update stats // Last response time - stats.lastResponseTime = responseTime; + stats.lastResponseTime = responseTime ?? 0; // Avg response time: let avgResponseTime = stats.avgResponseTime; @@ -97,8 +117,7 @@ export class StatusService implements IStatusService { // latest check stats.lastCheckTimestamp = new Date().getTime(); - - await stats.save(); + await this.monitorStatsRepository.create(stats); return true; } catch (error: any) { this.logger.error({ @@ -111,19 +130,12 @@ export class StatusService implements IStatusService { } } - getStatusString = (status: boolean | undefined) => { - if (status === true) return "up"; - if (status === false) return "down"; - return "unknown"; - }; - handleIncidentForCheck = async (check: Check, monitor: Monitor, action: any, errorContext = "incident handling") => { try { - let savedCheck; + let savedCheck: Check | null = null; if (!check.id) { try { - const checkModel = new CheckModel(check); - savedCheck = await checkModel.save(); + savedCheck = await this.checksRepository.create(check); this.buffer.removeCheckFromBuffer(check); } catch (checkError: any) { @@ -208,16 +220,13 @@ export class StatusService implements IStatusService { monitor.recentChecks.shift(); } - if (monitor.status === undefined || monitor.status === null) { - monitor.status = status; - } - const prevStatus = monitor.status; - let newStatus = monitor.status; + let newStatus: MonitorStatus = status === true ? "up" : "down"; let statusChanged = false; // Return early if not enough data points if (monitor.statusWindow.length < monitor.statusWindowSize) { + monitor.status = newStatus; const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor); return { monitor: updated, @@ -233,17 +242,99 @@ export class StatusService implements IStatusService { const failureRate = (failures / monitor.statusWindow.length) * 100; // If threshold has been met and the monitor is not already down, mark down: - if (failureRate >= monitor.statusWindowThreshold && monitor.status !== false) { - newStatus = false; + if (failureRate >= monitor.statusWindowThreshold && monitor.status !== "down") { + newStatus = "down"; statusChanged = true; } // If the failure rate is below the threshold and the monitor is down, recover: - else if (failureRate < monitor.statusWindowThreshold && monitor.status === false) { - newStatus = true; + else if (failureRate < monitor.statusWindowThreshold && monitor.status === "down") { + newStatus = "up"; statusChanged = true; } + // Evaluate hardware threshold breaches (only for hardware monitors) + let thresholdBreaches: { cpu: boolean; memory: boolean; disk: boolean; temp: boolean } | undefined; + if (monitor.type === "hardware" && statusResponse.payload) { + const payload = statusResponse.payload as HardwareStatusPayload; + const metrics = payload?.data; + + if (metrics) { + // Evaluate threshold breaches + const cpuUsage = metrics.cpu?.usage_percent ?? -1; + const cpuBreach = cpuUsage !== -1 && cpuUsage > monitor.cpuAlertThreshold / 100; + + const memoryUsage = metrics.memory?.usage_percent ?? -1; + const memoryBreach = memoryUsage !== -1 && memoryUsage > monitor.memoryAlertThreshold / 100; + + const diskBreach = + metrics.disk?.some((d: any) => typeof d?.usage_percent === "number" && d.usage_percent > monitor.diskAlertThreshold / 100) ?? false; + + const temps = metrics.cpu?.temperature ?? []; + const tempBreach = temps.some((temp: number) => temp > monitor.tempAlertThreshold); + + thresholdBreaches = { + cpu: cpuBreach, + memory: memoryBreach, + disk: diskBreach, + temp: tempBreach, + }; + + // Update counters: decrement if breached, reset to 5 if not breached + if (cpuBreach) { + monitor.cpuAlertCounter = Math.max(0, monitor.cpuAlertCounter - 1); + } else { + monitor.cpuAlertCounter = 5; + } + + if (memoryBreach) { + monitor.memoryAlertCounter = Math.max(0, monitor.memoryAlertCounter - 1); + } else { + monitor.memoryAlertCounter = 5; + } + + if (diskBreach) { + monitor.diskAlertCounter = Math.max(0, monitor.diskAlertCounter - 1); + } else { + monitor.diskAlertCounter = 5; + } + + if (tempBreach) { + monitor.tempAlertCounter = Math.max(0, monitor.tempAlertCounter - 1); + } else { + monitor.tempAlertCounter = 5; + } + + // Check if any counter has reached zero (initial breach) + const anyCounterZero = + monitor.cpuAlertCounter === 0 || monitor.memoryAlertCounter === 0 || monitor.diskAlertCounter === 0 || monitor.tempAlertCounter === 0; + + const anyThresholdBreached = cpuBreach || memoryBreach || diskBreach || tempBreach; + const allThresholdsNormal = !cpuBreach && !memoryBreach && !diskBreach && !tempBreach; + + // Update monitor status based on threshold breach state + if (newStatus !== "down") { + // Don't override "down" status - service unreachable takes precedence + // Check current monitor status, not newStatus for comparison + if (anyCounterZero && anyThresholdBreached && monitor.status !== "breached") { + // Initial breach: counter hit zero, change status to breached + newStatus = "breached"; + statusChanged = true; + } else if (anyCounterZero && anyThresholdBreached && monitor.status === "breached") { + // Already breached, keep status but don't mark as changed + newStatus = "breached"; + // statusChanged remains false + } else if (allThresholdsNormal && monitor.status === "breached") { + // All thresholds returned to normal, recover from breached state + newStatus = "up"; + statusChanged = true; + } + } + } + } + + // Apply the final status monitor.status = newStatus; + const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor); return { @@ -252,6 +343,7 @@ export class StatusService implements IStatusService { prevStatus, code, timestamp: new Date().getTime(), + thresholdBreaches, }; } catch (error: any) { error.service = SERVICE_NAME; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index f6c0f83a7..a196aaae6 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -1,16 +1,11 @@ -import type { Check } from "@/types/check.js"; import type { CheckSnapshot } from "@/types/check.js"; export type { CheckSnapshot } from "@/types/check.js"; export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "unknown"] as const; export type MonitorType = (typeof MonitorTypes)[number]; -export interface MonitorThresholds { - usage_cpu?: number; - usage_memory?: number; - usage_disk?: number; - usage_temperature?: number; -} +export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const; +export type MonitorStatus = (typeof MonitorStatuses)[number]; export type MonitorMatchMethod = "equal" | "include" | "regex" | ""; @@ -20,7 +15,7 @@ export interface Monitor { teamId: string; name: string; description?: string; - status?: boolean; + status: MonitorStatus; statusWindow: boolean[]; statusWindowSize: number; statusWindowThreshold: number; @@ -37,12 +32,14 @@ export interface Monitor { uptimePercentage?: number; notifications: string[]; secret?: string; - thresholds?: MonitorThresholds; - alertThreshold: number; cpuAlertThreshold: number; + cpuAlertCounter: number; memoryAlertThreshold: number; + memoryAlertCounter: number; diskAlertThreshold: number; + diskAlertCounter: number; tempAlertThreshold: number; + tempAlertCounter: number; selectedDisks: string[]; gameId?: string; group: string | null; @@ -56,6 +53,9 @@ export interface MonitorsSummary { upMonitors: number; downMonitors: number; pausedMonitors: number; + initializingMonitors: number; + maintenanceMonitors: number; + breachedMonitors: number; } export interface MonitorsWithChecksByTeamIdResult { diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 6bed40316..d87f1ea4f 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -11,6 +11,7 @@ import type { Monitor, MonitorMatchMethod, MonitorType, + MonitorStatus, } from "@/types/index.js"; export interface MonitorStatusResponse { @@ -106,7 +107,13 @@ export interface MonitorPayloadMap { export type StatusChangeResult = { monitor: Monitor; statusChanged: boolean; - prevStatus: boolean | undefined; + prevStatus: MonitorStatus; code: number; timestamp: number; + thresholdBreaches?: { + cpu: boolean; + memory: boolean; + disk: boolean; + temp: boolean; + }; };