resolve merge conflicts

This commit is contained in:
Vishnu Sreekumaran Nair
2025-05-07 16:18:26 -04:00
57 changed files with 907 additions and 13928 deletions
-3
View File
@@ -13,9 +13,6 @@ on:
required: false
default: "key_value_json"
# For automatic execution at a specific time (every day at midnight)
schedule:
- cron: "0 0 * * *"
permissions:
contents: write
+2 -1
View File
@@ -1,2 +1,3 @@
VITE_APP_API_BASE_URL=UPTIME_APP_API_BASE_URL
VITE_STATUS_PAGE_SUBDOMAIN_PREFIX=UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX
VITE_APP_CLIENT_HOST=UPTIME_APP_CLIENT_HOST
VITE_APP_LOG_LEVEL=UPTIME_APP_LOG_LEVEL
+71 -13627
View File
File diff suppressed because it is too large Load Diff
+8 -6
View File
@@ -22,12 +22,6 @@
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11",
"@reduxjs/toolkit": "2.7.0",
"@solana/wallet-adapter-base": "0.9.25",
"@solana/wallet-adapter-material-ui": "0.16.35",
"@solana/wallet-adapter-react": "0.15.37",
"@solana/wallet-adapter-react-ui": "0.9.37",
"@solana/wallet-adapter-wallets": "0.19.34",
"@solana/web3.js": "1.98.0",
"axios": "^1.7.4",
"dayjs": "1.11.13",
"flag-icons": "7.3.2",
@@ -52,6 +46,14 @@
"redux-persist": "6.0.0",
"vite-plugin-svgr": "^4.2.0"
},
"unusedDepencies": {
"@solana/wallet-adapter-base": "0.9.25",
"@solana/wallet-adapter-material-ui": "0.16.35",
"@solana/wallet-adapter-react": "0.15.37",
"@solana/wallet-adapter-react-ui": "0.9.37",
"@solana/wallet-adapter-wallets": "0.19.34",
"@solana/web3.js": "1.98.0"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
-25
View File
@@ -1,43 +1,18 @@
import { useEffect } from "react";
import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
import { ThemeProvider } from "@emotion/react";
import lightTheme from "./Utils/Theme/lightTheme";
import darkTheme from "./Utils/Theme/darkTheme";
import { CssBaseline, GlobalStyles } from "@mui/material";
import { getAppSettings } from "./Features/Settings/settingsSlice";
import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Routes } from "./Routes";
import WalletProvider from "./Components/WalletProvider";
import { useTranslation } from "react-i18next";
import { setLanguage } from "./Features/UI/uiSlice";
function App() {
const mode = useSelector((state) => state.ui.mode);
const { authToken } = useSelector((state) => state.auth);
const dispatch = useDispatch();
const { i18n } = useTranslation();
useEffect(() => {
if (authToken) {
dispatch(getAppSettings({ authToken })).then((action) => {
if (action.payload && action.payload.success) {
const { language } = action.payload.data;
const availableLanguages = Object.keys(i18n.options.resources || {});
if (language && availableLanguages.includes(language)) {
dispatch(setLanguage(language));
i18n.changeLanguage(language);
} else {
dispatch(setLanguage(availableLanguages[0]));
i18n.changeLanguage(availableLanguages[0]);
}
}
});
}
}, [dispatch, authToken, i18n]);
// Cleanup
useEffect(() => {
@@ -1,14 +1,13 @@
import { Stack, Button } from "@mui/material";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import SkeletonLayout from "./skeleton";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
const CreateMonitorHeader = ({
isAdmin,
label = "Create new",
shouldRender = true,
isLoading = true,
path,
bulkPath,
}) => {
@@ -17,7 +16,6 @@ const CreateMonitorHeader = ({
const theme = useTheme();
if (!isAdmin) return null;
if (!shouldRender) return <SkeletonLayout />;
return (
<Stack
@@ -27,6 +25,7 @@ const CreateMonitorHeader = ({
gap={theme.spacing(6)}
>
<Button
loading={isLoading}
variant="contained"
color="accent"
onClick={() => navigate(path)}
@@ -35,6 +34,7 @@ const CreateMonitorHeader = ({
</Button>
{bulkPath && (
<Button
loading={isLoading}
variant="contained"
color="accent"
onClick={() => {
@@ -52,7 +52,7 @@ export default CreateMonitorHeader;
CreateMonitorHeader.propTypes = {
isAdmin: PropTypes.bool.isRequired,
shouldRender: PropTypes.bool,
isLoading: PropTypes.bool,
path: PropTypes.string.isRequired,
label: PropTypes.string,
bulkPath: PropTypes.string,
@@ -55,10 +55,9 @@ const MonitorTimeFrameHeader = ({
return (
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-end"
justifyContent="flex-end"
alignItems="center"
gap={theme.spacing(4)}
mb={theme.spacing(8)}
>
<Typography variant="body2">
Showing statistics for past{" "}
@@ -10,6 +10,7 @@ import { update } from "../../../Features/Auth/authSlice";
import { useDispatch, useSelector } from "react-redux";
import { createToast } from "../../../Utils/toastUtils";
import { getTouchedFieldErrors } from "../../../Validation/error";
import { useTranslation } from "react-i18next";
const defaultPasswordsState = {
password: "",
@@ -26,6 +27,7 @@ const defaultPasswordsState = {
const PasswordPanel = () => {
const theme = useTheme();
const dispatch = useDispatch();
const { t } = useTranslation();
const SPACING_GAP = theme.spacing(12);
@@ -203,7 +205,7 @@ const PasswordPanel = () => {
<TextInput
type="password"
id="edit-confirm-password"
placeholder="Reenter your new password"
placeholder={t("confirmPassword")}
autoComplete="new-password"
value={localData.confirm}
onChange={handleChange}
@@ -344,7 +344,7 @@ const ProfilePanel = () => {
spellCheck="false"
>
<Box mb={theme.spacing(6)}>
<Typography component="h1">{t('DeleteAccount')}</Typography>
<Typography component="h1">{t('DeleteAccountTitle')}</Typography>
<Typography
component="p"
sx={{ opacity: 0.6 }}
@@ -357,7 +357,7 @@ const ProfilePanel = () => {
color="error"
onClick={() => setIsOpen("delete")}
>
{t('DeleteAccount')}
{t('DeleteAccountButton')}
</Button>
</Box>
)}
@@ -367,7 +367,7 @@ const ProfilePanel = () => {
title={t('DeleteWarningTitle')}
description={t('DeleteAccountWarning')}
onCancel={() => setIsOpen("")}
confirmationButtonLabel={t('DeleteAccount')}
confirmationButtonLabel={t('DeleteAccountButton')}
onConfirm={handleDeleteAccount}
isLoading={isLoading}
/>
+40 -36
View File
@@ -1,46 +1,50 @@
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 { 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";
// 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";
// // 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;
// 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]);
// // 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]
);
// const wallets = useMemo(
// () => [new PhantomWalletAdapter()],
// // eslint-disable-next-line react-hooks/exhaustive-deps
// [network]
// );
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider
wallets={wallets}
autoConnect
>
<WalletModalProvider>{children}</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
};
// return (
// <ConnectionProvider endpoint={endpoint}>
// <WalletProvider
// wallets={wallets}
// autoConnect
// >
// <WalletModalProvider>{children}</WalletModalProvider>
// </WalletProvider>
// </ConnectionProvider>
// );
// };
Wallet.propTypes = {
children: PropTypes.node,
// Wallet.propTypes = {
// children: PropTypes.node,
// };
const Wallet = ({ children }) => {
return children;
};
export default Wallet;
@@ -101,7 +101,7 @@ export const updatePageSpeed = createAsyncThunk(
name: monitor.name,
description: monitor.description,
interval: monitor.interval,
// notifications: monitor.notifications,
notifications: monitor.notifications,
};
const res = await networkService.updateMonitor({
monitorId: monitor._id,
@@ -5,7 +5,7 @@ const initialState = {
isLoading: false,
apiBaseUrl: "",
logLevel: "debug",
language: "",
language: "gb",
pagespeedApiKey: "",
};
+1 -1
View File
@@ -2,7 +2,7 @@ import { useState } from "react";
import { networkService } from "../main";
import { useTranslation } from "react-i18next";
const CLIENT_HOST = import.meta.env.VITE_CLIENT_HOST;
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST;
const useGetInviteToken = () => {
const { t } = useTranslation();
@@ -58,7 +58,7 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email}
helperText={errors.email ? t(errors.email) : ""}
ref={inputRef}
/>
<Stack
@@ -70,8 +70,17 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
color="accent"
type="submit"
disabled={errors.email && true}
className="dashboard-style-button"
sx={{
width: "30%",
px: theme.spacing(6),
borderRadius: `${theme.shape.borderRadius}px !important`,
'&.MuiButtonBase-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
},
'&.MuiButton-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
},
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
@@ -75,8 +75,16 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
variant="outlined"
color="info"
onClick={onBack}
className="dashboard-style-button"
sx={{
px: theme.spacing(5),
borderRadius: `${theme.shape.borderRadius}px !important`,
'&.MuiButtonBase-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
},
'&.MuiButton-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
},
"& svg.MuiSvgIcon-root": {
mr: theme.spacing(3),
},
@@ -95,13 +103,22 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
type="submit"
loading={authState.isLoading}
disabled={errors.password && true}
className="dashboard-style-button"
sx={{
width: "30%",
px: theme.spacing(4),
borderRadius: `${theme.shape.borderRadius}px !important`,
'&.MuiButtonBase-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
},
'&.MuiButton-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
},
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
boxShadow: `none`,
}}
>
{t("continue")}
+44 -22
View File
@@ -9,13 +9,13 @@ import { createToast } from "../../../Utils/toastUtils";
import { networkService } from "../../../main";
import Background from "../../../assets/Images/background-grid.svg?react";
import Logo from "../../../assets/icons/checkmate-icon.svg?react";
import { logger } from "../../../Utils/Logger";
import "../index.css";
import EmailStep from "./Components/EmailStep";
import PasswordStep from "./Components/PasswordStep";
import ThemeSwitch from "../../../Components/ThemeSwitch";
import ForgotPasswordLabel from "./Components/ForgotPasswordLabel";
import LanguageSelector from "../../../Components/LanguageSelector";
import { useTranslation } from "react-i18next";
const DEMO = import.meta.env.VITE_APP_DEMO;
@@ -24,10 +24,10 @@ const DEMO = import.meta.env.VITE_APP_DEMO;
*/
const Login = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
const dispatch = useDispatch();
const { t } = useTranslation();
const navigate = useNavigate();
const authState = useSelector((state) => state.auth);
const { authToken } = authState;
@@ -43,33 +43,28 @@ const Login = () => {
const [errors, setErrors] = useState({});
const [step, setStep] = useState(0);
useEffect(() => {
if (authToken) {
navigate("/uptime");
return;
}
networkService
.doesSuperAdminExist()
.then((response) => {
if (response.data.data === false) {
navigate("/register");
}
})
.catch((error) => {
logger.error(error);
});
}, [authToken, navigate]);
const handleChange = (event) => {
const { value, id } = event.target;
const name = idMap[id];
const lowerCasedValue = name === idMap["login-email-input"]? value?.toLowerCase()||value : value
const lowerCasedValue =
name === idMap["login-email-input"] ? value?.toLowerCase() || value : value;
setForm((prev) => ({
...prev,
[name]: lowerCasedValue,
}));
const { error } = credentials.validate({ [name]: lowerCasedValue }, { abortEarly: false });
const { error } = credentials.validate(
{ [name]: lowerCasedValue },
{ abortEarly: false }
);
setErrors((prev) => {
const prevErrors = { ...prev };
@@ -88,8 +83,10 @@ const Login = () => {
{ abortEarly: false }
);
if (error) {
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
createToast({ body: error.details[0].message });
const errorMessage = error.details[0].message;
const translatedMessage = errorMessage.startsWith('auth') ? t(errorMessage) : errorMessage;
setErrors((prev) => ({ ...prev, email: translatedMessage }));
createToast({ body: translatedMessage });
} else {
setStep(1);
}
@@ -106,15 +103,15 @@ const Login = () => {
createToast({
body:
error.details && error.details.length > 0
? error.details[0].message
: "Error validating data.",
? (error.details[0].message.startsWith('auth') ? t(error.details[0].message) : error.details[0].message)
: t("Error validating data."),
});
} else {
const action = await dispatch(login(form));
if (action.payload.success) {
navigate("/uptime");
createToast({
body: "Welcome back! You're successfully logged in.",
body: t("welcomeBack"),
});
} else {
if (action.payload) {
@@ -230,6 +227,31 @@ const Login = () => {
email={form.email}
errorEmail={errors.email}
/>
{/* Registration link */}
<Box textAlign="center" >
<Typography
className="forgot-p"
display="inline-block"
color={theme.palette.primary.main}
>
{t("doNotHaveAccount")}
</Typography>
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
sx={{
cursor: 'pointer',
'&:hover': {
color: theme.palette.accent.darker
}
}}
onClick={() => navigate("/register")}
>
{t("registerHere")}
</Typography>
</Box>
</Stack>
</Stack>
);
+1 -1
View File
@@ -387,7 +387,7 @@ const Register = ({ isSuperAdmin }) => {
}}
sx={{ userSelect: "none", color: theme.palette.accent.main }}
>
{t("authLoginTitle")}
{t("authRegisterLoginLink")}
</Typography>
</Box>
</Stack>
@@ -68,7 +68,11 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email && t("authRegisterEmailRequired")}
helperText={errors.email && (
errors.email.includes("required") ? t("authRegisterEmailRequired") :
errors.email.includes("valid email") ? t("authRegisterEmailInvalid") :
errors.email
)}
ref={inputRef}
/>
<Stack
@@ -75,6 +75,7 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
<IconButton
aria-label="monitor actions"
onClick={openMenu}
disabled={!isAdmin}
sx={{
"&:focus": {
outline: "none",
@@ -94,7 +94,7 @@ const InfrastructureMonitors = () => {
<Breadcrumbs list={BREADCRUMBS} />
<MonitorCreateHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
isLoading={isLoading}
path="/infrastructure/create"
/>
<Stack direction={"row"}>
@@ -62,7 +62,7 @@ const PageSpeed = () => {
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
isLoading={isLoading}
path="/pagespeed/create"
/>
<MonitorCountHeader
+161
View File
@@ -0,0 +1,161 @@
import React, { useState } from "react";
import { Box, Typography, Button, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router";
import { networkService } from "../Utils/NetworkService";
import Alert from "../Components/Alert";
import { createToast } from "../Utils/toastUtils";
import { useTranslation } from "react-i18next";
import Background from "../assets/Images/background-grid.svg?react";
import Logo from "../assets/icons/checkmate-icon.svg?react";
import ThemeSwitch from "../Components/ThemeSwitch";
import LanguageSelector from "../Components/LanguageSelector";
const ServerUnreachable = () => {
const theme = useTheme();
const navigate = useNavigate();
const { t } = useTranslation();
// State for tracking connection check status
const [isCheckingConnection, setIsCheckingConnection] = useState(false);
const handleRetry = React.useCallback(async () => {
setIsCheckingConnection(true);
try {
// Try to connect to the backend with a simple API call
// We'll use any lightweight endpoint that doesn't require authentication
await networkService.axiosInstance.get('/health', { timeout: 5000 });
// If successful, show toast and navigate to login page
createToast({
body: t("backendReconnected", "Connection to server restored"),
});
navigate("/login");
} catch (error) {
// If still unreachable, stay on this page and show toast
createToast({
body: t("backendStillUnreachable", "Server is still unreachable"),
});
} finally {
setIsCheckingConnection(false);
}
}, [navigate, t]);
return (
<Stack
className="login-page auth"
overflow="hidden"
>
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
{/* Header with logo */}
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
</Stack>
<Stack
direction="row"
spacing={2}
alignItems="center"
>
<LanguageSelector />
<ThemeSwitch />
</Stack>
</Stack>
<Stack
width="100%"
maxWidth={600}
flex={1}
justifyContent="center"
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
pb={theme.spacing(20)}
mx="auto"
rowGap={theme.spacing(8)}
sx={{
"& > .MuiStack-root": {
border: 1,
borderRadius: theme.spacing(5),
borderColor: theme.palette.primary.lowContrast,
backgroundColor: theme.palette.primary.main,
padding: {
xs: theme.spacing(12),
sm: theme.spacing(20),
},
},
}}
>
<Stack spacing={theme.spacing(6)} alignItems="center">
<Box sx={{
width: theme.spacing(220),
mx: 'auto',
'& .alert.row-stack': {
width: '100%',
alignItems: 'center',
gap: theme.spacing(3)
}
}}>
<Alert
variant="error"
body={t("backendUnreachable", "Server Unreachable")}
hasIcon={true}
/>
</Box>
<Box mt={theme.spacing(2)}>
<Typography
variant="body1"
align="center"
color={theme.palette.primary.contrastTextSecondary}
>
{t("backendUnreachableMessage", "The Checkmate server is not responding. Please check your deployment configuration or try again later.")}
</Typography>
</Box>
<Box sx={{ mt: theme.spacing(4) }}>
<Button
variant="contained"
color="accent"
onClick={handleRetry}
disabled={isCheckingConnection}
className="dashboard-style-button"
sx={{
px: theme.spacing(6),
borderRadius: `${theme.shape.borderRadius}px !important`,
'&.MuiButtonBase-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
},
'&.MuiButton-root': {
borderRadius: `${theme.shape.borderRadius}px !important`
}
}}
>
{isCheckingConnection ?
t("retryingConnection", "Retrying Connection...") :
t("retryConnection", "Retry Connection")}
</Button>
</Box>
</Stack>
</Stack>
</Stack>
);
};
export default ServerUnreachable;
+38 -27
View File
@@ -8,10 +8,10 @@ import Dialog from "../../Components/Dialog";
import ConfigBox from "../../Components/ConfigBox";
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
import { getAppSettings } from "../../Features/Settings/settingsSlice";
import {
WalletMultiButton,
WalletDisconnectButton,
} from "@solana/wallet-adapter-react-ui";
// import {
// WalletMultiButton,
// WalletDisconnectButton,
// } from "@solana/wallet-adapter-react-ui";
//Utils
import { useTheme } from "@emotion/react";
@@ -290,7 +290,7 @@ const Settings = () => {
></Select>
</Stack>
</ConfigBox>
{isAdmin && (
{/* {isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDistributedUptime")}</Typography>
@@ -312,7 +312,7 @@ const Settings = () => {
: t("settingsDisabled")}
</Box>
</ConfigBox>
)}
)} */}
<ConfigBox>
<Box>
<Typography component="h1">{t("pageSpeedApiKeyFieldTitle")}</Typography>
@@ -350,7 +350,7 @@ const Settings = () => {
)}
</Stack>
</ConfigBox>
{isAdmin && (
{/* {isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsWallet")}</Typography>
@@ -375,7 +375,7 @@ const Settings = () => {
</Stack>
</Box>
</ConfigBox>
)}
)} */}
{isAdmin && (
<ConfigBox>
<Box>
@@ -421,14 +421,15 @@ const Settings = () => {
</ConfigBox>
)}
{isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsDemoMonitorsDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<>
{/* Demo Monitors Section */}
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsDemoMonitorsDescription")}
</Typography>
</Box>
<Box>
<Typography>{t("settingsAddDemoMonitors")}</Typography>
<Button
@@ -441,6 +442,16 @@ const Settings = () => {
{t("settingsAddDemoMonitorsButton")}
</Button>
</Box>
</ConfigBox>
{/* System Reset Section */}
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsSystemReset")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsSystemResetDescription")}
</Typography>
</Box>
<Box>
<Typography>{t("settingsRemoveAllMonitors")}</Typography>
<Button
@@ -455,17 +466,17 @@ const Settings = () => {
{t("settingsRemoveAllMonitorsButton")}
</Button>
</Box>
</Stack>
<Dialog
open={isOpen.deleteMonitors}
theme={theme}
title={t("settingsRemoveAllMonitorsDialogTitle")}
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
onConfirm={handleDeleteAllMonitors}
isLoading={isLoading || authIsLoading || checksIsLoading}
/>
</ConfigBox>
<Dialog
open={isOpen.deleteMonitors}
theme={theme}
title={t("settingsRemoveAllMonitorsDialogTitle")}
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
onConfirm={handleDeleteAllMonitors}
isLoading={isLoading || authIsLoading || checksIsLoading}
/>
</ConfigBox>
</>
)}
<ConfigBox>
@@ -3,6 +3,7 @@ import { Box, Stack, Typography, Button } from "@mui/material";
import Image from "../../../../../Components/Image";
import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react";
import ThemeSwitch from "../../../../../Components/ThemeSwitch";
import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward";
//Utils
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
@@ -86,6 +87,8 @@ const ControlsHeader = ({
type = "uptime",
}) => {
const theme = useTheme();
const { t } = useTranslation();
const publicUrl = `/status/uptime/public/${url}`;
return (
<Stack
@@ -118,6 +121,26 @@ const ControlsHeader = ({
>
{statusPage?.companyName}
</Typography>
{statusPage?.isPublished && !isPublic && (
<Stack
direction="row"
alignItems="center"
justifyContent="center"
onClick={() => {
window.open(publicUrl, "_blank", "noopener,noreferrer");
}}
sx={{
display: "inline-flex",
":hover": {
cursor: "pointer",
borderBottom: 1,
},
}}
>
<Typography>{t("publicLink")}</Typography>
<ArrowOutwardIcon />
</Stack>
)}
</Stack>
<Controls
isDeleting={isDeleting}
@@ -139,6 +139,7 @@ const PublicStatus = () => {
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
url={url}
isPublic={isPublic}
/>
<Typography variant="h2">{t("statusPageStatusServiceStatus")}</Typography>
<StatusBar monitors={monitors} />
@@ -61,6 +61,7 @@ const StatusPages = () => {
label="Create status page"
isAdmin={isAdmin}
path="/status/uptime/create"
isLoading={isLoading}
/>
<StatusPagesTable data={statusPages} />
</Stack>
@@ -61,18 +61,20 @@ const UptimeStatusBoxes = ({
</>
}
/>
<StatBox
heading="certificate expiry"
subHeading={
<Typography
component="span"
fontSize={13}
color={theme.palette.primary.contrastText}
>
{certificateExpiry}
</Typography>
}
/>
{monitor?.type === "http" && (
<StatBox
heading="certificate expiry"
subHeading={
<Typography
component="span"
fontSize={13}
color={theme.palette.primary.contrastText}
>
{certificateExpiry}
</Typography>
}
/>
)}
</StatusBoxes>
);
};
+1 -1
View File
@@ -189,7 +189,7 @@ const UptimeMonitors = () => {
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
isLoading={isLoading}
path="/uptime/create"
bulkPath="/uptime/bulk-import"
/>
+24 -17
View File
@@ -36,6 +36,9 @@ import DistributedUptimeDetails from "../Pages/DistributedUptime/Details";
import CreateDistributedUptimeStatus from "../Pages/DistributedUptimeStatus/Create";
import DistributedUptimeStatus from "../Pages/DistributedUptimeStatus/Status";
// Server Status
import ServerUnreachable from "../Pages/ServerUnreachable";
// Incidents
import Incidents from "../Pages/Incidents";
@@ -96,16 +99,16 @@ const Routes = () => {
path="/uptime/configure/:monitorId/"
element={<UptimeConfigure />}
/>
<Route
{/* <Route
path="/distributed-uptime"
element={
<ProtectedDistributedUptimeRoute>
<DistributedUptimeMonitors />{" "}
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
{/* <Route
path="/distributed-uptime/create"
element={
<ProtectedDistributedUptimeRoute>
@@ -120,15 +123,15 @@ const Routes = () => {
<CreateDistributedUptime />
</ProtectedDistributedUptimeRoute>
}
/>
<Route
/> */}
{/* <Route
path="/distributed-uptime/:monitorId"
element={
<ProtectedDistributedUptimeRoute>
<DistributedUptimeDetails />
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
path="pagespeed"
@@ -154,9 +157,9 @@ const Routes = () => {
path="infrastructure/create"
element={<InfrastructureCreate />}
/>
<Route
path="/infrastructure/configure/:monitorId"
element={<InfrastructureCreate />}
<Route
path="/infrastructure/configure/:monitorId"
element={<InfrastructureCreate />}
/>
<Route
path="infrastructure/:monitorId"
@@ -177,42 +180,42 @@ const Routes = () => {
element={<Status />}
/>
<Route
{/* <Route
path="/status/distributed/:url"
element={
<ProtectedDistributedUptimeRoute>
<DistributedUptimeStatus />
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
path="status/uptime/create"
element={<CreateStatus />}
/>
<Route
{/* <Route
path="/status/distributed/create/:monitorId"
element={
<ProtectedDistributedUptimeRoute>
<CreateDistributedUptimeStatus />
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
path="status/uptime/configure/:url"
element={<CreateStatus />}
/>
<Route
{/* <Route
path="/status/distributed/configure/:url"
element={
<ProtectedDistributedUptimeRoute>
<CreateDistributedUptimeStatus />
</ProtectedDistributedUptimeRoute>
}
/>
/> */}
<Route
path="integrations"
@@ -280,11 +283,15 @@ const Routes = () => {
path="/status/uptime/public/:url"
element={<Status />}
/>
<Route
{/* <Route
path="/status/distributed/public/:url"
element={<DistributedUptimeStatus />}
/>
/> */}
<Route
path="/server-unreachable"
element={<ServerUnreachable />}
/>
<Route
path="*"
element={<NotFound />}
+14 -2
View File
@@ -32,7 +32,7 @@ class NetworkService {
config.headers = {
Authorization: `Bearer ${authToken}`,
"Accept-Language": currentLanguage,
"Accept-Language": currentLanguage === "gb" ? "en" : currentLanguage,
...config.headers,
};
@@ -45,6 +45,16 @@ class NetworkService {
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
// Handle network errors (server unreachable)
if (error.code === "ERR_NETWORK") {
// Navigate to server unreachable page
navigate("/server-unreachable");
// Return an empty resolved promise to stop the error propagation
return Promise.reject(error);
}
// Handle authentication errors
if (error.response && error.response.status === 401) {
dispatch(clearAuthState());
dispatch(clearUptimeMonitorState());
@@ -505,6 +515,8 @@ class NetworkService {
return this.axiosInstance.get("/auth/users/superadmin");
}
/**
* ************************************
* Get all users
@@ -921,7 +933,7 @@ class NetworkService {
onOpen?.();
};
this.eventSource.addEventListener("open", (e) => {});
this.eventSource.addEventListener("open", (e) => { });
this.eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
+1 -1
View File
@@ -16,7 +16,7 @@ Object.keys(translations).forEach((path) => {
});
const savedLanguage = store.getState()?.ui?.language;
const initialLanguage = savedLanguage || primaryLanguage;
const initialLanguage = savedLanguage;
i18n.use(initReactI18next).init({
resources,
+5 -3
View File
@@ -66,8 +66,8 @@ const credentials = joi.object({
return lowercasedValue;
})
.messages({
"string.empty": "Email is required",
"string.email": "Must be a valid email address",
"string.empty": "authRegisterEmailRequired",
"string.email": "authRegisterEmailInvalid",
}),
password: passwordSchema,
newPassword: passwordSchema,
@@ -253,7 +253,9 @@ const statusPageValidation = joi.object({
});
const settingsValidation = joi.object({
ttl: joi.number().required().messages({
"string.empty": "TTL is required",
"string.empty": "Please enter a value",
"number.base": "Please enter a valid number",
"any.required": "Please enter a value"
}),
pagespeedApiKey: joi.string().allow("").optional(),
})
+45 -24
View File
@@ -1,9 +1,11 @@
{
"dontHaveAccount": "Don't have account",
"doNotHaveAccount": "Do not have an account?",
"registerHere": "Register here",
"email": "E-mail",
"forgotPassword": "Forgot Password",
"password": "password",
"signUp": "Sign up",
"password": "Password",
"signUp": "Sign Up",
"submit": "Submit",
"title": "Title",
"continue": "Continue",
@@ -15,9 +17,11 @@
"authForgotPasswordTitle": "Forgot password?",
"authForgotPasswordResetPassword": "Reset password",
"createPassword": "Create your password",
"createAPassword": "Create a password",
"createAPassword": "Password",
"authRegisterAlreadyHaveAccount": "Already have an account?",
"authRegisterLoginLink": "Log In",
"commonAppName": "Checkmate",
"welcomeBack": "Welcome back! You're successfully logged in.",
"authLoginEnterEmail": "Enter your email",
"authRegisterTitle": "Create an account",
"authRegisterStepOneTitle": "Create your account",
@@ -40,7 +44,7 @@
"authSetNewPasswordDescription": "Your new password must be different from previously used passwords.",
"authSetNewPasswordNewPassword": "New password",
"authSetNewPasswordConfirmPassword": "Confirm password",
"confirmPassword": "Confirm your password",
"confirmPassword": "Re-enter password to confirm",
"authSetNewPasswordResetPassword": "Reset password",
"authSetNewPasswordBackTo": "Back to",
"authPasswordMustBeAtLeast": "Must be at least",
@@ -51,7 +55,10 @@
"authPasswordUpperCharacter": "one upper character",
"authPasswordLowerCharacter": "one lower character",
"authPasswordConfirmAndPassword": "Confirm password and password",
"authPasswordMustMatch": "must match",
"authPasswordMustMatch": "Passwords must match",
"validationNameRequired": "Please enter your name",
"validationNameTooLong": "Name should be less than 50 characters",
"validationNameInvalidCharacters": "Please use only letters, spaces, apostrophes, or hyphens",
"authRegisterCreateAccount": "Create your account to get started",
"authRegisterCreateSuperAdminAccount": "Create your super admin account to get started",
"authRegisterSignUpWithEmail": "Create super admin account",
@@ -60,7 +67,7 @@
"distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city",
"settingsGeneralSettings": "General settings",
"settingsDisplayTimezone": "Display timezone",
"settingsDisplayTimezoneDescription": "The timezone of the dashboard you publicly display.",
"settingsDisplayTimezoneDescription": "Select the timezone used to display dates and times throughout the application.",
"settingsAppearance": "Appearance",
"settingsAppearanceDescription": "Switch between light and dark mode, or change user interface language",
"settingsThemeMode": "Theme Mode",
@@ -69,23 +76,25 @@
"settingsDistributedUptimeDescription": "Enable/disable distributed uptime monitoring.",
"settingsEnabled": "Enabled",
"settingsDisabled": "Disabled",
"settingsHistoryAndMonitoring": "History and monitoring",
"settingsHistoryAndMonitoringDescription": "Define here for how long you want to keep the data. You can also remove all past data.",
"settingsHistoryAndMonitoring": "History of monitoring",
"settingsHistoryAndMonitoringDescription": "Define how long you want to retain historical data. You can also clear all existing data.",
"settingsTTLLabel": "The days you want to keep monitoring history.",
"settingsTTLOptionalLabel": "0 for infinite",
"settingsClearAllStats": "Clear all stats. This is irreversible.",
"settingsClearAllStatsButton": "Clear all stats",
"settingsClearAllStatsDialogTitle": "Do you want to clear all stats?",
"settingsClearAllStatsDialogDescription": "Once deleted, your monitors cannot be retrieved.",
"settingsClearAllStatsDialogDescription": "Once removed, the monitoring history and stats cannot be retrieved.",
"settingsClearAllStatsDialogConfirm": "Yes, clear all stats",
"settingsDemoMonitors": "Demo monitors",
"settingsDemoMonitorsDescription": "Here you can add and remove demo monitors.",
"settingsAddDemoMonitors": "Add demo monitors",
"settingsDemoMonitorsDescription": "Add sample monitors for demonstration purposes.",
"settingsAddDemoMonitors": "Adding demo monitors",
"settingsAddDemoMonitorsButton": "Add demo monitors",
"settingsRemoveAllMonitors": "Remove all monitors",
"settingsSystemReset": "System reset",
"settingsSystemResetDescription": "Remove all monitors from your system.",
"settingsRemoveAllMonitors": "Removing all monitors",
"settingsRemoveAllMonitorsButton": "Remove all monitors",
"settingsRemoveAllMonitorsDialogTitle": "Do you want to remove all monitors?",
"settingsRemoveAllMonitorsDialogConfirm": "Yes, clear all monitors",
"settingsRemoveAllMonitorsDialogConfirm": "Yes, remove all monitors",
"settingsWallet": "Wallet",
"settingsWalletDescription": "Connect your wallet here. This is required for the Distributed Uptime monitor to connect to multiple nodes globally.",
"settingsAbout": "About",
@@ -99,18 +108,26 @@
"settingsFailedToAddDemoMonitors": "Failed to add demo monitors",
"settingsMonitorsDeleted": "Successfully deleted all monitors",
"settingsFailedToDeleteMonitors": "Failed to delete all monitors",
"backendUnreachable": "Server Connection Error",
"backendUnreachableMessage": "We're unable to connect to the server. Please check your internet connection or verify your deployment configuration if the problem persists.",
"backendUnreachableError": "Cannot connect to the server. Please try again later.",
"retryConnection": "Retry connection",
"retryingConnection": "Connecting...",
"backendReconnected": "Successfully reconnected to the server.",
"backendStillUnreachable": "Server is still unreachable. Please try again later.",
"backendConnectionError": "Error connecting to the server. Please check your network connection.",
"starPromptTitle": "Star Checkmate",
"starPromptDescription": "See the latest releases and help grow the community on GitHub",
"https": "HTTPS",
"http": "HTTP",
"monitor": "monitor",
"aboutus": "About Us",
"signUP": "Sign Up",
"now": "Now",
"delete": "Delete",
"configure": "Configure",
"networkError": "Network error",
"responseTime": "Response time:",
"responseTime": "Response time",
"ms": "ms",
"bar": "Bar",
"area": "Area",
@@ -386,20 +403,22 @@
"DragandDrop": "drag and drop",
"MaxSize": "Maximum Size",
"SupportedFormats": "Supported formats",
"FirstName": "First name",
"LastName": "Last name",
"EmailDescriptionText": "This is your current email address — it cannot be changed.",
"YourPhoto": "Your photo",
"FirstName": "Name",
"LastName": "Surname",
"EmailDescriptionText": "Your current emailit cannot be changed.",
"YourPhoto": "Profile photo",
"PhotoDescriptionText": "This photo will be displayed in your profile page.",
"save": "Save",
"DeleteAccount": "Delete account",
"DeleteDescriptionText": "Note that deleting your account will remove all data from the server. This is permanent and non-recoverable.",
"DeleteAccountWarning": "If you delete your account, you will no longer be able to sign in, and all of your data will be deleted. Deleting your account is permanent and non-recoverable action.",
"DeleteWarningTitle": "Really delete this account?",
"DeleteAccountTitle": "Remove account",
"DeleteAccountButton": "Remove account",
"DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.",
"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.",
"DeleteWarningTitle": "Really remove this account?",
"authRegisterFirstName": "Name",
"authRegisterLastName": "Surname",
"authRegisterEmail": "Email",
"authRegisterEmailRequired": "Email is required",
"authRegisterEmailRequired": "To continue, please enter your email address",
"authRegisterEmailInvalid": "Please enter a valid email address",
"bulkImport": {
"title": "Bulk Import",
"selectFileTips": "Select CSV file to upload",
@@ -411,6 +430,7 @@
"noFileSelected": "No file selected",
"fallbackPage": "Import a file to upload a list of servers in bulk"
},
"publicLink": "Public link",
"maskedPageSpeedKeyPlaceholder": "*************************************",
"pageSpeedApiKeyFieldTitle": "Google PageSpeed API key",
"pageSpeedApiKeyFieldLabel": "PageSpeed API key",
@@ -418,3 +438,4 @@
"pageSpeedApiKeyFieldResetLabel": "API key is set. Click Reset to change it.",
"reset": "Reset"
}
+43 -17
View File
@@ -1,8 +1,10 @@
{
"dontHaveAccount": "Нет аккаунта",
"doNotHaveAccount": "У вас нет учетной записи?",
"registerHere": "Зарегистрироваться здесь",
"email": "Почта",
"forgotPassword": "Забыли пароль",
"password": "пароль",
"password": "Пароль",
"signUp": "Зарегистрироваться",
"submit": "Подтвердить",
"title": "Название",
@@ -15,9 +17,11 @@
"authForgotPasswordTitle": "Забыли пароль?",
"authForgotPasswordResetPassword": "Сбросить пароль",
"createPassword": "Создайте свой пароль",
"createAPassword": "Создайте пароль",
"createAPassword": "Пароль",
"authRegisterAlreadyHaveAccount": "Уже есть аккаунт?",
"authRegisterLoginLink": "Войти",
"commonAppName": "Checkmate",
"welcomeBack": "Добро пожаловать обратно! Вы успешно вошли в систему.",
"authLoginEnterEmail": "Введите свой email",
"authRegisterTitle": "Создать аккаунт",
"authRegisterStepOneTitle": "Создайте свой аккаут",
@@ -40,7 +44,8 @@
"authSetNewPasswordDescription": "Ваш новый пароль должен отличаться от ранее использованных паролей.",
"authSetNewPasswordNewPassword": "Новый пароль",
"authSetNewPasswordConfirmPassword": "Подтвердите пароль",
"confirmPassword": "Подтвердите ваш пароль",
"confirmPassword": "Введите пароль еще раз для подтверждения",
"confirmNewPasswordPlaceholder": "Подтвердите ваш новый пароль",
"authSetNewPasswordResetPassword": "Сбросить пароль",
"authSetNewPasswordBackTo": "Назад к",
"authPasswordMustBeAtLeast": "Должно быть как минимум",
@@ -51,7 +56,7 @@
"authPasswordUpperCharacter": "один верхний символ",
"authPasswordLowerCharacter": "один нижний символ",
"authPasswordConfirmAndPassword": "Подтвердите пароль и пароль",
"authPasswordMustMatch": "должен совпадать",
"authPasswordMustMatch": "Пароли должны совпадать",
"authRegisterCreateAccount": "Создайте свою учетную запись, чтобы начать",
"authRegisterCreateSuperAdminAccount": "Создайте учетную запись суперадминистратора, чтобы начать работу",
"authRegisterSignUpWithEmail": "Создать учетную запись суперадминистратора",
@@ -59,12 +64,13 @@
"authRegisterFirstName": "Имя",
"authRegisterLastName": "Фамилия",
"authRegisterEmail": "Эл. почта",
"authRegisterEmailRequired": "Эл. почта обязательна",
"authRegisterEmailRequired": "Чтобы продолжить, пожалуйста, введите ваш адрес электронной почты",
"authRegisterEmailInvalid": "Пожалуйста, введите корректный адрес электронной почты",
"distributedStatusHeaderText": "Охват реального времени и реального устройства",
"distributedStatusSubHeaderText": "Работает на миллионах устройств по всему миру, просматривайте производительность системы по глобальному региону, стране или городу",
"settingsGeneralSettings": "Общие настройки",
"settingsDisplayTimezone": "Отображать часовой пояс",
"settingsDisplayTimezoneDescription": "Часовой пояс панели мониторинга, которую вы публично отображаете.",
"settingsDisplayTimezoneDescription": "Выберите часовой пояс, используемый для отображения дат и времени в приложении.",
"settingsAppearance": "Внешний вид",
"settingsAppearanceDescription": "Переключение между светлым и темным режимом или изменение языка пользовательского интерфейса",
"settingsThemeMode": "Тема",
@@ -73,23 +79,25 @@
"settingsDistributedUptimeDescription": "Включить/выключить distributed uptime monitoring.",
"settingsEnabled": "Включено",
"settingsDisabled": "Выключено",
"settingsHistoryAndMonitoring": "История и мониторинг",
"settingsHistoryAndMonitoringDescription": "Определите здесь, как долго вы хотите хранить данные. Вы также можете удалить все прошлые данные.",
"settingsHistoryAndMonitoring": "История мониторинга",
"settingsHistoryAndMonitoringDescription": "Определите, как долго вы хотите хранить исторические данные. Вы также можете очистить все существующие данные.",
"settingsTTLLabel": "Дни, за которыми вы хотите следить.",
"settingsTTLOptionalLabel": "0 для бесконечности",
"settingsClearAllStats": "Очистить всю статистику. Это необратимо.",
"settingsClearAllStatsButton": "Очистить всю статистику",
"settingsClearAllStatsDialogTitle": "Хотите очистить всю статистику?",
"settingsClearAllStatsDialogDescription": "После удаления ваши мониторы не могут быть восстановлены.",
"settingsClearAllStatsDialogDescription": "После удаления история мониторинга и статистика не могут быть восстановлены.",
"settingsClearAllStatsDialogConfirm": "Да, очистить всю статистику",
"settingsDemoMonitors": "Демо мониторы",
"settingsDemoMonitorsDescription": "Здесь вы можете добавлять и удалять демонстрационные мониторы.",
"settingsAddDemoMonitors": "Добавьте демонстрационные мониторы",
"settingsDemoMonitorsDescription": "Добавьте примеры мониторов для демонстрации.",
"settingsAddDemoMonitors": "Добавление демонстрационных мониторов",
"settingsAddDemoMonitorsButton": "Добавьте демонстрационные мониторы",
"settingsRemoveAllMonitors": "Удалить все демонстрационные мониторы",
"settingsRemoveAllMonitorsButton": "Удалить все демонстрационные мониторы",
"settingsSystemReset": "Сброс системы",
"settingsSystemResetDescription": "Удалить все мониторы из вашей системы.",
"settingsRemoveAllMonitors": "Удаление всех мониторов",
"settingsRemoveAllMonitorsButton": "Удалить все мониторы",
"settingsRemoveAllMonitorsDialogTitle": "Хотите удалить все мониторы?",
"settingsRemoveAllMonitorsDialogConfirm": "Да, очистить все мониторы",
"settingsRemoveAllMonitorsDialogConfirm": "Да, удалить все мониторы",
"settingsWallet": "Кошелёк",
"settingsWalletDescription": "Подключите свой кошелек здесь. Это необходимо для того, чтобы монитор Distributed Uptime мог подключиться к нескольким узлам по всему миру.",
"settingsAbout": "О",
@@ -99,22 +107,40 @@
"settingsFailedToSave": "Не удалось сохранить настройки",
"settingsStatsCleared": "Статистика успешно очищена",
"settingsFailedToClearStats": "Не удалось очистить статистику",
"FirstName": "Имя",
"LastName": "Фамилия",
"YourPhoto": "Фото профиля",
"PhotoDescriptionText": "Это фото будет отображаться на странице вашего профиля.",
"EmailDescriptionText": "Ваш текущий email—его нельзя изменить.",
"DeleteAccountTitle": "Удалить аккаунт",
"DeleteAccountButton": "Удалить аккаунт",
"DeleteDescriptionText": "Это удалит аккаунт и все связанные данные с сервера. Это необратимо.",
"DeleteAccountWarning": "Удаление аккаунта означает, что вы не сможете снова войти в систему, и все ваши данные будут удалены. Это необратимо.",
"DeleteWarningTitle": "Действительно удалить этот аккаунт?",
"settingsDemoMonitorsAdded": "Успешно добавлены демонстрационные мониторы",
"settingsFailedToAddDemoMonitors": "Не удалось добавить демонстрационные мониторы",
"settingsMonitorsDeleted": "Успешно удалены все мониторы",
"settingsFailedToDeleteMonitors": "Не удалось удалить все мониторы",
"backendUnreachable": "Ошибка подключения к серверу",
"backendUnreachableMessage": "Мы не можем подключиться к серверу. Пожалуйста, проверьте ваше интернет-соединение или проверьте конфигурацию развертывания, если проблема не устраняется.",
"backendUnreachableError": "Невозможно подключиться к серверу. Пожалуйста, повторите попытку позже.",
"retryConnection": "Повторить подключение",
"retryingConnection": "Подключение...",
"backendReconnected": "Успешно восстановлено подключение к серверу.",
"backendStillUnreachable": "Сервер по-прежнему недоступен. Пожалуйста, повторите попытку позже.",
"backendConnectionError": "Ошибка подключения к серверу. Пожалуйста, проверьте ваше сетевое подключение.",
"starPromptTitle": "Star Checkmate",
"starPromptDescription": "Ознакомьтесь с последними релизами и помогите развить сообщество на GitHub",
"https": "HTTPS",
"http": "HTTP",
"monitor": "монитор",
"aboutus": "О Нас",
"signUP": "Зарегистрироваться",
"now": "Сейчас",
"delete": "Удалить",
"configure": "Настроить",
"networkError": "Ошибка сети",
"responseTime": "Время ответа:",
"responseTime": "Время ответа",
"ms": "мс",
"bar": "Bar",
"area": "Area",
@@ -368,4 +394,4 @@
"pageSpeedWarning": "Предупреждение: Вы не добавили ключ API Google PageSpeed. Без него монитор PageSpeed не будет работать.",
"pageSpeedLearnMoreLink": "Нажмите здесь, чтобы узнать",
"pageSpeedAddApiKey": "как добавить ваш ключ API."
}
}
+43 -26
View File
@@ -1,5 +1,7 @@
{
"dontHaveAccount": "Hesabınız yok mu",
"doNotHaveAccount": "Hesabınız yok mu?",
"registerHere": "Buradan kayıt olun",
"email": "E-posta",
"forgotPassword": "Parolamı unuttum",
"password": "Parola",
@@ -15,9 +17,11 @@
"authForgotPasswordTitle": "Parolanızı mı unuttunuz?",
"authForgotPasswordResetPassword": "Parola sıfırla",
"createPassword": "Parolanızı oluşturun",
"createAPassword": "Bir parola oluşturun",
"createAPassword": "Parola",
"authRegisterAlreadyHaveAccount": "Zaten hesabınız var mı?",
"authRegisterLoginLink": "Giriş Yap",
"commonAppName": "Checkmate",
"welcomeBack": "Tekrar hoş geldiniz! Başarıyla giriş yaptınız.",
"authLoginEnterEmail": "E-posta adresinizi girin",
"authRegisterTitle": "Hesap oluştur",
"authRegisterStepOneTitle": "Hesabınızı oluşturun",
@@ -40,7 +44,8 @@
"authSetNewPasswordDescription": "Yeni şifreniz daha önce kullanılan şifrelerden farklı olmalıdır.",
"authSetNewPasswordNewPassword": "Yeni şifre",
"authSetNewPasswordConfirmPassword": "Parolayı onayla",
"confirmPassword": "Parolanızı onaylayın",
"confirmPassword": "Onaylamak için parolayı tekrar girin",
"confirmNewPasswordPlaceholder": "Yeni parolanızı onaylayın",
"authSetNewPasswordResetPassword": "Parola sıfırla",
"authSetNewPasswordBackTo": "Geri dön",
"authPasswordMustBeAtLeast": "En az",
@@ -51,7 +56,7 @@
"authPasswordUpperCharacter": "bir büyük harf",
"authPasswordLowerCharacter": "bir küçük harf",
"authPasswordConfirmAndPassword": "Onay şifresi ve şifre",
"authPasswordMustMatch": "eşleşmelidir",
"authPasswordMustMatch": "Parolalar eşleşmelidir",
"authRegisterCreateAccount": "Hesap oluşturmak için devam et",
"authRegisterCreateSuperAdminAccount": "Super admin hesabınızı oluşturmak için devam edin",
"authRegisterSignUpWithEmail": "E-posta ile kayıt ol",
@@ -60,7 +65,7 @@
"distributedStatusSubHeaderText": "Dünya çapında milyonlarca cihaz tarafından desteklenen sistem performansını küresel bölgeye, ülkeye veya şehre göre görüntüleyin",
"settingsGeneralSettings": "Genel ayarlar",
"settingsDisplayTimezone": "Görüntüleme saat dilimi",
"settingsDisplayTimezoneDescription": "Herkese açık olarak görüntülediğiniz kontrol panelinin saat dilimi.",
"settingsDisplayTimezoneDescription": "Uygulama genelinde tarih ve saatlerin görüntülenmesi için kullanılacak saat dilimini seçin.",
"settingsAppearance": "Görünüm",
"settingsAppearanceDescription": "Açık ve koyu mod arasında geçiş yapın veya kullanıcı arayüzü dilini değiştirin",
"settingsThemeMode": "Tema",
@@ -69,23 +74,25 @@
"settingsDistributedUptimeDescription": "Dağıtılmış çalışma süresi izlemeyi etkinleştirin/devre dışı bırakın.",
"settingsEnabled": "Etkin",
"settingsDisabled": "Devre dışı",
"settingsHistoryAndMonitoring": "Geçmiş ve izleme",
"settingsHistoryAndMonitoringDescription": "Verileri ne kadar süreyle saklamak istediğinizi burada tanımlayın. Ayrıca tüm geçmiş verileri kaldırabilirsiniz.",
"settingsHistoryAndMonitoring": "İzleme geçmişi",
"settingsHistoryAndMonitoringDescription": "Geçmiş verileri ne kadar süre saklamak istediğinizi tanımlayın. Ayrıca mevcut tüm verileri temizleyebilirsiniz.",
"settingsTTLLabel": "İzleme geçmişini saklamak istediğiniz gün sayısı.",
"settingsTTLOptionalLabel": "Sınırsız için 0",
"settingsClearAllStats": "Tüm istatistikleri temizle. Bu geri alınamaz.",
"settingsClearAllStatsButton": "Tüm istatistikleri temizle",
"settingsClearAllStatsDialogTitle": "Tüm istatistikleri temizlemek istiyor musunuz?",
"settingsClearAllStatsDialogDescription": "Silindikten sonra, monitörleriniz geri alınamaz.",
"settingsClearAllStatsDialogDescription": "Kaldırıldıktan sonra, izleme geçmişi ve istatistikleri geri alınamaz.",
"settingsClearAllStatsDialogConfirm": "Evet, tüm istatistikleri temizle",
"settingsDemoMonitors": "Demo monitörler",
"settingsDemoMonitorsDescription": "Burada demo monitörler ekleyebilir ve kaldırabilirsiniz.",
"settingsAddDemoMonitors": "Demo monitörler ekle",
"settingsDemoMonitorsDescription": "Gösterim amaçlı örnek monitörler ekleyin.",
"settingsAddDemoMonitors": "Demo monitörler ekleniyor",
"settingsAddDemoMonitorsButton": "Demo monitörler ekle",
"settingsRemoveAllMonitors": "Tüm monitörleri kaldır",
"settingsSystemReset": "Sistem sıfırlama",
"settingsSystemResetDescription": "Sisteminizden tüm monitörleri kaldırın.",
"settingsRemoveAllMonitors": "Tüm monitörler kaldırılıyor",
"settingsRemoveAllMonitorsButton": "Tüm monitörleri kaldır",
"settingsRemoveAllMonitorsDialogTitle": "Tüm monitörleri kaldırmak istiyor musunuz?",
"settingsRemoveAllMonitorsDialogConfirm": "Evet, tüm monitörleri temizle",
"settingsRemoveAllMonitorsDialogConfirm": "Evet, tüm monitörleri kaldır",
"settingsWallet": "Cüzdan",
"settingsWalletDescription": "Cüzdanınızı buradan bağlayın. Bu, Dağıtılmış Çalışma Süresi monitörünün küresel olarak birden çok düğüme bağlanması için gereklidir.",
"settingsAbout": "Hakkında",
@@ -99,18 +106,26 @@
"settingsFailedToAddDemoMonitors": "Demo monitörler eklenemedi",
"settingsMonitorsDeleted": "Tüm monitörler başarıyla silindi",
"settingsFailedToDeleteMonitors": "Monitörler silinemedi",
"backendUnreachable": "Sunucu Bağlantı Hatası",
"backendUnreachableMessage": "Sunucuya bağlanamıyoruz. Lütfen internet bağlantınızı kontrol edin veya sorun devam ederse dağıtım yapılandırmanızı doğrulayın.",
"backendUnreachableError": "Sunucuya bağlanılamıyor. Lütfen daha sonra tekrar deneyin.",
"retryConnection": "Bağlantıyı Yeniden Dene",
"retryingConnection": "Bağlanıyor...",
"backendReconnected": "Sunucuya başarıyla yeniden bağlandı.",
"backendStillUnreachable": "Sunucu hala erişilemez durumda. Lütfen daha sonra tekrar deneyin.",
"backendConnectionError": "Sunucuya bağlanırken hata oluştu. Lütfen ağ bağlantınızı kontrol edin.",
"starPromptTitle": "Checkmate yıldızla değerlendirin",
"starPromptDescription": "En son sürümleri görün ve GitHub'daki topluluğun büyümesine yardımcı olun",
"https": "HTTPS",
"http": "HTTP",
"monitor": "monitör",
"aboutus": "Hakkımızda",
"signUP": "Hesap Oluştur",
"now": "Şimdi",
"delete": "Sil",
"configure": "Yapılandır",
"networkError": "Ağ hatası",
"responseTime": "Yanıt süresi:",
"responseTime": "Yanıt süresi",
"ms": "ms",
"bar": "Çubuk",
"area": "Alan",
@@ -386,20 +401,22 @@
"DragandDrop": "",
"MaxSize": "",
"SupportedFormats": "",
"FirstName": "",
"LastName": "",
"EmailDescriptionText": "",
"YourPhoto": "",
"PhotoDescriptionText": "",
"FirstName": "Ad",
"LastName": "Soyad",
"EmailDescriptionText": "Mevcut e-postanız—değiştirilemez.",
"YourPhoto": "Profil fotoğrafı",
"PhotoDescriptionText": "Bu fotoğraf profil sayfanızda görüntülenecektir.",
"save": "",
"DeleteAccount": "",
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "",
"authRegisterFirstName": "",
"authRegisterLastName": "",
"authRegisterEmail": "",
"authRegisterEmailRequired": "",
"DeleteAccountTitle": "Hesabı kaldır",
"DeleteAccountButton": "Hesabı kaldır",
"DeleteDescriptionText": "Bu, hesabı ve tüm ilişkili verileri sunucudan kaldıracaktır. Bu geri alınamaz.",
"DeleteAccountWarning": "Hesabınızı kaldırmak, tekrar oturum açamayacağınız ve tüm verilerinizin kaldırılacağı anlamına gelir. Bu geri alınamaz.",
"DeleteWarningTitle": "Bu hesabı gerçekten kaldırmak istiyor musunuz?",
"authRegisterFirstName": "Ad",
"authRegisterLastName": "Soyad",
"authRegisterEmail": "E-posta",
"authRegisterEmailRequired": "Devam etmek için lütfen e-posta adresinizi girin",
"authRegisterEmailInvalid": "Lütfen geçerli bir e-posta adresi girin",
"bulkImport": {
"title": "",
"selectFileTips": "",
+1 -1
View File
@@ -5,7 +5,7 @@ cd "$(dirname "$0")"
cd ../..
# Define service names and their corresponding Dockerfiles in parallel arrays
services=("uptime_client" "uptime_database_mongo" "uptime_redis" "uptime_server")
services=("uptime_client" "uptime_mongo" "uptime_redis" "uptime_server")
dockerfiles=(
"./docker/dev/client.Dockerfile"
"./docker/dev/mongoDB.Dockerfile"
+7 -4
View File
@@ -18,10 +18,13 @@ COPY ./client/package*.json ./
RUN npm install
COPY ./client .
COPY ./client ./
RUN npm run build-dev
RUN npm run build
RUN npm install -g serve
FROM nginx:1.27.1-alpine
CMD ["serve","-s", "dist", "-l", "5173"]
COPY --from=build /app/dist /usr/share/nginx/html
COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh
RUN chmod +x /docker-entrypoint.d/env.sh
CMD ["nginx", "-g", "daemon off;"]
+15 -4
View File
@@ -3,8 +3,12 @@ services:
image: uptime_client:latest
restart: always
ports:
- "5173:5173"
- "80:80"
environment:
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
UPTIME_APP_CLIENT_HOST: "http://localhost"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d/
depends_on:
- server
server:
@@ -31,10 +35,17 @@ services:
retries: 5
start_period: 5s
mongodb:
image: uptime_database_mongo:latest
image: uptime_mongo:latest
restart: always
command: ["mongod", "--quiet"]
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
ports:
- "27017:27017"
volumes:
- ./mongo/data:/data/db
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet
interval: 5s
timeout: 30s
start_period: 0s
start_interval: 1s
retries: 30
+35
View File
@@ -0,0 +1,35 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name localhost; # Set server name to localhost
server_tokens off;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# location /api/ {
# proxy_pass http://server:5000/api/;
# proxy_http_version 1.1;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_set_header Connection '';
# chunked_transfer_encoding off;
# proxy_buffering off;
# proxy_cache off;
# }
location /api-docs/ {
proxy_pass http://server:5000/api-docs/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
+1
View File
@@ -4,6 +4,7 @@ services:
restart: always
environment:
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
UPTIME_APP_CLIENT_HOST: "http://localhost"
ports:
- "80:80"
- "443:443"
+1 -1
View File
@@ -4,7 +4,7 @@ services:
restart: always
environment:
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
UPTIME_APP_CLIENT_HOST: "https://checkmate-demo.bluewavelabs.ca"
ports:
- "80:80"
- "443:443"
+1 -1
View File
@@ -4,7 +4,7 @@ services:
restart: always
environment:
UPTIME_APP_API_BASE_URL: "https://checkmate-test.bluewavelabs.ca/api/v1"
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
UPTIME_APP_CLIENT_HOST: "https://checkmate-test.bluewavelabs.ca"
ports:
- "80:80"
- "443:443"
+51 -8
View File
@@ -27,11 +27,12 @@ const SERVICE_NAME = "monitorController";
import pkg from "papaparse";
class MonitorController {
constructor(db, settingsService, jobQueue, stringService) {
constructor(db, settingsService, jobQueue, stringService, emailService) {
this.db = db;
this.settingsService = settingsService;
this.jobQueue = jobQueue;
this.stringService = stringService;
this.emailService = emailService;
}
/**
@@ -537,18 +538,16 @@ class MonitorController {
const monitorBeforeEdit = await this.db.getMonitorById(monitorId);
// Get notifications from the request body
const notifications = req.body.notifications;
const notifications = req.body.notifications ?? [];
const editedMonitor = await this.db.editMonitor(monitorId, req.body);
await this.db.deleteNotificationsByMonitorId(editedMonitor._id);
await Promise.all(
notifications &&
notifications.map(async (notification) => {
notification.monitorId = editedMonitor._id;
await this.db.createNotification(notification);
})
notifications.map(async (notification) => {
notification.monitorId = editedMonitor._id;
await this.db.createNotification(notification);
})
);
// Delete the old job(editedMonitor has the same ID as the old monitor)
@@ -636,6 +635,50 @@ class MonitorController {
}
};
/**
* Sends a test email to verify email delivery functionality.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.to - The email address to send the test email to.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status and the email delivery message ID.
* @throws {Error} If there is an error while sending the test email.
*/
sendTestEmail = async (req, res, next) => {
try {
const { to } = req.body;
if (!to || typeof to !== "string") {
return res.error({ msg: this.stringService.errorForValidEmailAddress });
}
const subject = this.stringService.testEmailSubject;
const context = { testName: "Monitoring System" };
const messageId = await this.emailService.buildAndSendEmail(
"testEmailTemplate",
context,
to,
subject
);
if (!messageId) {
return res.error({
msg: "Failed to send test email.",
});
}
return res.success({
msg: this.stringService.sendTestEmail,
data: { messageId },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "sendTestEmail"));
}
};
getMonitorsByTeamId = async (req, res, next) => {
try {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
-4
View File
@@ -7,10 +7,6 @@ const AppSettingsSchema = mongoose.Schema(
required: true,
default: "http://localhost:5000/api/v1",
},
language: {
type: String,
default: "en",
},
logLevel: {
type: String,
default: "debug",
+4
View File
@@ -39,6 +39,10 @@ const MonitorSchema = mongoose.Schema(
"distributed_test",
],
},
ignoreTlsErrors: {
type: Boolean,
default: false,
},
jsonPath: {
type: String,
},
+19 -13
View File
@@ -81,7 +81,9 @@ import ServiceRegistry from "./service/serviceRegistry.js";
import MongoDB from "./db/mongo/MongoDB.js";
// Redis Service and dependencies
import IORedis from "ioredis";
import RedisService from "./service/redisService.js";
import TranslationService from "./service/translationService.js";
import languageMiddleware from "./middleware/languageMiddleware.js";
@@ -113,21 +115,16 @@ const shutdown = async () => {
method: "shutdown",
});
// flush Redis
const settings =
ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings() || {};
const redisService = ServiceRegistry.get(RedisService.SERVICE_NAME);
await redisService.flushall();
const { redisUrl } = settings;
const redis = new IORedis(redisUrl, { maxRetriesPerRequest: null });
logger.info({ message: "Flushing Redis" });
await redis.flushall();
logger.info({ message: "Redis flushed" });
process.exit(1);
}, SHUTDOWN_TIMEOUT);
try {
server.close();
await ServiceRegistry.get(JobQueue.SERVICE_NAME).obliterate();
await ServiceRegistry.get(MongoDB.SERVICE_NAME).disconnect();
await ServiceRegistry.get(RedisService.SERVICE_NAME).flushall();
logger.info({ message: "Graceful shutdown complete" });
process.exit(0);
} catch (error) {
@@ -185,7 +182,13 @@ const startApp = async () => {
stringService
);
const jobQueue = new JobQueue(
const redisService = await RedisService.createInstance({
logger,
IORedis,
SettingsService: settingsService,
});
const jobQueue = new JobQueue({
db,
statusService,
networkService,
@@ -194,8 +197,9 @@ const startApp = async () => {
stringService,
logger,
Queue,
Worker
);
Worker,
redisService,
});
// Register services
ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue);
@@ -207,6 +211,7 @@ const startApp = async () => {
ServiceRegistry.register(StatusService.SERVICE_NAME, statusService);
ServiceRegistry.register(NotificationService.SERVICE_NAME, notificationService);
ServiceRegistry.register(TranslationService.SERVICE_NAME, translationService);
ServiceRegistry.register(RedisService.SERVICE_NAME, redisService);
await translationService.initialize();
@@ -231,7 +236,8 @@ const startApp = async () => {
ServiceRegistry.get(MongoDB.SERVICE_NAME),
ServiceRegistry.get(SettingsService.SERVICE_NAME),
ServiceRegistry.get(JobQueue.SERVICE_NAME),
ServiceRegistry.get(StringService.SERVICE_NAME)
ServiceRegistry.get(StringService.SERVICE_NAME),
ServiceRegistry.get(EmailService.SERVICE_NAME),
);
const settingsController = new SettingsController(
@@ -343,7 +349,7 @@ const startApp = async () => {
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
app.use("/api/v1/distributed-uptime", distributedUptimeRoutes.getRouter());
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter());
app.use("/api/v1/health", (req, res) => {
res.json({
+4 -1
View File
@@ -158,5 +158,8 @@
"platformRequired": "Platform is required",
"testNotificationFailed": "Failed to send test notification",
"monitorUpAlert": "Uptime Alert: One of your monitors is back online.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: UP\n📟 Status Code: {code}\n\u200B\n",
"monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n"
"monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n",
"sendTestEmail": "Test email sent successfully",
"errorForValidEmailAddress": "A valid recipient email address is required.",
"testEmailSubject": "Test Email from Monitoring System"
}
+3 -9
View File
@@ -1,16 +1,10 @@
import logger from "../utils/logger.js";
const languageMiddleware =
(stringService, translationService, settingsService) => async (req, res, next) => {
(stringService, translationService) => async (req, res, next) => {
try {
const settings = await settingsService.getSettings();
let language = settings && settings.language ? settings.language : null;
if (!language) {
const acceptLanguage = req.headers["accept-language"] || "en";
language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase();
}
const acceptLanguage = req.headers["accept-language"] || "en";
const language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase();
translationService.setLanguage(language);
stringService.setLanguage(language);
+6
View File
@@ -99,6 +99,12 @@ class MonitorRoutes {
);
this.router.post("/seed", isAllowed(["superadmin"]), this.monitorController.seedDb);
this.router.post(
"/test-email",
isAllowed(["admin", "superadmin"]),
this.monitorController.sendTestEmail
);
}
getRouter() {
+1
View File
@@ -67,6 +67,7 @@ class EmailService {
serverIsUpTemplate: this.loadTemplate("serverIsUp"),
passwordResetTemplate: this.loadTemplate("passwordReset"),
hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"),
testEmailTemplate: this.loadTemplate("testEmailTemplate")
};
/**
+6 -10
View File
@@ -1,5 +1,3 @@
import IORedis from "ioredis";
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware", "distributed"];
const SERVICE_NAME = "JobQueue";
const JOBS_PER_WORKER = 5;
@@ -18,7 +16,7 @@ const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`;
class NewJobQueue {
static SERVICE_NAME = SERVICE_NAME;
constructor(
constructor({
db,
statusService,
networkService,
@@ -27,16 +25,14 @@ class NewJobQueue {
stringService,
logger,
Queue,
Worker
) {
const settings = settingsService.getSettings() || {};
const { redisUrl } = settings;
const connection = new IORedis(redisUrl, { maxRetriesPerRequest: null });
Worker,
redisService,
}) {
this.connection = redisService.getConnection();
this.queues = {};
this.workers = {};
this.lastJobProcessedTime = {};
this.connection = connection;
this.db = db;
this.networkService = networkService;
this.statusService = statusService;
@@ -47,7 +43,7 @@ class NewJobQueue {
this.stringService = stringService;
QUEUE_NAMES.forEach((name) => {
const q = new Queue(name, { connection });
const q = new Queue(name, { connection: this.connection });
this.lastJobProcessedTime[q.name] = Date.now();
q.on("error", (error) => {
this.logger.error({
+9
View File
@@ -1,4 +1,6 @@
import jmespath from "jmespath";
import https from "https";
const SERVICE_NAME = "NetworkService";
const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push";
@@ -133,6 +135,7 @@ class NetworkService {
name,
teamId,
type,
ignoreTlsErrors,
jsonPath,
matchMethod,
expectedValue,
@@ -141,6 +144,12 @@ class NetworkService {
secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` });
if (ignoreTlsErrors === true) {
config.httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
}
const { response, responseTime, error } = await this.timeRequest(() =>
this.axios.get(url, config)
);
+72
View File
@@ -0,0 +1,72 @@
class RedisService {
static SERVICE_NAME = "RedisService";
constructor({ logger, IORedis, SettingsService }) {
this.logger = logger;
this.IORedis = IORedis;
this.SettingsService = SettingsService;
this.connection = null;
}
static async createInstance({ logger, IORedis, SettingsService }) {
const instance = new RedisService({ logger, IORedis, SettingsService });
await instance.connect();
return instance;
}
async connect() {
const settings = this.SettingsService.getSettings();
const { redisUrl } = settings;
this.connection = new this.IORedis(redisUrl, {
maxRetriesPerRequest: null,
retryStrategy: (times) => {
if (times >= 5) {
throw new Error("Failed to connect to Redis");
}
this.logger.debug({
message: "Retrying Redis connection",
service: RedisService.SERVICE_NAME,
details: { times },
});
return Math.min(times * 100, 2000);
},
});
await new Promise((resolve, reject) => {
let errorOccurred = false;
this.connection.on("ready", () => {
if (!errorOccurred) {
this.logger.info({
message: "Redis connection established",
service: RedisService.SERVICE_NAME,
});
resolve();
}
});
this.connection.on("error", (err) => {
errorOccurred = true;
this.logger.error({
message: "Redis connection error",
service: RedisService.SERVICE_NAME,
error: err,
});
setTimeout(() => reject(err), 5000);
});
});
}
async flushall() {
this.logger.debug({
message: "Flushing all Redis data",
service: RedisService.SERVICE_NAME,
});
await this.connection.flushall();
}
getConnection() {
return this.connection;
}
}
export default RedisService;
-1
View File
@@ -3,7 +3,6 @@ import dotenv from "dotenv";
dotenv.config();
const envConfig = {
logLevel: process.env.LOG_LEVEL,
language: process.env.LANGUAGE,
clientHost: process.env.CLIENT_HOST,
jwtSecret: process.env.JWT_SECRET,
dbType: process.env.DB_TYPE,
+17
View File
@@ -0,0 +1,17 @@
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text font-size="20px" font-family="Helvetica Neue">
Hello!
</mj-text>
<mj-text font-size="16px" font-family="Helvetica Neue">
This is a test email from the Monitoring System.
</mj-text>
<mj-text font-size="14px" font-family="Helvetica Neue">
If you're receiving this, your email configuration is working correctly.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
+21 -1
View File
@@ -1,4 +1,6 @@
import { createLogger, format, transports } from "winston";
import dotenv from "dotenv";
dotenv.config();
class Logger {
constructor() {
@@ -40,8 +42,10 @@ class Logger {
}
);
const logLevel = process.env.LOG_LEVEL || "info";
this.logger = createLogger({
level: "info",
level: logLevel,
format: format.combine(format.timestamp()),
transports: [
new transports.Console({
@@ -107,6 +111,22 @@ class Logger {
stack: config.stack,
});
}
/**
* Logs a debug message.
* @param {Object} config - The configuration object.
* @param {string} config.message - The message to log.
* @param {string} config.service - The service name.
* @param {string} config.method - The method name.
* @param {Object} config.details - Additional details.
*/
debug(config) {
this.logger.debug(config.message, {
service: config.service,
method: config.method,
details: config.details,
stack: config.stack,
});
}
}
const logger = new Logger();
+2
View File
@@ -183,6 +183,7 @@ const createMonitorBodyValidation = joi.object({
description: joi.string().required(),
type: joi.string().required(),
url: joi.string().required(),
ignoreTlsErrors: joi.boolean().default(false),
port: joi.number(),
isActive: joi.boolean(),
interval: joi.number(),
@@ -207,6 +208,7 @@ const editMonitorBodyValidation = joi.object({
interval: joi.number(),
notifications: joi.array().items(joi.object()),
secret: joi.string(),
ignoreTlsErrors: joi.boolean(),
jsonPath: joi.string().allow(""),
expectedValue: joi.string().allow(""),
matchMethod: joi.string(),