From efcd7d116292a5c1de7406ee77e40a9ed1f81547 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 13:24:47 -0700 Subject: [PATCH 01/18] update validation --- server/validation/joi.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/server/validation/joi.js b/server/validation/joi.js index bd402fb8a..b28ed3850 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -424,15 +424,7 @@ const editMaintenanceByIdWindowBodyValidation = joi.object({ // SettingsValidation //**************************************** const updateAppSettingsBodyValidation = joi.object({ - apiBaseUrl: joi.string().allow(""), - logLevel: joi.string().valid("debug", "none", "error", "warn").allow(""), - clientHost: joi.string().allow(""), - dbType: joi.string().allow(""), - dbConnectionString: joi.string().allow(""), - redisHost: joi.string().allow(""), - redisPort: joi.number().allow(null, ""), - redisUrl: joi.string().allow(""), - jwtTTL: joi.string().allow(""), + checkTTL: joi.number().allow(""), pagespeedApiKey: joi.string().allow(""), language: joi.string().allow(""), systemEmailHost: joi.string().allow(""), From cdb7e6fb9adb61e953e992c437c93c8e7a0738c6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 13:25:50 -0700 Subject: [PATCH 02/18] create settings if it doesn't exist --- server/service/settingsService.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/server/service/settingsService.js b/server/service/settingsService.js index 548d76eee..290e645e9 100755 --- a/server/service/settingsService.js +++ b/server/service/settingsService.js @@ -59,7 +59,17 @@ class SettingsService { } async getDBSettings() { - const settings = await this.appSettings.findOne({ singleton: true }).lean(); + let settings = await this.appSettings + .findOne({ singleton: true }) + .select("-__v -_id -createdAt -updatedAt -singleton") + .lean(); + if (settings === null) { + await this.appSettings.create({}); + settings = await this.appSettings + .findOne({ singleton: true }) + .select("-__v -_id -createdAt -updatedAt -singleton") + .lean(); + } return settings; } } From e4c5457518dbe7218fd0a0b8530b4b4f66d0836c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 13:26:17 -0700 Subject: [PATCH 03/18] update model --- server/db/models/AppSettings.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/db/models/AppSettings.js b/server/db/models/AppSettings.js index 3942305e8..1113bbe46 100755 --- a/server/db/models/AppSettings.js +++ b/server/db/models/AppSettings.js @@ -2,13 +2,16 @@ import mongoose from "mongoose"; const AppSettingsSchema = mongoose.Schema( { + checkTTL: { + type: Number, + default: 30, + }, language: { type: String, default: "gb", }, pagespeedApiKey: { type: String, - default: "", }, systemEmailHost: { type: String, From 217268b42f4c8adc92ef40b0573fc01f775e77cb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 13:26:39 -0700 Subject: [PATCH 04/18] unset API key if --- server/db/mongo/modules/settingsModule.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/server/db/mongo/modules/settingsModule.js b/server/db/mongo/modules/settingsModule.js index b817d2a32..2c1b402c3 100755 --- a/server/db/mongo/modules/settingsModule.js +++ b/server/db/mongo/modules/settingsModule.js @@ -14,12 +14,17 @@ const getAppSettings = async () => { const updateAppSettings = async (newSettings) => { try { - console.log(newSettings); - const settings = await AppSettings.findOneAndUpdate( - {}, - { $set: newSettings }, - { new: true, upsert: true } - ); + const update = { $set: { ...newSettings } }; + + if (newSettings.pagespeedApiKey === "") { + update.$unset = { pagespeedApiKey: "" }; + delete update.$set.pagespeedApiKey; + } + + const settings = await AppSettings.findOneAndUpdate({}, update, { + new: true, + upsert: true, + }); return settings; } catch (error) { error.service = SERVICE_NAME; From a6ab7b652fb87f834e31217d21e1a45129e1218a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 13:27:30 -0700 Subject: [PATCH 05/18] update prop types --- client/src/Components/Inputs/Select/index.jsx | 2 +- client/src/Components/Inputs/TextInput/index.jsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/Components/Inputs/Select/index.jsx b/client/src/Components/Inputs/Select/index.jsx index 2aa979434..927247d77 100644 --- a/client/src/Components/Inputs/Select/index.jsx +++ b/client/src/Components/Inputs/Select/index.jsx @@ -156,7 +156,7 @@ const Select = ({ }; Select.propTypes = { - id: PropTypes.string.isRequired, + id: PropTypes.string, name: PropTypes.string, label: PropTypes.string, placeholder: PropTypes.string, diff --git a/client/src/Components/Inputs/TextInput/index.jsx b/client/src/Components/Inputs/TextInput/index.jsx index 58e0e17eb..bb2df5d21 100644 --- a/client/src/Components/Inputs/TextInput/index.jsx +++ b/client/src/Components/Inputs/TextInput/index.jsx @@ -141,9 +141,9 @@ TextInput.displayName = "TextInput"; TextInput.propTypes = { type: PropTypes.string, - id: PropTypes.string.isRequired, + id: PropTypes.string, name: PropTypes.string, - value: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), placeholder: PropTypes.string, isRequired: PropTypes.bool, isOptional: PropTypes.bool, From cba744f777690a08c34c2ff1d8c16bbdc4cc09bc Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 13:28:04 -0700 Subject: [PATCH 06/18] update validation --- client/src/Validation/validation.js | 47 ++++++++++++++++------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 0ace7b02c..6b6f85227 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -11,7 +11,8 @@ const nameSchema = joi .messages({ "string.empty": "Name is required", "string.max": "Name must be less than 50 characters", - "string.pattern.base": "Name must contain only letters, spaces, apostrophes, or hyphens" + "string.pattern.base": + "Name must contain only letters, spaces, apostrophes, or hyphens", }); const passwordSchema = joi @@ -152,20 +153,21 @@ const monitorValidation = joi.object({ "string.invalidUrl": "Please enter a valid URL with optional port", "string.pattern.base": "Please enter a valid container ID.", }), - port: joi.number() - .integer() - .min(1) - .max(65535) - .when("type", { - is: "port", - then: joi.required().messages({ - "number.base": "Port must be a number.", - "number.min": "Port must be at least 1.", - "number.max": "Port must be at most 65535.", - "any.required": "Port is required for port monitors.", + port: joi + .number() + .integer() + .min(1) + .max(65535) + .when("type", { + is: "port", + then: joi.required().messages({ + "number.base": "Port must be a number.", + "number.min": "Port must be at least 1.", + "number.max": "Port must be at most 65535.", + "any.required": "Port is required for port monitors.", + }), + otherwise: joi.optional(), }), - otherwise: joi.optional(), - }), name: joi.string().trim().max(50).allow("").messages({ "string.max": "This field should not exceed the 50 characters limit.", }), @@ -253,14 +255,14 @@ const statusPageValidation = joi.object({ showCharts: joi.boolean(), }); const settingsValidation = joi.object({ - ttl: joi.number().required().messages({ + checkTTL: joi.number().required().messages({ "string.empty": "Please enter a value", "number.base": "Please enter a valid number", - "any.required": "Please enter a value" + "any.required": "Please enter a value", }), pagespeedApiKey: joi.string().allow("").optional(), -}) -.unknown(true); + language: joi.string().required(), +}); const dayjsValidator = (value, helpers) => { if (!dayjs(value).isValid()) { @@ -363,9 +365,12 @@ const infrastructureMonitorValidation = joi.object({ notifications: joi.array().items( joi.object({ type: joi.string().valid("email").required(), - address: joi.string().email({ tlds: { allow: false } }).required(), + address: joi + .string() + .email({ tlds: { allow: false } }) + .required(), }) - ) + ), }); export { @@ -377,5 +382,5 @@ export { advancedSettingsValidation, infrastructureMonitorValidation, statusPageValidation, - logoImageValidation + logoImageValidation, }; From 6d79b1d8230d4a0ca9efe250263730d15052898c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 13:28:44 -0700 Subject: [PATCH 07/18] refactor settings page --- client/src/Pages/Settings/SettingsAbout.jsx | 32 + .../Pages/Settings/SettingsDemoMonitors.jsx | 98 +++ .../src/Pages/Settings/SettingsPagespeed.jsx | 81 +++ client/src/Pages/Settings/SettingsStats.jsx | 84 +++ .../src/Pages/Settings/SettingsTimeZone.jsx | 43 ++ client/src/Pages/Settings/SettingsUI.jsx | 52 ++ client/src/Pages/Settings/index.jsx | 604 ++++-------------- 7 files changed, 530 insertions(+), 464 deletions(-) create mode 100644 client/src/Pages/Settings/SettingsAbout.jsx create mode 100644 client/src/Pages/Settings/SettingsDemoMonitors.jsx create mode 100644 client/src/Pages/Settings/SettingsPagespeed.jsx create mode 100644 client/src/Pages/Settings/SettingsStats.jsx create mode 100644 client/src/Pages/Settings/SettingsTimeZone.jsx create mode 100644 client/src/Pages/Settings/SettingsUI.jsx diff --git a/client/src/Pages/Settings/SettingsAbout.jsx b/client/src/Pages/Settings/SettingsAbout.jsx new file mode 100644 index 000000000..b2b5df308 --- /dev/null +++ b/client/src/Pages/Settings/SettingsAbout.jsx @@ -0,0 +1,32 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import ConfigBox from "../../Components/ConfigBox"; +// Utils +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import Link from "../../Components/Link"; + +const SettingsAbout = () => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + + {t("settingsAbout")} + + + Checkmate {2.0} + + {t("settingsDevelopedBy")} + + + + + ); +}; + +export default SettingsAbout; diff --git a/client/src/Pages/Settings/SettingsDemoMonitors.jsx b/client/src/Pages/Settings/SettingsDemoMonitors.jsx new file mode 100644 index 000000000..6722e97ac --- /dev/null +++ b/client/src/Pages/Settings/SettingsDemoMonitors.jsx @@ -0,0 +1,98 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import ConfigBox from "../../Components/ConfigBox"; +// Utils +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useTranslation } from "react-i18next"; +import Dialog from "../../Components/Dialog"; +import { useState } from "react"; +const SettingsDemoMonitors = ({ + isLoading, + authIsLoading, + checksIsLoading, + handleChange, +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + // Local state + const [isOpen, setIsOpen] = useState(false); + return ( + <> + + + {t("settingsDemoMonitors")} + + {t("settingsDemoMonitorsDescription")} + + + + {t("settingsAddDemoMonitors")} + + + + + + {t("settingsSystemReset")} + + {t("settingsSystemResetDescription")} + + + + {t("settingsRemoveAllMonitors")} + + + setIsOpen(false)} + confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")} + onConfirm={() => { + const syntheticEvent = { + target: { + name: "deleteMonitors", + }, + }; + handleChange(syntheticEvent); + setIsOpen(false); + }} + isLoading={isLoading || authIsLoading || checksIsLoading} + /> + + + ); +}; + +SettingsDemoMonitors.propTypes = { + isLoading: PropTypes.bool, + authIsLoading: PropTypes.bool, + checksIsLoading: PropTypes.bool, + handleChange: PropTypes.func, +}; + +export default SettingsDemoMonitors; diff --git a/client/src/Pages/Settings/SettingsPagespeed.jsx b/client/src/Pages/Settings/SettingsPagespeed.jsx new file mode 100644 index 000000000..87f558f32 --- /dev/null +++ b/client/src/Pages/Settings/SettingsPagespeed.jsx @@ -0,0 +1,81 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import ConfigBox from "../../Components/ConfigBox"; +import TextInput from "../../Components/Inputs/TextInput"; +import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; +// Utils +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; + +const SettingsPagespeed = ({ HEADING_SX, settings, setSettings }) => { + const { t } = useTranslation(); + const theme = useTheme(); + + // Local state + const [apiKey, setApiKey] = useState(""); + const [isApiKeySet, setIsApiKeySet] = useState(false); + const [hasBeenReset, setHasBeenReset] = useState(false); + + // Handler + const handleChange = (e) => { + setApiKey(e.target.value); + setSettings({ ...settings, pagespeedApiKey: e.target.value }); + }; + + useEffect(() => { + console.log(settings); + setIsApiKeySet(Boolean(settings.pagespeedApiKey) && apiKey === ""); + }, [settings, apiKey]); + + return ( + + + {t("pageSpeedApiKeyFieldTitle")} + {t("pageSpeedApiKeyFieldDescription")} + + + {(isApiKeySet === false || hasBeenReset === true) && ( + } + /> + )} + + {isApiKeySet === true && hasBeenReset === false && ( + + {t("pageSpeedApiKeyFieldResetLabel")} + + + )} + + + ); +}; + +SettingsPagespeed.propTypes = { + HEADING_SX: PropTypes.object, + settings: PropTypes.object, + setSettings: PropTypes.func, +}; + +export default SettingsPagespeed; diff --git a/client/src/Pages/Settings/SettingsStats.jsx b/client/src/Pages/Settings/SettingsStats.jsx new file mode 100644 index 000000000..c9178b1da --- /dev/null +++ b/client/src/Pages/Settings/SettingsStats.jsx @@ -0,0 +1,84 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import ConfigBox from "../../Components/ConfigBox"; +import TextInput from "../../Components/Inputs/TextInput"; +import Dialog from "../../Components/Dialog"; + +// Utils +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; + +const SettingsStats = ({ HEADING_SX, handleChange, settings, errors }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + return ( + + + + {t("settingsHistoryAndMonitoring")} + + + {t("settingsHistoryAndMonitoringDescription")} + + + + + + {t("settingsClearAllStats")} + + + + setIsOpen(false)} + confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")} + onConfirm={() => { + const syntheticEvent = { + target: { + name: "deleteStats", + }, + }; + handleChange(syntheticEvent); + setIsOpen(false); + }} + isLoading={false} + /> + + ); +}; + +SettingsStats.propTypes = { + HEADING_SX: PropTypes.object, + handleChange: PropTypes.func, + settings: PropTypes.object, + errors: PropTypes.object, +}; + +export default SettingsStats; diff --git a/client/src/Pages/Settings/SettingsTimeZone.jsx b/client/src/Pages/Settings/SettingsTimeZone.jsx new file mode 100644 index 000000000..e6ba1a77b --- /dev/null +++ b/client/src/Pages/Settings/SettingsTimeZone.jsx @@ -0,0 +1,43 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import ConfigBox from "../../Components/ConfigBox"; +import Select from "../../Components/Inputs/Select"; +import timezones from "../../Utils/timezones.json"; + +// Utils +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useTranslation } from "react-i18next"; +const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + + {t("settingsGeneralSettings")} + + {t("settingsDisplayTimezone")}-{" "} + {t("settingsDisplayTimezoneDescription")} + + + + + + + + ); +}; + +SettingsUI.propTypes = { + HEADING_SX: PropTypes.object, + handleChange: PropTypes.func, + mode: PropTypes.string, + language: PropTypes.string, +}; + +export default SettingsUI; diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index 79e5ea657..e1e8c3859 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -1,116 +1,67 @@ -// Components -import { Box, Stack, Typography, Button } from "@mui/material"; -import TextInput from "../../Components/Inputs/TextInput"; -import Link from "../../Components/Link"; -import Select from "../../Components/Inputs/Select"; -import { useIsAdmin } from "../../Hooks/useIsAdmin"; -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"; - -//Utils -import { useTheme } from "@emotion/react"; -import { logger } from "../../Utils/Logger"; -import { useDispatch, useSelector } from "react-redux"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Breadcrumbs from "../../Components/Breadcrumbs"; +import SettingsTimeZone from "./SettingsTimeZone"; +import SettingsUI from "./SettingsUI"; +import SettingsPagespeed from "./SettingsPagespeed"; +import SettingsDemoMonitors from "./SettingsDemoMonitors"; +import SettingsAbout from "./SettingsAbout"; +import Button from "@mui/material/Button"; +// Utils +import { settingsValidation } from "../../Validation/validation"; import { createToast } from "../../Utils/toastUtils"; +import { useState } from "react"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { useSelector, useDispatch } from "react-redux"; +import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice"; +import { getAppSettings } from "../../Features/Settings/settingsSlice"; +import SettingsStats from "./SettingsStats"; import { deleteMonitorChecksByTeamId, addDemoMonitors, deleteAllMonitors, } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import { update } from "../../Features/Auth/authSlice"; -import PropTypes from "prop-types"; -import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice"; -import timezones from "../../Utils/timezones.json"; -import { useState, useEffect } from "react"; -import { networkService } from "../../main"; -import { settingsValidation } from "../../Validation/validation"; -import { updateAppSettings } from "../../Features/Settings/settingsSlice"; -import { useTranslation } from "react-i18next"; +import { useFetchSettings, useSaveSettings } from "../../Hooks/useFetchSettings"; +import { UseDeleteMonitorStats } from "../../Hooks/useDeleteMonitorStats"; // Constants -const SECONDS_PER_DAY = 86400; +const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }]; const Settings = () => { - const theme = useTheme(); - const { t, i18n } = useTranslation(); - const isAdmin = useIsAdmin(); + // Redux state + const { mode, language, timezone } = useSelector((state) => state.ui); const { user } = useSelector((state) => state.auth); - const { language } = useSelector((state) => state.ui); - const { checkTTL } = user; - const { isLoading } = useSelector((state) => state.uptimeMonitors); - const { isLoading: authIsLoading } = useSelector((state) => state.auth); - const { timezone, distributedUptimeEnabled } = useSelector((state) => state.ui); - const { mode } = useSelector((state) => state.ui); - const { pagespeedApiKey } = useSelector((state) => state.settings); - const [checksIsLoading, setChecksIsLoading] = useState(false); - const [form, setForm] = useState({ - enableDistributedUptime: distributedUptimeEnabled, - ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0, - pagespeedApiKey: pagespeedApiKey, + + // Local state + const [settings, setSettings] = useState({}); + const [errors, setErrors] = useState({}); + + // Network + const [isSettingsLoading, settingsError] = useFetchSettings({ + settings, + setSettings, }); - const [version, setVersion] = useState("unknown"); - const [apiKeyFieldType, setApiKeyFieldType] = useState("password"); - const [isApiKeySet, setIsApiKeySet] = useState(pagespeedApiKey ? true : false); - const [tempPSKey, setTempPSKey] = useState(""); - const [errors, setErrors] = useState({}); - const deleteStatsMonitorsInitState = { deleteMonitors: false, deleteStats: false }; - const [isOpen, setIsOpen] = useState(deleteStatsMonitorsInitState); + const [isSaving, saveError, saveSettings] = useSaveSettings(); + + const [deleteMonitorStats, isDeletingMonitorStats] = UseDeleteMonitorStats(); + + // Setup + const theme = useTheme(); + const HEADING_SX = { mt: theme.spacing(2), mb: theme.spacing(2) }; + const { t, i18n } = useTranslation(); const dispatch = useDispatch(); - //Fetching latest release version from github - useEffect(() => { - const fetchLatestVersion = async () => { - let version = "unknown"; - try { - const response = await networkService.fetchGithubLatestRelease(); - if (!response.status === 200) { - throw new Error("Failed to fetch latest version"); - } - version = response.data.tag_name; - } catch (error) { - createToast({ body: error.message || "Error fetching latest version" }); // Set error message - } finally { - setVersion(version); - } - }; - fetchLatestVersion(); - }, []); + // Handlers + const handleChange = async (e) => { + const { name, value } = e.target; - useEffect(() => { - dispatch(getAppSettings()); - }, [dispatch]); - - const handleChange = (event) => { - const { type, checked, value, id } = event.target; - - if (type === "checkbox") { - setForm((prev) => ({ - ...prev, - [id]: checked, - })); - return; - } - - if (id === "pagespeedApiKey") { - setTempPSKey(value); - return; - } - - let inputValue = value; - if (id === "ttl") { - inputValue = value.replace(/[^0-9]/g, ""); - } - - const updatedForm = { ...form, [id]: inputValue }; - const { error } = settingsValidation.validate(updatedForm, { abortEarly: false }); + // Build next state early + const newSettings = { ...settings, [name]: value }; + // Validate + const { error } = settingsValidation.validate(newSettings, { abortEarly: false }); if (!error || error.details.length === 0) { setErrors({}); } else { @@ -119,397 +70,122 @@ const Settings = () => { newErrors[err.path[0]] = err.message; }); setErrors(newErrors); - logger.error("Validation errors:", error.details); } - setForm(updatedForm); - }; - - // TODO Handle saving - const handleSave = async () => { - try { - setChecksIsLoading(true); - await networkService.updateChecksTTL({ - ttl: form.ttl, - }); - const updatedUser = { ...user, checkTTL: form.ttl }; - const [userAction, settingsAction] = await Promise.all([ - dispatch(update({ localData: updatedUser })), - dispatch( - updateAppSettings({ - settings: { - language: language, - pagespeedApiKey: tempPSKey ? tempPSKey : form.pagespeedApiKey, - }, - }) - ), - ]); - - if (userAction.payload.success && settingsAction.payload.success) { - createToast({ body: t("settingsSuccessSaved") }); - } else { - throw new Error("Failed to save settings"); - } - } catch (error) { - createToast({ body: t("settingsFailedToSave") }); - } finally { - setChecksIsLoading(false); + if (name === "timezone") { + dispatch(setTimezone({ timezone: value })); } - }; - const handleClearStats = async () => { - try { - const action = await dispatch(deleteMonitorChecksByTeamId({ teamId: user.teamId })); - - if (deleteMonitorChecksByTeamId.fulfilled.match(action)) { - createToast({ body: t("settingsStatsCleared") }); - } else { - createToast({ body: t("settingsFailedToClearStats") }); - } - } catch (error) { - logger.error(error); - createToast({ body: t("settingsFailedToClearStats") }); - } finally { - setIsOpen(deleteStatsMonitorsInitState); + if (name === "mode") { + dispatch(setMode(value)); } - }; - const handleInsertDemoMonitors = async () => { - try { - const action = await dispatch(addDemoMonitors()); - if (addDemoMonitors.fulfilled.match(action)) { - createToast({ body: t("settingsDemoMonitorsAdded") }); - } else { + if (name === "language") { + dispatch(setLanguage(value)); + i18n.changeLanguage(value); + } + + if (name === "deleteStats") { + await deleteMonitorStats({ teamId: user.teamId }); + return; + } + + if (name === "demo") { + try { + const action = await dispatch(addDemoMonitors()); + if (addDemoMonitors.fulfilled.match(action)) { + createToast({ body: t("settingsDemoMonitorsAdded") }); + } else { + createToast({ body: t("settingsFailedToAddDemoMonitors") }); + } + } catch (error) { createToast({ body: t("settingsFailedToAddDemoMonitors") }); } - } catch (error) { - logger.error(error); - createToast({ Body: t("settingsFailedToAddDemoMonitors") }); + return; } - }; - const handleDeleteAllMonitors = async () => { - try { - const action = await dispatch(deleteAllMonitors()); - if (deleteAllMonitors.fulfilled.match(action)) { - createToast({ body: t("settingsMonitorsDeleted") }); - } else { + if (name === "deleteMonitors") { + try { + const action = await dispatch(deleteAllMonitors()); + if (deleteAllMonitors.fulfilled.match(action)) { + createToast({ body: t("settingsMonitorsDeleted") }); + } else { + createToast({ body: t("settingsFailedToDeleteMonitors") }); + } + } catch (error) { createToast({ body: t("settingsFailedToDeleteMonitors") }); } - } catch (error) { - logger.error(error); - createToast({ Body: t("settingsFailedToDeleteMonitors") }); - } finally { - setIsOpen(deleteStatsMonitorsInitState); + return; } + + setSettings(newSettings); }; - const handleResetApiKey = () => { - setIsApiKeySet(false); - setForm((prev) => ({ - ...prev, - pagespeedApiKey: "", - })); + const handleSave = () => { + const { error } = settingsValidation.validate(settings, { abortEarly: false }); + if (!error || error.details.length === 0) { + setErrors({}); + } else { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + setErrors(newErrors); + } + saveSettings(settings); }; - const languages = Object.keys(i18n.options.resources || {}); - return ( - + + + Settings + + + + + + - - - {t("settingsGeneralSettings")} - - {t("settingsDisplayTimezone")}-{" "} - {t("settingsDisplayTimezoneDescription")} - - - - { - dispatch(setMode(e.target.value)); - }} - items={[ - { _id: "light", name: "Light" }, - { _id: "dark", name: "Dark" }, - ]} - > - - - - {/* {isAdmin && ( - - - {t("settingsDistributedUptime")} - - {t("settingsDistributedUptimeDescription")} - - - - { - dispatch(setDistributedUptimeEnabled(e.target.checked)); - }} - /> - {distributedUptimeEnabled === true - ? t("settingsEnabled") - : t("settingsDisabled")} - - - )} */} - - - {t("pageSpeedApiKeyFieldTitle")} - - {t("pageSpeedApiKeyFieldDescription")} - - - - - ) - } - /> - {isApiKeySet && ( - - {t("pageSpeedApiKeyFieldResetLabel")} - - - )} - - - {/* {isAdmin && ( - - - {t("settingsWallet")} - - {t("settingsWalletDescription")} - - - - - - - - - - )} */} - {isAdmin && ( - - - {t("settingsHistoryAndMonitoring")} - - {t("settingsHistoryAndMonitoringDescription")} - - - - - - {t("settingsClearAllStats")} - - - - setIsOpen(deleteStatsMonitorsInitState)} - confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")} - onConfirm={handleClearStats} - isLoading={isLoading || authIsLoading || checksIsLoading} - /> - - )} - {isAdmin && ( - <> - {/* Demo Monitors Section */} - - - {t("settingsDemoMonitors")} - - {t("settingsDemoMonitorsDescription")} - - - - {t("settingsAddDemoMonitors")} - - - - - {/* System Reset Section */} - - - {t("settingsSystemReset")} - - {t("settingsSystemResetDescription")} - - - - {t("settingsRemoveAllMonitors")} - - - setIsOpen(deleteStatsMonitorsInitState)} - confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")} - onConfirm={handleDeleteAllMonitors} - isLoading={isLoading || authIsLoading || checksIsLoading} - /> - - - )} - - - - {t("settingsAbout")} - - - Checkmate {version} - - {t("settingsDevelopedBy")} - - - - - 0} + variant="contained" + color="accent" + sx={{ px: theme.spacing(12), mt: theme.spacing(20) }} + onClick={handleSave} > - - + {t("settingsSave")} + - + ); }; -Settings.propTypes = { - isAdmin: PropTypes.bool, -}; export default Settings; From 10d575a7ce1f2a5605e32387aceedcf0a49b9510 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 13:29:05 -0700 Subject: [PATCH 08/18] add hooks --- client/src/Hooks/useDeleteMonitorStats.js | 24 ++++++++++ client/src/Hooks/useFetchSettings.js | 55 +++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 client/src/Hooks/useDeleteMonitorStats.js create mode 100644 client/src/Hooks/useFetchSettings.js diff --git a/client/src/Hooks/useDeleteMonitorStats.js b/client/src/Hooks/useDeleteMonitorStats.js new file mode 100644 index 000000000..64138929f --- /dev/null +++ b/client/src/Hooks/useDeleteMonitorStats.js @@ -0,0 +1,24 @@ +import { useState } from "react"; +import { networkService } from "../main"; +import { createToast } from "../Utils/toastUtils"; +import { useTranslation } from "react-i18next"; + +const UseDeleteMonitorStats = () => { + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const deleteMonitorStats = async ({ teamId }) => { + setIsLoading(true); + try { + const res = await networkService.deleteChecksByTeamId({ teamId }); + createToast({ body: t("settingsStatsCleared") }); + } catch (error) { + createToast({ body: t("settingsFailedToClearStats") }); + } finally { + setIsLoading(false); + } + }; + + return [deleteMonitorStats, isLoading]; +}; + +export { UseDeleteMonitorStats }; diff --git a/client/src/Hooks/useFetchSettings.js b/client/src/Hooks/useFetchSettings.js new file mode 100644 index 000000000..15859795f --- /dev/null +++ b/client/src/Hooks/useFetchSettings.js @@ -0,0 +1,55 @@ +import { useState, useEffect } from "react"; +import { networkService } from "../main"; +import { createToast } from "../Utils/toastUtils"; +import { useTranslation } from "react-i18next"; + +const useFetchSettings = ({ setSettings }) => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + useEffect(() => { + const fetchSettings = async () => { + setIsLoading(true); + try { + const response = await networkService.getAppSettings(); + setSettings(response?.data?.data); + } catch (error) { + createToast({ body: "Failed to fetch settings" }); + setError(error); + } finally { + setIsLoading(false); + } + }; + fetchSettings(); + }, []); + + return [isLoading, error]; +}; + +const useSaveSettings = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + const { t } = useTranslation(); + + const saveSettings = async (settings) => { + setIsLoading(true); + try { + await networkService.updateAppSettings({ settings }); + if (settings.checkTTL) { + await networkService.updateChecksTTL({ + ttl: settings.checkTTL, + }); + } + createToast({ body: t("settingsSuccessSaved") }); + } catch (error) { + createToast({ body: t("settingsFailedToSave") }); + + setError(error); + } finally { + setIsLoading(false); + } + }; + + return [isLoading, error, saveSettings]; +}; + +export { useFetchSettings, useSaveSettings }; From f7d780a8be597a5408cc6c76ead859337efad3f9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:52:09 -0700 Subject: [PATCH 09/18] add email settings component --- client/src/Pages/Settings/SettingsEmail.jsx | 126 ++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 client/src/Pages/Settings/SettingsEmail.jsx diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx new file mode 100644 index 000000000..ecc2a51f2 --- /dev/null +++ b/client/src/Pages/Settings/SettingsEmail.jsx @@ -0,0 +1,126 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import ConfigBox from "../../Components/ConfigBox"; +import TextInput from "../../Components/Inputs/TextInput"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +// Utils +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; +const SettingsEmail = ({ + HEADER_SX, + handleChange, + settingsData, + setSettingsData, + isPasswordSet, +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + + const [password, setPassword] = useState(""); + const [hasBeenReset, setHasBeenReset] = useState(false); + + const handlePasswordChange = (e) => { + setPassword(e.target.value); + setSettingsData({ + ...settingsData, + settings: { ...settingsData.settings, systemEmailPassword: e.target.value }, + }); + }; + + return ( + + + {t("settingsEmail")} + {t("settingsEmailDescription")} + + + + + {t("settingsEmailHost")} + + + + {t("settingsEmailPort")} + + + + {t("settingsEmailUser")} + + + + {t("settingsEmailAddress")} + + + {(isPasswordSet === false || hasBeenReset === true) && ( + + {t("settingsEmailPassword")} + } + /> + + )} + {isPasswordSet === true && hasBeenReset === false && ( + + {t("settingsEmailFieldResetLabel")} + + + )} + + + + ); +}; + +SettingsEmail.propTypes = { + settingsData: PropTypes.object, + setSettingsData: PropTypes.func, + handleChange: PropTypes.func, + HEADER_SX: PropTypes.object, + isPasswordSet: PropTypes.bool, +}; + +export default SettingsEmail; From d0ec67236d304b53cfb10e5d5b1dc802a5f0febd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:52:24 -0700 Subject: [PATCH 10/18] allow systememail user --- server/validation/joi.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/validation/joi.js b/server/validation/joi.js index b28ed3850..997d35d65 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -431,6 +431,7 @@ const updateAppSettingsBodyValidation = joi.object({ systemEmailPort: joi.number().allow(""), systemEmailAddress: joi.string().allow(""), systemEmailPassword: joi.string().allow(""), + systemEmailUser: joi.string().allow(""), }); //**************************************** From 0a723572a2de8c57925263287f85e5c1d243880c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:52:44 -0700 Subject: [PATCH 11/18] move transporter creation --- server/service/emailService.js | 49 +++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/server/service/emailService.js b/server/service/emailService.js index c9e34eeda..57f8c78e6 100755 --- a/server/service/emailService.js +++ b/server/service/emailService.js @@ -29,7 +29,10 @@ class EmailService { this.mjml2html = mjml2html; this.nodemailer = nodemailer; this.logger = logger; + this.init(); + } + init = async () => { /** * Loads an email template from the filesystem. * @@ -67,34 +70,14 @@ class EmailService { serverIsUpTemplate: this.loadTemplate("serverIsUp"), passwordResetTemplate: this.loadTemplate("passwordReset"), hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"), - testEmailTemplate: this.loadTemplate("testEmailTemplate") + testEmailTemplate: this.loadTemplate("testEmailTemplate"), }; /** * The email transporter used to send emails. * @type {Object} */ - - const { - systemEmailHost, - systemEmailPort, - systemEmailUser, - systemEmailAddress, - systemEmailPassword, - } = this.settingsService.getSettings(); - - const emailConfig = { - host: systemEmailHost, - port: systemEmailPort, - secure: true, - auth: { - user: systemEmailUser || systemEmailAddress, - pass: systemEmailPassword, - }, - }; - - this.transporter = this.nodemailer.createTransport(emailConfig); - } + }; /** * Asynchronously builds and sends an email using a specified template and context. @@ -106,6 +89,28 @@ class EmailService { * @returns {Promise} A promise that resolves to the messageId of the sent email. */ buildAndSendEmail = async (template, context, to, subject) => { + // TODO - Consider an update transporter method so this only needs to be recreated when smtp settings change + const { + systemEmailHost, + systemEmailPort, + systemEmailUser, + systemEmailAddress, + systemEmailPassword, + } = await this.settingsService.getDBSettings(); + + const emailConfig = { + host: systemEmailHost, + port: systemEmailPort, + secure: true, + auth: { + user: systemEmailUser || systemEmailAddress, + pass: systemEmailPassword, + }, + connectionTimeout: 5000, + }; + + this.transporter = this.nodemailer.createTransport(emailConfig); + const buildHtml = async (template, context) => { try { const mjml = this.templateLookup[template](context); From 0f80524127b80bb757126145cc7e0aea4ee206ce Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:53:03 -0700 Subject: [PATCH 12/18] unset email password --- server/db/mongo/modules/settingsModule.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/db/mongo/modules/settingsModule.js b/server/db/mongo/modules/settingsModule.js index 2c1b402c3..d3568ad32 100755 --- a/server/db/mongo/modules/settingsModule.js +++ b/server/db/mongo/modules/settingsModule.js @@ -21,6 +21,11 @@ const updateAppSettings = async (newSettings) => { delete update.$set.pagespeedApiKey; } + if (newSettings.systemEmailPassword === "") { + update.$unset = { systemEmailPassword: "" }; + delete update.$set.systemEmailPassword; + } + const settings = await AppSettings.findOneAndUpdate({}, update, { new: true, upsert: true, From fcd6af259eb873b12fe1c1e3b75d746f6da695a8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:53:20 -0700 Subject: [PATCH 13/18] add optional email user --- server/db/models/AppSettings.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/db/models/AppSettings.js b/server/db/models/AppSettings.js index 1113bbe46..06ba8c1df 100755 --- a/server/db/models/AppSettings.js +++ b/server/db/models/AppSettings.js @@ -25,6 +25,9 @@ const AppSettingsSchema = mongoose.Schema( systemEmailPassword: { type: String, }, + systemEmailUser: { + type: String, + }, singleton: { type: Boolean, required: true, From 36d78a9aa4ed607ca1bd2b5fdaca5a3927b2d287 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:53:36 -0700 Subject: [PATCH 14/18] update return type for settigns --- server/controllers/settingsController.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/server/controllers/settingsController.js b/server/controllers/settingsController.js index 4d561216e..b557e8dd6 100755 --- a/server/controllers/settingsController.js +++ b/server/controllers/settingsController.js @@ -13,12 +13,25 @@ class SettingsController { getAppSettings = async (req, res, next) => { const dbSettings = await this.settingsService.getDBSettings(); const sanitizedSettings = { ...dbSettings }; + + const returnSettings = { + pagespeedKeySet: false, + emailPasswordSet: false, + }; + if (typeof sanitizedSettings.pagespeedApiKey !== "undefined") { - sanitizedSettings.pagespeedApiKey = "********"; + returnSettings.pagespeedKeySet = true; + delete sanitizedSettings.pagespeedApiKey; } + if (typeof sanitizedSettings.systemEmailPassword !== "undefined") { + returnSettings.emailPasswordSet = true; + delete sanitizedSettings.systemEmailPassword; + } + + returnSettings.settings = sanitizedSettings; return res.success({ msg: this.stringService.getAppSettings, - data: sanitizedSettings, + data: returnSettings, }); }; From 342e67593b1d83e651642c21c70eeec4a5c087c6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:53:53 -0700 Subject: [PATCH 15/18] correct error handling --- server/controllers/monitorController.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/controllers/monitorController.js b/server/controllers/monitorController.js index 5ab654375..9a42ea4e5 100755 --- a/server/controllers/monitorController.js +++ b/server/controllers/monitorController.js @@ -649,9 +649,8 @@ class MonitorController { sendTestEmail = async (req, res, next) => { try { const { to } = req.body; - if (!to || typeof to !== "string") { - return res.error({ msg: this.stringService.errorForValidEmailAddress }); + throw new Error(this.stringService.errorForValidEmailAddress); } const subject = this.stringService.testEmailSubject; From 70441874afeda1162bd64422ba3da88d2f1abef1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:54:02 -0700 Subject: [PATCH 16/18] add strings --- client/src/locales/gb.json | 889 +++++++++++++++++++------------------ 1 file changed, 448 insertions(+), 441 deletions(-) diff --git a/client/src/locales/gb.json b/client/src/locales/gb.json index 49993eb89..428cfbd3c 100644 --- a/client/src/locales/gb.json +++ b/client/src/locales/gb.json @@ -1,444 +1,451 @@ { - "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", - "submit": "Submit", - "title": "Title", - "continue": "Continue", - "enterEmail": "Enter your email", - "authLoginTitle": "Log In", - "authLoginEnterPassword": "Enter your password", - "commonPassword": "Password", - "commonBack": "Back", - "authForgotPasswordTitle": "Forgot password?", - "authForgotPasswordResetPassword": "Reset password", - "createPassword": "Create your 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", - "authRegisterStepOneDescription": "Enter your details to get started", - "authRegisterStepTwoTitle": "Set up your profile", - "authRegisterStepTwoDescription": "Tell us more about yourself", - "authRegisterStepThreeTitle": "Almost done!", - "authRegisterStepThreeDescription": "Review your information", - "authForgotPasswordDescription": "No worries, we'll send you reset instructions.", - "authForgotPasswordSendInstructions": "Send instructions", - "authForgotPasswordBackTo": "Back to", - "authCheckEmailTitle": "Check your email", - "authCheckEmailDescription": "We sent a password reset link to", - "authCheckEmailResendEmail": "Resend email", - "authCheckEmailBackTo": "Back to", - "goBackTo": "Go back to", - "authCheckEmailDidntReceiveEmail": "Didn't receive the email?", - "authCheckEmailClickToResend": "Click to resend", - "authSetNewPasswordTitle": "Set new password", - "authSetNewPasswordDescription": "Your new password must be different from previously used passwords.", - "authSetNewPasswordNewPassword": "New password", - "authSetNewPasswordConfirmPassword": "Confirm password", - "confirmPassword": "Re-enter password to confirm", - "authSetNewPasswordResetPassword": "Reset password", - "authSetNewPasswordBackTo": "Back to", - "authPasswordMustBeAtLeast": "Must be at least", - "authPasswordCharactersLong": "8 characters long", - "authPasswordMustContainAtLeast": "Must contain at least", - "authPasswordSpecialCharacter": "one special character", - "authPasswordOneNumber": "one number", - "authPasswordUpperCharacter": "one upper character", - "authPasswordLowerCharacter": "one lower character", - "authPasswordConfirmAndPassword": "Confirm password and password", - "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", - "authRegisterBySigningUp": "By creating an account, you agree to our Terms of Service and Privacy Policy.", - "distributedStatusHeaderText": "Real-time, real-device coverage", - "distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city", - "settingsGeneralSettings": "General settings", - "settingsDisplayTimezone": "Display timezone", - "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", - "settingsLanguage": "Language", - "settingsDistributedUptime": "Distributed uptime", - "settingsDistributedUptimeDescription": "Enable/disable distributed uptime monitoring.", - "settingsEnabled": "Enabled", - "settingsDisabled": "Disabled", - "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 removed, the monitoring history and stats cannot be retrieved.", - "settingsClearAllStatsDialogConfirm": "Yes, clear all stats", - "settingsDemoMonitors": "Demo monitors", - "settingsDemoMonitorsDescription": "Add sample monitors for demonstration purposes.", - "settingsAddDemoMonitors": "Adding demo monitors", - "settingsAddDemoMonitorsButton": "Add demo 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, 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", - "settingsDevelopedBy": "Developed by Bluewave Labs.", - "settingsSave": "Save", - "settingsSuccessSaved": "Settings saved successfully", - "settingsFailedToSave": "Failed to save settings", - "settingsStatsCleared": "Stats cleared successfully", - "settingsFailedToClearStats": "Failed to clear stats", - "settingsDemoMonitorsAdded": "Successfully added demo monitors", - "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", + "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", + "submit": "Submit", + "title": "Title", + "continue": "Continue", + "enterEmail": "Enter your email", + "authLoginTitle": "Log In", + "authLoginEnterPassword": "Enter your password", + "commonPassword": "Password", + "commonBack": "Back", + "authForgotPasswordTitle": "Forgot password?", + "authForgotPasswordResetPassword": "Reset password", + "createPassword": "Create your 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", + "authRegisterStepOneDescription": "Enter your details to get started", + "authRegisterStepTwoTitle": "Set up your profile", + "authRegisterStepTwoDescription": "Tell us more about yourself", + "authRegisterStepThreeTitle": "Almost done!", + "authRegisterStepThreeDescription": "Review your information", + "authForgotPasswordDescription": "No worries, we'll send you reset instructions.", + "authForgotPasswordSendInstructions": "Send instructions", + "authForgotPasswordBackTo": "Back to", + "authCheckEmailTitle": "Check your email", + "authCheckEmailDescription": "We sent a password reset link to", + "authCheckEmailResendEmail": "Resend email", + "authCheckEmailBackTo": "Back to", + "goBackTo": "Go back to", + "authCheckEmailDidntReceiveEmail": "Didn't receive the email?", + "authCheckEmailClickToResend": "Click to resend", + "authSetNewPasswordTitle": "Set new password", + "authSetNewPasswordDescription": "Your new password must be different from previously used passwords.", + "authSetNewPasswordNewPassword": "New password", + "authSetNewPasswordConfirmPassword": "Confirm password", + "confirmPassword": "Re-enter password to confirm", + "authSetNewPasswordResetPassword": "Reset password", + "authSetNewPasswordBackTo": "Back to", + "authPasswordMustBeAtLeast": "Must be at least", + "authPasswordCharactersLong": "8 characters long", + "authPasswordMustContainAtLeast": "Must contain at least", + "authPasswordSpecialCharacter": "one special character", + "authPasswordOneNumber": "one number", + "authPasswordUpperCharacter": "one upper character", + "authPasswordLowerCharacter": "one lower character", + "authPasswordConfirmAndPassword": "Confirm password and password", + "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", + "authRegisterBySigningUp": "By creating an account, you agree to our Terms of Service and Privacy Policy.", + "distributedStatusHeaderText": "Real-time, real-device coverage", + "distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city", + "settingsGeneralSettings": "General settings", + "settingsDisplayTimezone": "Display timezone", + "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", + "settingsLanguage": "Language", + "settingsDistributedUptime": "Distributed uptime", + "settingsDistributedUptimeDescription": "Enable/disable distributed uptime monitoring.", + "settingsEnabled": "Enabled", + "settingsDisabled": "Disabled", + "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 removed, the monitoring history and stats cannot be retrieved.", + "settingsClearAllStatsDialogConfirm": "Yes, clear all stats", + "settingsDemoMonitors": "Demo monitors", + "settingsDemoMonitorsDescription": "Add sample monitors for demonstration purposes.", + "settingsAddDemoMonitors": "Adding demo monitors", + "settingsAddDemoMonitorsButton": "Add demo 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, 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", + "settingsDevelopedBy": "Developed by Bluewave Labs.", + "settingsSave": "Save", + "settingsSuccessSaved": "Settings saved successfully", + "settingsFailedToSave": "Failed to save settings", + "settingsStatsCleared": "Stats cleared successfully", + "settingsFailedToClearStats": "Failed to clear stats", + "settingsDemoMonitorsAdded": "Successfully added demo monitors", + "settingsFailedToAddDemoMonitors": "Failed to add demo monitors", + "settingsMonitorsDeleted": "Successfully deleted all monitors", + "settingsFailedToDeleteMonitors": "Failed to delete all monitors", + "settingsEmail": "Email settings", + "settingsEmailDescription": "Configure email settings", + "settingsEmailHost": "Email host", + "settingsEmailPort": "Email port", + "settingsEmailAddress": "Email address", + "settingsEmailPassword": "Email password", + "settingsEmailUser": "Email user", + "settingsEmailFieldResetLabel": "Password is set. Click Reset to change it.", + "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", - "now": "Now", - "delete": "Delete", - "configure": "Configure", - "networkError": "Network error", - "responseTime": "Response time", - "ms": "ms", - "bar": "Bar", - "area": "Area", - "country": "COUNTRY", - "city": "CITY", - "response": "RESPONSE", - "checkConnection": "Please check your connection", - "passwordreset": "Password Reset", - "authRegisterStepOnePersonalDetails": "Enter your personal details", - "authCheckEmailOpenEmailButton": "Open email app", - "authNewPasswordConfirmed": "Your password has been successfully reset. Click below to log in magically.", - "monitorStatusUp": "Monitor {name} ({url}) is now UP and responding", - "monitorStatusDown": "Monitor {name} ({url}) is DOWN and not responding", - "webhookSendSuccess": "Webhook notification sent successfully", - "webhookSendError": "Error sending webhook notification to {platform}", - "webhookUnsupportedPlatform": "Unsupported platform: {platform}", - "distributedRightCategoryTitle": "Monitor", - "distributedStatusServerMonitors": "Server Monitors", - "distributedStatusServerMonitorsDescription": "Monitor status of related servers", - "distributedUptimeCreateSelectURL": "Here you can select the URL of the host, together with the type of monitor.", - "distributedUptimeCreateChecks": "Checks to perform", - "distributedUptimeCreateChecksDescription": "You can always add or remove checks after adding your site.", - "distributedUptimeCreateIncidentNotification": "Incident notifications", - "distributedUptimeCreateIncidentDescription": "When there is an incident, notify users.", - "distributedUptimeCreateAdvancedSettings": "Advanced settings", - "distributedUptimeDetailsNoMonitorHistory": "There is no check history for this monitor yet.", - "distributedUptimeDetailsFooterHeading": "Made with ❤️ by UpRock & Bluewave Labs", - "distributedUptimeDetailsFooterBuilt": "Built on", - "distributedUptimeDetailsFooterSolana": "Solana", - "distributedUptimeDetailsMonitorHeader": "Distributed Uptime Monitoring powered by DePIN", - "distributedUptimeDetailsStatusHeaderUptime": "Uptime:", - "distributedUptimeDetailsStatusHeaderLastUpdate": "Last updated", - "notifications": { - "enableNotifications": "Enable {{platform}} notifications", - "testNotification": "Test notification", - "addOrEditNotifications": "Add or edit notifications", - "slack": { - "label": "Slack", - "description": "To enable Slack notifications, create a Slack app and enable incoming webhooks. After that, simply provide the webhook URL here.", - "webhookLabel": "Webhook URL", - "webhookPlaceholder": "https://hooks.slack.com/services/...", - "webhookRequired": "Slack webhook URL is required" - }, - "discord": { - "label": "Discord", - "description": "To send data to a Discord channel from Checkmate via Discord notifications using webhooks, you can use Discord's incoming Webhooks feature.", - "webhookLabel": "Discord Webhook URL", - "webhookPlaceholder": "https://discord.com/api/webhooks/...", - "webhookRequired": "Discord webhook URL is required" - }, - "telegram": { - "label": "Telegram", - "description": "To enable Telegram notifications, create a Telegram bot using BotFather, an official bot for creating and managing Telegram bots. Then, get the API token and chat ID and write them down here.", - "tokenLabel": "Your bot token", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "Your Chat ID", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "Telegram token and chat ID are required" - }, - "webhook": { - "label": "Webhooks", - "description": "You can set up a custom webhook to receive notifications when incidents occur.", - "urlLabel": "Webhook URL", - "urlPlaceholder": "https://your-server.com/webhook", - "urlRequired": "Webhook URL is required" - }, - "testNotificationDevelop": "Test notification 2", - "integrationButton": "Notification Integration", - "testSuccess": "Test notification sent successfully!", - "testFailed": "Failed to send test notification", - "unsupportedType": "Unsupported notification type", - "networkError": "Network error occurred" - }, - "testLocale": "testLocale", - "add": "Add", - "monitors": "monitors", - "distributedUptimeStatusCreateStatusPage": "status page", - "distributedUptimeStatusCreateStatusPageAccess": "Access", - "distributedUptimeStatusCreateStatusPageReady": "If your status page is ready, you can mark it as published.", - "distributedUptimeStatusBasicInfoHeader": "Basic Information", - "distributedUptimeStatusBasicInfoDescription": "Define company name and the subdomain that your status page points to.", - "distributedUptimeStatusLogoHeader": "Logo", - "distributedUptimeStatusLogoDescription": "Upload a logo for your status page", - "distributedUptimeStatusLogoUploadButton": "Upload logo", - "distributedUptimeStatusStandardMonitorsHeader": "Standard Monitors", - "distributedUptimeStatusStandardMonitorsDescription": "Attach standard monitors to your status page.", - "distributedUptimeStatusCreateYour": "Create your", - "distributedUptimeStatusEditYour": "Edit your", - "distributedUptimeStatusPublishedLabel": "Published and visible to the public", - "distributedUptimeStatusCompanyNameLabel": "Company name", - "distributedUptimeStatusPageAddressLabel": "Your status page address", - "distributedUptimeStatus30Days": "30 days", - "distributedUptimeStatus60Days": "60 days", - "distributedUptimeStatus90Days": "90 days", - "distributedUptimeStatusPageNotSetUp": "A status page is not set up.", - "distributedUptimeStatusContactAdmin": "Please contact your administrator", - "distributedUptimeStatusPageNotPublic": "This status page is not public.", - "distributedUptimeStatusPageDeleteDialog": "Do you want to delete this status page?", - "distributedUptimeStatusPageDeleteConfirm": "Yes, delete status page", - "distributedUptimeStatusPageDeleteDescription": "Once deleted, your status page cannot be retrieved.", - "distributedUptimeStatusDevices": "Devices", - "distributedUptimeStatusUpt": "UPT", - "distributedUptimeStatusUptBurned": "UPT Burned", - "distributedUptimeStatusUptLogo": "Upt Logo", - "incidentsTableNoIncidents": "No incidents recorded", - "incidentsTablePaginationLabel": "incidents", - "incidentsTableMonitorName": "Monitor Name", - "incidentsTableStatus": "Status", - "incidentsTableDateTime": "Date & Time", - "incidentsTableStatusCode": "Status Code", - "incidentsTableMessage": "Message", - "incidentsOptionsHeader": "Incidents for:", - "incidentsOptionsHeaderFilterBy": "Filter by:", - "incidentsOptionsHeaderFilterAll": "All", - "incidentsOptionsHeaderFilterDown": "Down", - "incidentsOptionsHeaderFilterCannotResolve": "Cannot resolve", - "incidentsOptionsHeaderShow": "Show:", - "incidentsOptionsHeaderLastHour": "Last hour", - "incidentsOptionsHeaderLastDay": "Last day", - "incidentsOptionsHeaderLastWeek": "Last week", - "incidentsOptionsPlaceholderAllServers": "All servers", - "infrastructureCreateYour": "Create your", - "infrastructureCreateGeneralSettingsDescription": "Here you can select the URL of the host, together with the friendly name and authorization secret to connect to the server agent.", - "infrastructureServerRequirement": "The server you are monitoring must be running the", - "infrastructureCustomizeAlerts": "Customize alerts", - "infrastructureAlertNotificationDescription": "Send a notification to user(s) when thresholds exceed a specified percentage.", - "infrastructureCreateMonitor": "Create Infrastructure Monitor", - "infrastructureProtocol": "Protocol", - "infrastructureServerUrlLabel": "Server URL", - "infrastructureDisplayNameLabel": "Display name", - "infrastructureAuthorizationSecretLabel": "Authorization secret", - "gb": "GB", - "mb": "MB", - "mem": "Mem", - "memoryUsage": "Memory usage", - "cpu": "CPU", - "cpuUsage": "CPU usage", - "cpuTemperature": "CPU Temperature", - "diskUsage": "Disk Usage", - "used": "Used", - "total": "Total", - "cores": "Cores", - "frequency": "Frequency", - "status": "Status", - "cpuPhysical": "CPU (Physical)", - "cpuLogical": "CPU (Logical)", - "cpuFrequency": "CPU Frequency", - "avgCpuTemperature": "Average CPU Temperature", - "memory": "Memory", - "disk": "Disk", - "uptime": "Uptime", - "os": "OS", - "host": "Host", - "actions": "Actions", - "integrations": "Integrations", - "integrationsPrism": "Connect Prism to your favorite service.", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "Connect with Slack and see incidents in a channel", - "integrationsDiscord": "Discord", - "integrationsDiscordInfo": "Connect with Discord and view incidents directly in a channel", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "Send all incidents to Zapier, and then see them everywhere", - "commonSave": "Save", - "createYour": "Create your", - "createMonitor": "Create monitor", - "pause": "Pause", - "resume": "Resume", - "editing": "Editing...", - "url": "URL", - "access": "Access", - "timezone": "Timezone", - "features": "Features", - "administrator": "Administrator?", - "loginHere": "Login here", - "displayName": "Display name", - "urlMonitor": "URL to monitor", - "portToMonitor": "Port to monitor", - "websiteMonitoring": "Website monitoring", - "websiteMonitoringDescription": "Use HTTP(s) to monitor your website or API endpoint.", - "pingMonitoring": "Ping monitoring", - "pingMonitoringDescription": "Check whether your server is available or not.", - "dockerContainerMonitoring": "Docker container monitoring", - "dockerContainerMonitoringDescription": "Check whether your Docker container is running or not.", - "portMonitoring": "Port monitoring", - "portMonitoringDescription": "Check whether your port is open or not.", - "createMaintenanceWindow": "Create maintenance window", - "createMaintenance": "Create maintenance", - "editMaintenance": "Edit maintenance", - "maintenanceWindowName": "Maintenance Window Name", - "friendlyNameInput": "Friendly name", - "friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes", - "maintenanceRepeat": "Maintenance Repeat", - "maintenance": "maintenance", - "duration": "Duration", - "addMonitors": "Add monitors", - "window": "window", - "cancel": "Cancel", - "message": "Message", - "low": "low", - "high": "high", - "statusCode": "Status code", - "date&Time": "Date & Time", - "type": "Type", - "statusPageName": "Status page name", - "publicURL": "Public URL", - "repeat": "Repeat", - "edit": "Edit", - "createA": "Create a", - "remove": "Remove", - "maintenanceWindowDescription": "Your pings won't be sent during this time frame", - "startTime": "Start time", - "timeZoneInfo": "All dates and times are in GMT+0 time zone.", - "monitorsToApply": "Monitors to apply maintenance window to", - "nextWindow": "Next window", - "notFoundButton": "Go to the main dashboard", - "pageSpeedConfigureSettingsDescription": "Here you can select the URL of the host, together with the type of monitor.", - "monitorDisplayName": "Monitor display name", - "whenNewIncident": "When there is a new incident,", - "notifySMS": "Notify via SMS (coming soon)", - "notifyEmails": "Also notify via email to multiple addresses (coming soon)", - "seperateEmails": "You can separate multiple emails with a comma", - "checkFrequency": "Check frequency", - "matchMethod": "Match Method", - "expectedValue": "Expected value", - "deleteDialogTitle": "Do you really want to delete this monitor?", - "deleteDialogDescription": "Once deleted, this monitor cannot be retrieved.", - "pageSpeedMonitor": "PageSpeed monitor", - "shown": "Shown", - "ago": "ago", - "companyName": "Company name", - "pageSpeedDetailsPerformanceReport": "Values are estimated and may vary.", - "pageSpeedDetailsPerformanceReportCalculator": "See calculator", - "checkingEvery": "Checking every", - "statusPageCreateSettings": "If your status page is ready, you can mark it as published.", - "basicInformation": "Basic Information", - "statusPageCreateBasicInfoDescription": "Define company name and the subdomain that your status page points to.", - "statusPageCreateSelectTimeZoneDescription": "Select the timezone that your status page will be displayed in.", - "statusPageCreateAppearanceDescription": "Define the default look and feel of your public status page.", - "statusPageCreateSettingsCheckboxLabel": "Published and visible to the public", - "statusPageCreateBasicInfoStatusPageAddress": "Your status page address", - "statusPageCreateTabsContent": "Status page servers", - "statusPageCreateTabsContentDescription": "You can add any number of servers that you monitor to your status page. You can also reorder them for the best viewing experience.", - "statusPageCreateTabsContentFeaturesDescription": "Show more details on the status page", - "showCharts": "Show charts", - "showUptimePercentage": "Show uptime percentage", - "removeLogo": "Remove Logo", - "statusPageStatus": "A public status page is not set up.", - "statusPageStatusContactAdmin": "Please contact to your administrator", - "statusPageStatusNotPublic": "This status page is not public.", - "statusPageStatusNoPage": "There's no status page here.", - "statusPageStatusServiceStatus": "Service status", - "deleteStatusPage": "Do you want to delete this status page?", - "deleteStatusPageConfirm": "Yes, delete status page", - "deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.", - "uptimeCreate": "The expected value is used to match against response result, and the match determines the status.", - "uptimeCreateJsonPath": "This expression will be evaluated against the reponse JSON data and the result will be used to match against the expected value. See", - "uptimeCreateJsonPathQuery": "for query language documentation.", - "maintenanceTableActionMenuDialogTitle": "Do you really want to remove this maintenance window?", - "infrastructureEditYour": "Edit your", - "infrastructureEditMonitor": "Save Infrastructure Monitor", - "infrastructureMonitorCreated": "Infrastructure monitor created successfully!", - "infrastructureMonitorUpdated": "Infrastructure monitor updated successfully!", - "errorInvalidTypeId": "Invalid notification type provided", - "errorInvalidFieldId": "Invalid field ID provided", - "inviteNoTokenFound": "No invite token found", - "pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key yet. Without it, the PageSpeed monitor won't function.", - "pageSpeedLearnMoreLink": "Click here", - "pageSpeedAddApiKey": "to add your API key.", - "update": "Update", - "invalidFileFormat": "Unsupported file format!", - "invalidFileSize": "File size is too large!", - "ClickUpload": "Click to upload", - "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.", - "ignoreTLSError": "Ignore TLS/SSL error", - "tlsErrorIgnored": "TLS/SSL errors ignored", - "ignoreTLSErrorDescription": "Ignore TLS/SSL errors and continue checking the website's availability", - "YourPhoto": "Profile photo", - "PhotoDescriptionText": "This photo will be displayed in your profile page.", - "save": "Save", - "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": "To continue, please enter your email address", - "authRegisterEmailInvalid": "Please enter a valid email address", - "bulkImport": { - "title": "Bulk Import", - "selectFileTips": "Select CSV file to upload", - "selectFileDescription": "You can download our or sample", - "selectFile": "Select File", - "parsingFailed": "Parsing failed", - "uploadSuccess": "Monitors created successfully!", - "validationFailed": "Validation failed", - "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", - "pageSpeedApiKeyFieldDescription": "Enter your Google PageSpeed API key to enable pagespeed monitoring. Click Reset to update the key.", - "pageSpeedApiKeyFieldResetLabel": "API key is set. Click Reset to change it.", - "reset": "Reset" + "now": "Now", + "delete": "Delete", + "configure": "Configure", + "networkError": "Network error", + "responseTime": "Response time", + "ms": "ms", + "bar": "Bar", + "area": "Area", + "country": "COUNTRY", + "city": "CITY", + "response": "RESPONSE", + "checkConnection": "Please check your connection", + "passwordreset": "Password Reset", + "authRegisterStepOnePersonalDetails": "Enter your personal details", + "authCheckEmailOpenEmailButton": "Open email app", + "authNewPasswordConfirmed": "Your password has been successfully reset. Click below to log in magically.", + "monitorStatusUp": "Monitor {name} ({url}) is now UP and responding", + "monitorStatusDown": "Monitor {name} ({url}) is DOWN and not responding", + "webhookSendSuccess": "Webhook notification sent successfully", + "webhookSendError": "Error sending webhook notification to {platform}", + "webhookUnsupportedPlatform": "Unsupported platform: {platform}", + "distributedRightCategoryTitle": "Monitor", + "distributedStatusServerMonitors": "Server Monitors", + "distributedStatusServerMonitorsDescription": "Monitor status of related servers", + "distributedUptimeCreateSelectURL": "Here you can select the URL of the host, together with the type of monitor.", + "distributedUptimeCreateChecks": "Checks to perform", + "distributedUptimeCreateChecksDescription": "You can always add or remove checks after adding your site.", + "distributedUptimeCreateIncidentNotification": "Incident notifications", + "distributedUptimeCreateIncidentDescription": "When there is an incident, notify users.", + "distributedUptimeCreateAdvancedSettings": "Advanced settings", + "distributedUptimeDetailsNoMonitorHistory": "There is no check history for this monitor yet.", + "distributedUptimeDetailsFooterHeading": "Made with ❤️ by UpRock & Bluewave Labs", + "distributedUptimeDetailsFooterBuilt": "Built on", + "distributedUptimeDetailsFooterSolana": "Solana", + "distributedUptimeDetailsMonitorHeader": "Distributed Uptime Monitoring powered by DePIN", + "distributedUptimeDetailsStatusHeaderUptime": "Uptime:", + "distributedUptimeDetailsStatusHeaderLastUpdate": "Last updated", + "notifications": { + "enableNotifications": "Enable {{platform}} notifications", + "testNotification": "Test notification", + "addOrEditNotifications": "Add or edit notifications", + "slack": { + "label": "Slack", + "description": "To enable Slack notifications, create a Slack app and enable incoming webhooks. After that, simply provide the webhook URL here.", + "webhookLabel": "Webhook URL", + "webhookPlaceholder": "https://hooks.slack.com/services/...", + "webhookRequired": "Slack webhook URL is required" + }, + "discord": { + "label": "Discord", + "description": "To send data to a Discord channel from Checkmate via Discord notifications using webhooks, you can use Discord's incoming Webhooks feature.", + "webhookLabel": "Discord Webhook URL", + "webhookPlaceholder": "https://discord.com/api/webhooks/...", + "webhookRequired": "Discord webhook URL is required" + }, + "telegram": { + "label": "Telegram", + "description": "To enable Telegram notifications, create a Telegram bot using BotFather, an official bot for creating and managing Telegram bots. Then, get the API token and chat ID and write them down here.", + "tokenLabel": "Your bot token", + "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", + "chatIdLabel": "Your Chat ID", + "chatIdPlaceholder": "-1001234567890", + "fieldsRequired": "Telegram token and chat ID are required" + }, + "webhook": { + "label": "Webhooks", + "description": "You can set up a custom webhook to receive notifications when incidents occur.", + "urlLabel": "Webhook URL", + "urlPlaceholder": "https://your-server.com/webhook", + "urlRequired": "Webhook URL is required" + }, + "testNotificationDevelop": "Test notification 2", + "integrationButton": "Notification Integration", + "testSuccess": "Test notification sent successfully!", + "testFailed": "Failed to send test notification", + "unsupportedType": "Unsupported notification type", + "networkError": "Network error occurred" + }, + "testLocale": "testLocale", + "add": "Add", + "monitors": "monitors", + "distributedUptimeStatusCreateStatusPage": "status page", + "distributedUptimeStatusCreateStatusPageAccess": "Access", + "distributedUptimeStatusCreateStatusPageReady": "If your status page is ready, you can mark it as published.", + "distributedUptimeStatusBasicInfoHeader": "Basic Information", + "distributedUptimeStatusBasicInfoDescription": "Define company name and the subdomain that your status page points to.", + "distributedUptimeStatusLogoHeader": "Logo", + "distributedUptimeStatusLogoDescription": "Upload a logo for your status page", + "distributedUptimeStatusLogoUploadButton": "Upload logo", + "distributedUptimeStatusStandardMonitorsHeader": "Standard Monitors", + "distributedUptimeStatusStandardMonitorsDescription": "Attach standard monitors to your status page.", + "distributedUptimeStatusCreateYour": "Create your", + "distributedUptimeStatusEditYour": "Edit your", + "distributedUptimeStatusPublishedLabel": "Published and visible to the public", + "distributedUptimeStatusCompanyNameLabel": "Company name", + "distributedUptimeStatusPageAddressLabel": "Your status page address", + "distributedUptimeStatus30Days": "30 days", + "distributedUptimeStatus60Days": "60 days", + "distributedUptimeStatus90Days": "90 days", + "distributedUptimeStatusPageNotSetUp": "A status page is not set up.", + "distributedUptimeStatusContactAdmin": "Please contact your administrator", + "distributedUptimeStatusPageNotPublic": "This status page is not public.", + "distributedUptimeStatusPageDeleteDialog": "Do you want to delete this status page?", + "distributedUptimeStatusPageDeleteConfirm": "Yes, delete status page", + "distributedUptimeStatusPageDeleteDescription": "Once deleted, your status page cannot be retrieved.", + "distributedUptimeStatusDevices": "Devices", + "distributedUptimeStatusUpt": "UPT", + "distributedUptimeStatusUptBurned": "UPT Burned", + "distributedUptimeStatusUptLogo": "Upt Logo", + "incidentsTableNoIncidents": "No incidents recorded", + "incidentsTablePaginationLabel": "incidents", + "incidentsTableMonitorName": "Monitor Name", + "incidentsTableStatus": "Status", + "incidentsTableDateTime": "Date & Time", + "incidentsTableStatusCode": "Status Code", + "incidentsTableMessage": "Message", + "incidentsOptionsHeader": "Incidents for:", + "incidentsOptionsHeaderFilterBy": "Filter by:", + "incidentsOptionsHeaderFilterAll": "All", + "incidentsOptionsHeaderFilterDown": "Down", + "incidentsOptionsHeaderFilterCannotResolve": "Cannot resolve", + "incidentsOptionsHeaderShow": "Show:", + "incidentsOptionsHeaderLastHour": "Last hour", + "incidentsOptionsHeaderLastDay": "Last day", + "incidentsOptionsHeaderLastWeek": "Last week", + "incidentsOptionsPlaceholderAllServers": "All servers", + "infrastructureCreateYour": "Create your", + "infrastructureCreateGeneralSettingsDescription": "Here you can select the URL of the host, together with the friendly name and authorization secret to connect to the server agent.", + "infrastructureServerRequirement": "The server you are monitoring must be running the", + "infrastructureCustomizeAlerts": "Customize alerts", + "infrastructureAlertNotificationDescription": "Send a notification to user(s) when thresholds exceed a specified percentage.", + "infrastructureCreateMonitor": "Create Infrastructure Monitor", + "infrastructureProtocol": "Protocol", + "infrastructureServerUrlLabel": "Server URL", + "infrastructureDisplayNameLabel": "Display name", + "infrastructureAuthorizationSecretLabel": "Authorization secret", + "gb": "GB", + "mb": "MB", + "mem": "Mem", + "memoryUsage": "Memory usage", + "cpu": "CPU", + "cpuUsage": "CPU usage", + "cpuTemperature": "CPU Temperature", + "diskUsage": "Disk Usage", + "used": "Used", + "total": "Total", + "cores": "Cores", + "frequency": "Frequency", + "status": "Status", + "cpuPhysical": "CPU (Physical)", + "cpuLogical": "CPU (Logical)", + "cpuFrequency": "CPU Frequency", + "avgCpuTemperature": "Average CPU Temperature", + "memory": "Memory", + "disk": "Disk", + "uptime": "Uptime", + "os": "OS", + "host": "Host", + "actions": "Actions", + "integrations": "Integrations", + "integrationsPrism": "Connect Prism to your favorite service.", + "integrationsSlack": "Slack", + "integrationsSlackInfo": "Connect with Slack and see incidents in a channel", + "integrationsDiscord": "Discord", + "integrationsDiscordInfo": "Connect with Discord and view incidents directly in a channel", + "integrationsZapier": "Zapier", + "integrationsZapierInfo": "Send all incidents to Zapier, and then see them everywhere", + "commonSave": "Save", + "createYour": "Create your", + "createMonitor": "Create monitor", + "pause": "Pause", + "resume": "Resume", + "editing": "Editing...", + "url": "URL", + "access": "Access", + "timezone": "Timezone", + "features": "Features", + "administrator": "Administrator?", + "loginHere": "Login here", + "displayName": "Display name", + "urlMonitor": "URL to monitor", + "portToMonitor": "Port to monitor", + "websiteMonitoring": "Website monitoring", + "websiteMonitoringDescription": "Use HTTP(s) to monitor your website or API endpoint.", + "pingMonitoring": "Ping monitoring", + "pingMonitoringDescription": "Check whether your server is available or not.", + "dockerContainerMonitoring": "Docker container monitoring", + "dockerContainerMonitoringDescription": "Check whether your Docker container is running or not.", + "portMonitoring": "Port monitoring", + "portMonitoringDescription": "Check whether your port is open or not.", + "createMaintenanceWindow": "Create maintenance window", + "createMaintenance": "Create maintenance", + "editMaintenance": "Edit maintenance", + "maintenanceWindowName": "Maintenance Window Name", + "friendlyNameInput": "Friendly name", + "friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes", + "maintenanceRepeat": "Maintenance Repeat", + "maintenance": "maintenance", + "duration": "Duration", + "addMonitors": "Add monitors", + "window": "window", + "cancel": "Cancel", + "message": "Message", + "low": "low", + "high": "high", + "statusCode": "Status code", + "date&Time": "Date & Time", + "type": "Type", + "statusPageName": "Status page name", + "publicURL": "Public URL", + "repeat": "Repeat", + "edit": "Edit", + "createA": "Create a", + "remove": "Remove", + "maintenanceWindowDescription": "Your pings won't be sent during this time frame", + "startTime": "Start time", + "timeZoneInfo": "All dates and times are in GMT+0 time zone.", + "monitorsToApply": "Monitors to apply maintenance window to", + "nextWindow": "Next window", + "notFoundButton": "Go to the main dashboard", + "pageSpeedConfigureSettingsDescription": "Here you can select the URL of the host, together with the type of monitor.", + "monitorDisplayName": "Monitor display name", + "whenNewIncident": "When there is a new incident,", + "notifySMS": "Notify via SMS (coming soon)", + "notifyEmails": "Also notify via email to multiple addresses (coming soon)", + "seperateEmails": "You can separate multiple emails with a comma", + "checkFrequency": "Check frequency", + "matchMethod": "Match Method", + "expectedValue": "Expected value", + "deleteDialogTitle": "Do you really want to delete this monitor?", + "deleteDialogDescription": "Once deleted, this monitor cannot be retrieved.", + "pageSpeedMonitor": "PageSpeed monitor", + "shown": "Shown", + "ago": "ago", + "companyName": "Company name", + "pageSpeedDetailsPerformanceReport": "Values are estimated and may vary.", + "pageSpeedDetailsPerformanceReportCalculator": "See calculator", + "checkingEvery": "Checking every", + "statusPageCreateSettings": "If your status page is ready, you can mark it as published.", + "basicInformation": "Basic Information", + "statusPageCreateBasicInfoDescription": "Define company name and the subdomain that your status page points to.", + "statusPageCreateSelectTimeZoneDescription": "Select the timezone that your status page will be displayed in.", + "statusPageCreateAppearanceDescription": "Define the default look and feel of your public status page.", + "statusPageCreateSettingsCheckboxLabel": "Published and visible to the public", + "statusPageCreateBasicInfoStatusPageAddress": "Your status page address", + "statusPageCreateTabsContent": "Status page servers", + "statusPageCreateTabsContentDescription": "You can add any number of servers that you monitor to your status page. You can also reorder them for the best viewing experience.", + "statusPageCreateTabsContentFeaturesDescription": "Show more details on the status page", + "showCharts": "Show charts", + "showUptimePercentage": "Show uptime percentage", + "removeLogo": "Remove Logo", + "statusPageStatus": "A public status page is not set up.", + "statusPageStatusContactAdmin": "Please contact to your administrator", + "statusPageStatusNotPublic": "This status page is not public.", + "statusPageStatusNoPage": "There's no status page here.", + "statusPageStatusServiceStatus": "Service status", + "deleteStatusPage": "Do you want to delete this status page?", + "deleteStatusPageConfirm": "Yes, delete status page", + "deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.", + "uptimeCreate": "The expected value is used to match against response result, and the match determines the status.", + "uptimeCreateJsonPath": "This expression will be evaluated against the reponse JSON data and the result will be used to match against the expected value. See", + "uptimeCreateJsonPathQuery": "for query language documentation.", + "maintenanceTableActionMenuDialogTitle": "Do you really want to remove this maintenance window?", + "infrastructureEditYour": "Edit your", + "infrastructureEditMonitor": "Save Infrastructure Monitor", + "infrastructureMonitorCreated": "Infrastructure monitor created successfully!", + "infrastructureMonitorUpdated": "Infrastructure monitor updated successfully!", + "errorInvalidTypeId": "Invalid notification type provided", + "errorInvalidFieldId": "Invalid field ID provided", + "inviteNoTokenFound": "No invite token found", + "pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key yet. Without it, the PageSpeed monitor won't function.", + "pageSpeedLearnMoreLink": "Click here", + "pageSpeedAddApiKey": "to add your API key.", + "update": "Update", + "invalidFileFormat": "Unsupported file format!", + "invalidFileSize": "File size is too large!", + "ClickUpload": "Click to upload", + "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.", + "ignoreTLSError": "Ignore TLS/SSL error", + "tlsErrorIgnored": "TLS/SSL errors ignored", + "ignoreTLSErrorDescription": "Ignore TLS/SSL errors and continue checking the website's availability", + "YourPhoto": "Profile photo", + "PhotoDescriptionText": "This photo will be displayed in your profile page.", + "save": "Save", + "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": "To continue, please enter your email address", + "authRegisterEmailInvalid": "Please enter a valid email address", + "bulkImport": { + "title": "Bulk Import", + "selectFileTips": "Select CSV file to upload", + "selectFileDescription": "You can download our or sample", + "selectFile": "Select File", + "parsingFailed": "Parsing failed", + "uploadSuccess": "Monitors created successfully!", + "validationFailed": "Validation failed", + "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", + "pageSpeedApiKeyFieldDescription": "Enter your Google PageSpeed API key to enable pagespeed monitoring. Click Reset to update the key.", + "pageSpeedApiKeyFieldResetLabel": "API key is set. Click Reset to change it.", + "reset": "Reset" } - From 2f93b70d7f0e38f31adbfad387c6da40f16cecf9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:54:17 -0700 Subject: [PATCH 17/18] add client validation --- client/src/Validation/validation.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 6b6f85227..81359d903 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -262,6 +262,11 @@ const settingsValidation = joi.object({ }), pagespeedApiKey: joi.string().allow("").optional(), language: joi.string().required(), + systemEmailHost: joi.string().allow(""), + systemEmailPort: joi.number().allow(null, ""), + systemEmailAddress: joi.string().allow(""), + systemEmailPassword: joi.string().allow(""), + systemEmailUser: joi.string().allow(""), }); const dayjsValidator = (value, helpers) => { From fc655e77111a354a1de57076f0ceee1ce329bb76 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:54:28 -0700 Subject: [PATCH 18/18] refactor to use new settings return type --- client/src/Hooks/useFetchSettings.js | 4 +- .../Pages/Settings/SettingsDemoMonitors.jsx | 22 +++------ .../src/Pages/Settings/SettingsPagespeed.jsx | 28 ++++++----- client/src/Pages/Settings/SettingsStats.jsx | 6 +-- client/src/Pages/Settings/index.jsx | 46 +++++++++++-------- 5 files changed, 57 insertions(+), 49 deletions(-) diff --git a/client/src/Hooks/useFetchSettings.js b/client/src/Hooks/useFetchSettings.js index 15859795f..f7651e789 100644 --- a/client/src/Hooks/useFetchSettings.js +++ b/client/src/Hooks/useFetchSettings.js @@ -3,7 +3,7 @@ import { networkService } from "../main"; import { createToast } from "../Utils/toastUtils"; import { useTranslation } from "react-i18next"; -const useFetchSettings = ({ setSettings }) => { +const useFetchSettings = ({ setSettingsData }) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); useEffect(() => { @@ -11,7 +11,7 @@ const useFetchSettings = ({ setSettings }) => { setIsLoading(true); try { const response = await networkService.getAppSettings(); - setSettings(response?.data?.data); + setSettingsData(response?.data?.data); } catch (error) { createToast({ body: "Failed to fetch settings" }); setError(error); diff --git a/client/src/Pages/Settings/SettingsDemoMonitors.jsx b/client/src/Pages/Settings/SettingsDemoMonitors.jsx index 6722e97ac..1a592e776 100644 --- a/client/src/Pages/Settings/SettingsDemoMonitors.jsx +++ b/client/src/Pages/Settings/SettingsDemoMonitors.jsx @@ -8,12 +8,8 @@ import { PropTypes } from "prop-types"; import { useTranslation } from "react-i18next"; import Dialog from "../../Components/Dialog"; import { useState } from "react"; -const SettingsDemoMonitors = ({ - isLoading, - authIsLoading, - checksIsLoading, - handleChange, -}) => { + +const SettingsDemoMonitors = ({ HEADER_SX, handleChange, isLoading }) => { const { t } = useTranslation(); const theme = useTheme(); // Local state @@ -23,16 +19,14 @@ const SettingsDemoMonitors = ({ {t("settingsDemoMonitors")} - - {t("settingsDemoMonitorsDescription")} - + {t("settingsDemoMonitorsDescription")} {t("settingsAddDemoMonitors")}