diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9ff54e807..26956309b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,7 +18,7 @@ Fixes #123 const { t } = useTranslation();
{t('add')}
``` -- [ ] The issue I am working on is assigned to me. +- [ ] I have **not** included any files that are not related to my pull request, including package-lock and package-json if dependencies have not changed - [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application). - [ ] I made sure font sizes, color choices etc are all referenced from the theme. I have no hardcoded dimensions. - [ ] My PR is granular and targeted to one specific feature. 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, diff --git a/client/src/Components/MonitorCreateHeader/index.jsx b/client/src/Components/MonitorCreateHeader/index.jsx index 05a9b7f9b..15d6d5c0f 100644 --- a/client/src/Components/MonitorCreateHeader/index.jsx +++ b/client/src/Components/MonitorCreateHeader/index.jsx @@ -6,7 +6,7 @@ import { useTheme } from "@emotion/react"; const CreateMonitorHeader = ({ isAdmin, - label = "Create new", + label, isLoading = true, path, bulkPath, @@ -14,6 +14,8 @@ const CreateMonitorHeader = ({ const navigate = useNavigate(); const { t } = useTranslation(); const theme = useTheme(); + + // Use the provided label or fall back to the translated default if (!isAdmin) return null; @@ -30,7 +32,7 @@ const CreateMonitorHeader = ({ color="accent" onClick={() => navigate(path)} > - {label} + {label || t("createNew")} {bulkPath && ( @@ -237,22 +238,20 @@ const TeamPanel = () => { color="accent" onClick={() => setIsOpen(true)} > - Invite a team member + {t("teamPanel.inviteTeamMember")} { marginBottom={SPACING_GAP} type="email" id="input-team-member" - placeholder="Email" + placeholder={t("teamPanel.email")} value={toInvite.email} onChange={handleChange} error={errors.email ? true : false} @@ -269,7 +268,7 @@ const TeamPanel = () => { /> + + + ); +}; + +SettingsTimeZone.propTypes = { + HEADING_SX: PropTypes.object, + handleChange: PropTypes.func, + timezone: PropTypes.string, +}; + +export default SettingsTimeZone; diff --git a/client/src/Pages/Settings/SettingsUI.jsx b/client/src/Pages/Settings/SettingsUI.jsx new file mode 100644 index 000000000..24588d79e --- /dev/null +++ b/client/src/Pages/Settings/SettingsUI.jsx @@ -0,0 +1,52 @@ +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"; + +// Utils +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useTranslation } from "react-i18next"; + +const SettingsUI = ({ HEADING_SX, handleChange, mode, language }) => { + const { t, i18n } = useTranslation(); + const theme = useTheme(); + const languages = Object.keys(i18n.options.resources || {}); + return ( + + + {t("settingsAppearance")} + {t("settingsAppearanceDescription")} + + + + + + + ); +}; + +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 7ebf1e646..eb516ba01 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -1,134 +1,69 @@ -// Components -import { Box, Stack, Typography, Button, Switch } 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 SettingsEmail from "./SettingsEmail"; +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 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, - setDistributedUptimeEnabled, - 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 [checksIsLoading, setChecksIsLoading] = useState(false); - const [form, setForm] = useState({ - enableDistributedUptime: distributedUptimeEnabled, - ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0, - pagespeedApiKey: "", - }); - const [version, setVersion] = useState("unknown"); - const [apiKeyFieldType, setApiKeyFieldType] = useState("password"); - const [isApiKeySet, setIsApiKeySet] = useState(false); + + // Local state + const [settingsData, setSettingsData] = useState({}); const [errors, setErrors] = useState({}); - const deleteStatsMonitorsInitState = { deleteMonitors: false, deleteStats: false }; - const [isOpen, setIsOpen] = useState(deleteStatsMonitorsInitState); + // Network + const [isSettingsLoading, settingsError] = useFetchSettings({ + setSettingsData, + }); + + 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); - } + // Handlers + const handleChange = async (e) => { + const { name, value } = e.target; + + // Build next state early + const newSettingsData = { + ...settingsData, + settings: { ...settingsData.settings, [name]: value }, }; - fetchLatestVersion(); - }, []); - - useEffect(() => { - dispatch(getAppSettings()); - }, []); - - const { pagespeedApiKey } = useSelector((state) => state.settings); - - useEffect(() => { - if (pagespeedApiKey) { - setIsApiKeySet(true); - setForm((prev) => ({ - ...prev, - pagespeedApiKey: t("maskedPageSpeedKeyPlaceholder"), - })); - } else { - setIsApiKeySet(false); - setForm((prev) => ({ - ...prev, - pagespeedApiKey: "", - })); - } - }, [pagespeedApiKey]); - - const handleChange = (event) => { - const { type, checked, value, id } = event.target; - - if (type === "checkbox") { - setForm((prev) => ({ - ...prev, - [id]: checked, - })); - 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 } - ); + // Validate + const { error } = settingsValidation.validate(newSettingsData.settings, { + abortEarly: false, + }); if (!error || error.details.length === 0) { setErrors({}); } else { @@ -137,385 +72,130 @@ 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: 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; } + + setSettingsData(newSettingsData); }; - const handleResetApiKey = () => { - setIsApiKeySet(false); - setForm((prev) => ({ - ...prev, - pagespeedApiKey: "", - })); - }; - - const languages = Object.keys(i18n.options.resources || {}); + const handleSave = () => { + const { error } = settingsValidation.validate(settingsData.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(settingsData?.settings); + }; return ( - + + + Settings + + + + + + + - - - {t("settingsGeneralSettings")} - - {t("settingsDisplayTimezone")}-{" "} - {t("settingsDisplayTimezoneDescription")} - - - - { - dispatch(setMode(e.target.value)); - }} - items={[ - { _id: "light", name: t("settingsThemeModeLight") }, - { _id: "dark", name: t("settingsThemeModeDark") }, - ]} - > - - - - {/* {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; diff --git a/client/src/Pages/StatusPage/Create/index.jsx b/client/src/Pages/StatusPage/Create/index.jsx index f97b097df..5534ef68d 100644 --- a/client/src/Pages/StatusPage/Create/index.jsx +++ b/client/src/Pages/StatusPage/Create/index.jsx @@ -54,7 +54,7 @@ const CreateStatusPage = () => { const [createStatusPage, createStatusIsLoading, createStatusPageNetworkError] = useCreateStatusPage(isCreate); const navigate = useNavigate(); - const { t } = useTranslation(); + const { t } = useTranslation(); const [statusPage, statusPageMonitors, statusPageIsLoading, statusPageNetworkError] = useStatusPageFetch(isCreate, url); @@ -89,29 +89,29 @@ const CreateStatusPage = () => { const handleImageChange = useCallback((fileObj) => { if (!fileObj || !fileObj.file) return; - + setForm((prev) => ({ - ...prev, - logo: { - src: fileObj.src, - name: fileObj.name, - type: fileObj.file.type, - size: fileObj.file.size, - }, + ...prev, + logo: { + src: fileObj.src, + name: fileObj.name, + type: fileObj.file.type, + size: fileObj.file.size, + }, })); - + intervalRef.current = setInterval(() => { - const buffer = 12; - setProgress((prev) => { - if (prev.value + buffer >= 100) { - clearInterval(intervalRef.current); - return { value: 100, isLoading: false }; - } - return { ...prev, value: prev.value + buffer }; - }); + const buffer = 12; + setProgress((prev) => { + if (prev.value + buffer >= 100) { + clearInterval(intervalRef.current); + return { value: 100, isLoading: false }; + } + return { ...prev, value: prev.value + buffer }; + }); }, 120); }, []); - + const removeLogo = () => { setForm((prev) => ({ ...prev, diff --git a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx index cf55ce3aa..7d9f5a6bf 100644 --- a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx @@ -27,22 +27,14 @@ import { useTranslation } from "react-i18next"; * @returns {JSX.Element} The rendered Filter component. */ -const typeOptions = [ +const getTypeOptions = () => [ { value: "http", label: "HTTP(S)" }, { value: "ping", label: "Ping" }, { value: "docker", label: "Docker" }, { value: "port", label: "Port" }, ]; -const statusOptions = [ - { value: "Up", label: "Up" }, - { value: "Down", label: "Down" }, -]; - -const stateOptions = [ - { value: "Active", label: "Active" }, - { value: "Paused", label: "Paused" }, -]; +// These functions were moved inline to ensure translations are applied correctly const Filter = ({ selectedTypes, @@ -58,6 +50,18 @@ const Filter = ({ const theme = useTheme(); const { t } = useTranslation(); + const typeOptions = getTypeOptions(); + // Create status options with translations + const statusOptions = [ + { value: "Up", label: t("monitorStatus.up") }, + { value: "Down", label: t("monitorStatus.down") }, + ]; + // Create state options with translations + const stateOptions = [ + { value: "Active", label: t("monitorState.active") }, + { value: "Paused", label: t("monitorState.paused") }, + ]; + const handleTypeChange = (event) => { const selectedValues = event.target.value; setSelectedTypes(selectedValues.length > 0 ? selectedValues : undefined); diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx index aa7b7ee0d..5587dad66 100644 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx @@ -2,10 +2,12 @@ import PropTypes from "prop-types"; import { Stack } from "@mui/material"; import StatusBox from "./statusBox"; import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; import SkeletonLayout from "./skeleton"; const StatusBoxes = ({ shouldRender, monitorsSummary }) => { const theme = useTheme(); + const { t } = useTranslation(); if (!shouldRender) return ; return ( { justifyContent="space-between" > @@ -31,6 +33,7 @@ const StatusBoxes = ({ shouldRender, monitorsSummary }) => { StatusBoxes.propTypes = { monitorsSummary: PropTypes.object, + shouldRender: PropTypes.bool, }; export default StatusBoxes; diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index 60a308141..ce9123816 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -31,11 +31,11 @@ import PropTypes from "prop-types"; import useFetchMonitorsWithSummary from "../../../Hooks/useFetchMonitorsWithSummary"; import useFetchMonitorsWithChecks from "../../../Hooks/useFetchMonitorsWithChecks"; import { useTranslation } from "react-i18next"; -const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }]; const TYPES = ["http", "ping", "docker", "port"]; const CreateMonitorButton = ({ shouldRender }) => { // Utils const navigate = useNavigate(); + const { t } = useTranslation(); if (shouldRender === false) { return; } @@ -49,7 +49,7 @@ const CreateMonitorButton = ({ shouldRender }) => { navigate("/uptime/create"); }} > - Create new + {t("createNew")} ); @@ -78,10 +78,13 @@ const UptimeMonitors = () => { // Utils const theme = useTheme(); + const navigate = useNavigate(); const isAdmin = useIsAdmin(); const dispatch = useDispatch(); const { t } = useTranslation(); + const BREADCRUMBS = [{ name: t("menu.uptime"), path: "/uptime" }]; + // Handlers const handleChangePage = (event, newPage) => { setPage(newPage); diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index 6a6724150..631f056dd 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -9,7 +9,7 @@ const shadow = const baseTheme = (palette) => ({ typography: { fontFamily: fontFamilyPrimary, - fontSize: 14, + fontSize: typographyLevels.base, h1: { fontSize: typographyLevels.xl, color: palette.primary.contrastText, @@ -253,6 +253,7 @@ const baseTheme = (palette) => ({ MuiTableCell: { styleOverrides: { root: ({ theme }) => ({ + fontSize: typographyLevels.base, borderBottomColor: theme.palette.primary.lowContrast, }), }, @@ -376,7 +377,6 @@ const baseTheme = (palette) => ({ MuiTab: { styleOverrides: { root: ({ theme }) => ({ - fontSize: theme.typography.fontSize - 1, color: theme.palette.tertiary.contrastText, height: "34px", minHeight: "34px", diff --git a/client/src/Utils/greeting.jsx b/client/src/Utils/greeting.jsx index 0193ae688..269d4c73b 100644 --- a/client/src/Utils/greeting.jsx +++ b/client/src/Utils/greeting.jsx @@ -3,6 +3,7 @@ import { useTheme } from "@emotion/react"; import { Box, Typography } from "@mui/material"; import { useDispatch, useSelector } from "react-redux"; import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { setGreeting } from "../Features/UI/uiSlice"; const early = [ @@ -133,6 +134,7 @@ const evening = [ const Greeting = ({ type = "" }) => { const theme = useTheme(); const dispatch = useDispatch(); + const { t } = useTranslation(); const { firstName } = useSelector((state) => state.auth.user); const index = useSelector((state) => state.ui.greeting.index); const lastUpdate = useSelector((state) => state.ui.greeting.lastUpdate); @@ -147,7 +149,7 @@ const Greeting = ({ type = "" }) => { let random = Math.floor(Math.random() * 5); dispatch(setGreeting({ index: random, lastUpdate: hour })); } - }, [dispatch, hour]); + }, [dispatch, hour, lastUpdate]); let greetingArray = hour < 6 ? early : hour < 12 ? morning : hour < 18 ? afternoon : evening; @@ -165,7 +167,7 @@ const Greeting = ({ type = "" }) => { fontSize="inherit" color={theme.palette.primary.contrastTextTertiary} > - {prepend},{" "} + {t("greeting.prepend", { defaultValue: prepend })}, {" "} { lineHeight={1} color={theme.palette.primary.contrastTextTertiary} > - {append} — Here’s an overview of your {type} monitors. + {t("greeting.append", { defaultValue: append })} — {t("greeting.overview", { type: t(`menu.${type}`) })} ); diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 0ace7b02c..81359d903 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,19 @@ 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(), + 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) => { if (!dayjs(value).isValid()) { @@ -363,9 +370,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 +387,5 @@ export { advancedSettingsValidation, infrastructureMonitorValidation, statusPageValidation, - logoImageValidation + logoImageValidation, }; diff --git a/client/src/locales/gb.json b/client/src/locales/gb.json index 89bab054e..b2b173f9e 100644 --- a/client/src/locales/gb.json +++ b/client/src/locales/gb.json @@ -1,451 +1,515 @@ { - "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", - "authForgotPasswordInstructions": "No worries, we'll send you reset instructions.", - "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", - "settingsThemeModeLight": "Light", - "settingsThemeModeDark": "Dark", - "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", - "statusBreadCrumbsStatusPages": "Status Pages", - "statusBreadCrumbsDetails": "Details", - "uptimeCreateSelectURL": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", - "navControls": "Controls" + "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", + "createNew": "Create new", + "greeting": { + "prepend": "Hey there", + "append": "The afternoon is your playground—let's make it epic!", + "overview": "Here's an overview of your {{type}} monitors." + }, + "monitorStatus": { + "up": "up", + "down": "down", + "paused": "paused" + }, + "roles": { + "superAdmin": "Super admin", + "admin": "Admin", + "teamMember": "Team member", + "demoUser": "Demo user" + }, + "teamPanel": { + "teamMembers": "Team members", + "filter": { + "all": "All", + "member": "Member" + }, + "inviteTeamMember": "Invite a team member", + "inviteNewTeamMember": "Invite new team member", + "inviteDescription": "When you add a new team member, they will get access to all monitors.", + "email": "Email", + "selectRole": "Select role", + "inviteLink": "Invite link", + "cancel": "Cancel", + "noMembers": "There are no team members with this role", + "getToken": "Get token", + "emailToken": "E-mail token", + "table": { + "name": "Name", + "email": "Email", + "role": "Role", + "created": "Created" + } + }, + "monitorState": { + "paused": "paused", + "resumed": "resumed", + "active": "active" + }, + "menu": { + "uptime": "Uptime", + "pagespeed": "Pagespeed", + "infrastructure": "Infrastructure", + "distributedUptime": "Distributed uptime", + "incidents": "Incidents", + "statusPages": "Status pages", + "maintenance": "Maintenance", + "integrations": "Integrations", + "settings": "Settings", + "support": "Support", + "discussions": "Discussions", + "docs": "Docs", + "changelog": "Changelog", + "profile": "Profile", + "password": "Password", + "team": "Team" + } } - diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json index c0a083b14..66eb4028a 100644 --- a/client/src/locales/ru.json +++ b/client/src/locales/ru.json @@ -16,7 +16,6 @@ "commonBack": "Назад", "authForgotPasswordTitle": "Забыли пароль?", "authForgotPasswordResetPassword": "Сбросить пароль", - "authForgotPasswordInstructions": "Не волнуйтесь, мы отправим вам инструкции по сбросу пароля.", "createPassword": "Создайте свой пароль", "createAPassword": "Пароль", "authRegisterAlreadyHaveAccount": "Уже есть аккаунт?", @@ -66,7 +65,18 @@ "authRegisterLastName": "Фамилия", "authRegisterEmail": "Эл. почта", "authRegisterEmailRequired": "Чтобы продолжить, пожалуйста, введите ваш адрес электронной почты", - "authRegisterEmailInvalid": "Пожалуйста, введите корректный адрес электронной почты", + "authRegisterEmailInvalid": "Пожалуйста, введите действительный адрес электронной почты", + "bulkImport": { + "title": "Массовый импорт", + "selectFileTips": "Выберите CSV-файл для загрузки", + "selectFileDescription": "Вы можете скачать наш или пример", + "selectFile": "Выбрать файл", + "parsingFailed": "Ошибка анализа", + "uploadSuccess": "Мониторы успешно созданы!", + "validationFailed": "Ошибка проверки", + "noFileSelected": "Файл не выбран", + "fallbackPage": "Импортируйте файл для загрузки списка серверов" + }, "distributedStatusHeaderText": "Охват реального времени и реального устройства", "distributedStatusSubHeaderText": "Работает на миллионах устройств по всему миру, просматривайте производительность системы по глобальному региону, стране или городу", "settingsGeneralSettings": "Общие настройки", @@ -75,8 +85,6 @@ "settingsAppearance": "Внешний вид", "settingsAppearanceDescription": "Переключение между светлым и темным режимом или изменение языка пользовательского интерфейса", "settingsThemeMode": "Тема", - "settingsThemeModeLight": "Светлая", - "settingsThemeModeDark": "Темная", "settingsLanguage": "Язык", "settingsDistributedUptime": "Distributed uptime", "settingsDistributedUptimeDescription": "Включить/выключить distributed uptime monitoring.", @@ -397,11 +405,67 @@ "pageSpeedWarning": "Предупреждение: Вы не добавили ключ API Google PageSpeed. Без него монитор PageSpeed не будет работать.", "pageSpeedLearnMoreLink": "Нажмите здесь, чтобы узнать", "pageSpeedAddApiKey": "как добавить ваш ключ API.", - "pageSpeedApiKeyFieldDescription": "Введите ваш API-ключ Google PageSpeed для включения мониторинга скорости страницы. Нажмите Сбросить, чтобы обновить ключ.", - "pageSpeedApiKeyFieldResetLabel": "API-ключ установлен. Нажмите Сбросить, чтобы изменить его.", - "reset": "Сбросить", - "statusBreadCrumbsStatusPages": "Страницы статуса", - "statusBreadCrumbsDetails": "Подробности", - "uptimeCreateSelectURL": "Введите URL или IP-адрес для мониторинга (например, https://example.com/ или 192.168.1.100) и добавьте понятное отображаемое имя, которое будет показано на панели управления.", - "navControls": "Управление" + "createNew": "Создать новый", + "greeting": { + "prepend": "Привет", + "append": "День прекрасен для новых достижений!", + "overview": "Вот обзор ваших мониторов {{type}}." + }, + "monitorStatus": { + "up": "работает", + "down": "не работает", + "paused": "приостановлен" + }, + "roles": { + "superAdmin": "Суперадминистратор", + "admin": "Администратор", + "teamMember": "Член команды", + "demoUser": "Демо-пользователь" + }, + "teamPanel": { + "teamMembers": "Члены команды", + "filter": { + "all": "Все", + "member": "Член" + }, + "inviteTeamMember": "Пригласить члена команды", + "inviteNewTeamMember": "Пригласить нового члена команды", + "inviteDescription": "Когда вы добавляете нового члена команды, он получит доступ ко всем мониторам.", + "email": "Эл. почта", + "selectRole": "Выберите роль", + "inviteLink": "Ссылка для приглашения", + "cancel": "Отмена", + "noMembers": "Нет членов команды с этой ролью", + "getToken": "Получить токен", + "emailToken": "Отправить токен по эл. почте", + "table": { + "name": "Имя", + "email": "Эл. почта", + "role": "Роль", + "created": "Создан" + } + }, + "monitorState": { + "paused": "приостановлен", + "resumed": "возобновлен", + "active": "активный" + }, + "menu": { + "uptime": "Аптайм", + "pagespeed": "Скорость страницы", + "infrastructure": "Инфраструктура", + "distributedUptime": "Распределенный аптайм", + "incidents": "Инциденты", + "statusPages": "Страницы статуса", + "maintenance": "Обслуживание", + "integrations": "Интеграции", + "settings": "Настройки", + "support": "Поддержка", + "discussions": "Обсуждения", + "docs": "Документация", + "changelog": "История изменений", + "profile": "Профиль", + "password": "Пароль", + "team": "Команда" + } } \ No newline at end of file diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json index 3cefcabdd..6a9ead23b 100644 --- a/client/src/locales/tr.json +++ b/client/src/locales/tr.json @@ -14,9 +14,8 @@ "authLoginEnterPassword": "Parolanızı girin", "commonPassword": "Parola", "commonBack": "Geri", - "authForgotPasswordTitle": "Şifrenizi mi unuttunuz?", - "authForgotPasswordResetPassword": "Şifreyi sıfırla", - "authForgotPasswordInstructions": "Endişelenmeyin, size sıfırlama talimatlarını göndereceğiz.", + "authForgotPasswordTitle": "Parolanızı mı unuttunuz?", + "authForgotPasswordResetPassword": "Parola sıfırla", "createPassword": "Parolanızı oluşturun", "createAPassword": "Parola", "authRegisterAlreadyHaveAccount": "Zaten hesabınız var mı?", @@ -70,8 +69,6 @@ "settingsAppearance": "Görünüm", "settingsAppearanceDescription": "Açık ve koyu mod arasında geçiş yapın veya kullanıcı arayüzü dilini değiştirin", "settingsThemeMode": "Tema", - "settingsThemeModeLight": "Açık", - "settingsThemeModeDark": "Koyu", "settingsLanguage": "Dil", "settingsDistributedUptime": "Dağıtılmış çalışma süresi", "settingsDistributedUptimeDescription": "Dağıtılmış çalışma süresi izlemeyi etkinleştirin/devre dışı bırakın.", @@ -421,18 +418,77 @@ "authRegisterEmailRequired": "Devam etmek için lütfen e-posta adresinizi girin", "authRegisterEmailInvalid": "Lütfen geçerli bir e-posta adresi girin", "bulkImport": { - "title": "", - "selectFileTips": "", - "selectFileDescription": "", - "selectFile": "", - "parsingFailed": "", - "uploadSuccess": "", - "validationFailed": "", - "noFileSelected": "", - "fallbackPage": "" + "title": "Toplu İçe Aktar", + "selectFileTips": "Yüklemek için CSV dosyası seçin", + "selectFileDescription": " veya örneğimizi indirebilirsiniz", + "selectFile": "Dosya Seç", + "parsingFailed": "Ayrıştırma başarısız oldu", + "uploadSuccess": "Monitörler başarıyla oluşturuldu!", + "validationFailed": "Doğrulama başarısız oldu", + "noFileSelected": "Dosya seçilmedi", + "fallbackPage": "Sunucuların listesini toplu olarak yüklemek için bir dosya içe aktarın" }, - "statusBreadCrumbsStatusPages": "Durum Sayfaları", - "statusBreadCrumbsDetails": "Detaylar", - "uptimeCreateSelectURL": "İzlenecek URL veya IP adresini girin (örneğin, https://example.com/ veya 192.168.1.100) ve kontrol panelinde görünecek net bir görüntü adı ekleyin.", - "navControls": "Kontroller" + "createNew": "Yeni oluştur", + "greeting": { + "prepend": "Merhaba", + "append": "Öğleden sonra senin oyun alanın—hadi onu muhteşem yapalım!", + "overview": "İşte {{type}} monitörlerinizin genel görünümü." + }, + "monitorStatus": { + "up": "aktif", + "down": "devre dışı", + "paused": "duraklatıldı" + }, + "roles": { + "superAdmin": "Süper yönetici", + "admin": "Yönetici", + "teamMember": "Takım üyesi", + "demoUser": "Demo kullanıcı" + }, + "teamPanel": { + "teamMembers": "Takım üyeleri", + "filter": { + "all": "Tümü", + "member": "Üye" + }, + "inviteTeamMember": "Takım üyesi davet et", + "inviteNewTeamMember": "Yeni takım üyesi davet et", + "inviteDescription": "Yeni bir takım üyesi eklediğinizde, tüm monitörlere erişim hakkı alacaktır.", + "email": "E-posta", + "selectRole": "Rol seçin", + "inviteLink": "Davet bağlantısı", + "cancel": "İptal", + "noMembers": "Bu role sahip takım üyesi bulunmamaktadır", + "getToken": "Token al", + "emailToken": "Token e-posta ile gönder", + "table": { + "name": "Ad", + "email": "E-posta", + "role": "Rol", + "created": "Oluşturuldu" + } + }, + "monitorState": { + "paused": "duraklatıldı", + "resumed": "devam ettirildi", + "active": "aktif" + }, + "menu": { + "uptime": "Çalışma Süresi", + "pagespeed": "Sayfa Hızı", + "infrastructure": "Altyapı", + "distributedUptime": "Dağıtılmış Çalışma Süresi", + "incidents": "Olaylar", + "statusPages": "Durum Sayfaları", + "maintenance": "Bakım", + "integrations": "Entegrasyonlar", + "settings": "Ayarlar", + "support": "Destek", + "discussions": "Tartışmalar", + "docs": "Belgeler", + "changelog": "Değişiklik Günlüğü", + "profile": "Profil", + "password": "Şifre", + "team": "Takım" + } } diff --git a/docker/dev/docker-compose.yaml b/docker/dev/docker-compose.yaml index 73904da37..292d0b1d5 100755 --- a/docker/dev/docker-compose.yaml +++ b/docker/dev/docker-compose.yaml @@ -5,7 +5,7 @@ services: ports: - "80:80" environment: - UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1" + UPTIME_APP_API_BASE_URL: "http://localhost:52345/api/v1" UPTIME_APP_CLIENT_HOST: "http://localhost" volumes: - ./nginx/conf.d:/etc/nginx/conf.d/ @@ -15,7 +15,7 @@ services: image: uptime_server:latest restart: always ports: - - "5000:5000" + - "52345:52345" env_file: - server.env depends_on: @@ -24,8 +24,6 @@ services: redis: image: uptime_redis:latest restart: always - ports: - - "6379:6379" volumes: - ./redis/data:/data healthcheck: @@ -38,8 +36,6 @@ services: image: uptime_mongo:latest restart: always command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"] - ports: - - "27017:27017" volumes: - ./mongo/data:/data/db healthcheck: diff --git a/docker/dev/nginx/conf.d/default.conf b/docker/dev/nginx/conf.d/default.conf index 9a7690aa5..e2c2a7268 100755 --- a/docker/dev/nginx/conf.d/default.conf +++ b/docker/dev/nginx/conf.d/default.conf @@ -12,7 +12,7 @@ server { } # location /api/ { - # proxy_pass http://server:5000/api/; + # proxy_pass http://server:52345/api/; # proxy_http_version 1.1; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; @@ -24,12 +24,12 @@ server { # proxy_cache off; # } - location /api-docs/ { - proxy_pass http://server:5000/api-docs/; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } + # location /api-docs/ { + # proxy_pass http://server:52345/api-docs/; + # proxy_http_version 1.1; + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # } } \ No newline at end of file diff --git a/docker/dev/server.Dockerfile b/docker/dev/server.Dockerfile index 202422dcc..d9d99a09d 100755 --- a/docker/dev/server.Dockerfile +++ b/docker/dev/server.Dockerfile @@ -8,6 +8,6 @@ RUN npm install COPY ./server/ ./ -EXPOSE 5000 +EXPOSE 52345 CMD ["node", "index.js"] \ No newline at end of file diff --git a/docker/dist/docker-compose.yaml b/docker/dist/docker-compose.yaml index ca8e24061..0bc0d89e8 100755 --- a/docker/dist/docker-compose.yaml +++ b/docker/dist/docker-compose.yaml @@ -14,7 +14,7 @@ services: image: ghcr.io/bluewave-labs/checkmate:backend-dist restart: always ports: - - "5000:5000" + - "52345:52345" depends_on: - redis - mongodb @@ -27,8 +27,6 @@ services: redis: image: ghcr.io/bluewave-labs/checkmate:redis-dist restart: always - ports: - - "6379:6379" volumes: - ./redis/data:/data healthcheck: @@ -43,8 +41,6 @@ services: volumes: - ./mongo/data:/data/db command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"] - ports: - - "27017:27017" healthcheck: test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet interval: 5s diff --git a/docker/dist/server.Dockerfile b/docker/dist/server.Dockerfile index 202422dcc..d9d99a09d 100755 --- a/docker/dist/server.Dockerfile +++ b/docker/dist/server.Dockerfile @@ -8,6 +8,6 @@ RUN npm install COPY ./server/ ./ -EXPOSE 5000 +EXPOSE 52345 CMD ["node", "index.js"] \ No newline at end of file diff --git a/docker/prod/docker-compose.yaml b/docker/prod/docker-compose.yaml index bc720862f..9cc4a82cb 100755 --- a/docker/prod/docker-compose.yaml +++ b/docker/prod/docker-compose.yaml @@ -25,7 +25,7 @@ services: image: ghcr.io/bluewave-labs/checkmate:backend-demo restart: always ports: - - "5000:5000" + - "52345:52345" env_file: - server.env depends_on: @@ -36,8 +36,6 @@ services: redis: image: ghcr.io/bluewave-labs/checkmate:redis-demo restart: always - ports: - - "6379:6379" volumes: - ./redis/data:/data healthcheck: @@ -50,8 +48,6 @@ services: image: ghcr.io/bluewave-labs/checkmate:mongo-demo restart: always command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"] - ports: - - "27017:27017" volumes: - ./mongo/data:/data/db # - ./mongo/init/init.js:/docker-entrypoint-initdb.d/init.js // No longer needed diff --git a/docker/prod/nginx/conf.d/default.conf b/docker/prod/nginx/conf.d/default.conf index 98e1bd97d..8afeb93b7 100755 --- a/docker/prod/nginx/conf.d/default.conf +++ b/docker/prod/nginx/conf.d/default.conf @@ -16,7 +16,7 @@ server { } location /api/ { - proxy_pass http://server:5000/api/; + proxy_pass http://server:52345/api/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -25,7 +25,7 @@ server { } location /api-docs/ { - proxy_pass http://server:5000/api-docs/; + proxy_pass http://server:52345/api-docs/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -50,7 +50,7 @@ server { } location /api/ { - proxy_pass http://server:5000/api/; + proxy_pass http://server:52345/api/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -59,7 +59,7 @@ server { } location /api-docs/ { - proxy_pass http://server:5000/api-docs/; + proxy_pass http://server:52345/api-docs/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/docker/prod/server.Dockerfile b/docker/prod/server.Dockerfile index 998bbe4fe..c80f468f8 100755 --- a/docker/prod/server.Dockerfile +++ b/docker/prod/server.Dockerfile @@ -10,6 +10,6 @@ RUN npm install COPY ./server ./ -EXPOSE 5000 +EXPOSE 52345 CMD ["node", "index.js"] \ No newline at end of file diff --git a/docker/staging/docker-compose.yaml b/docker/staging/docker-compose.yaml index d491a729e..b9ca52b6e 100755 --- a/docker/staging/docker-compose.yaml +++ b/docker/staging/docker-compose.yaml @@ -27,7 +27,7 @@ services: image: ghcr.io/bluewave-labs/checkmate:backend-staging restart: always ports: - - "5000:5000" + - "52345:52345" env_file: - server.env depends_on: diff --git a/docker/staging/nginx/conf.d/default.conf b/docker/staging/nginx/conf.d/default.conf index 4ea324b11..c755ddef1 100755 --- a/docker/staging/nginx/conf.d/default.conf +++ b/docker/staging/nginx/conf.d/default.conf @@ -16,7 +16,7 @@ server { } location /api/ { - proxy_pass http://server:5000/api/; + proxy_pass http://server:52345/api/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -29,7 +29,7 @@ server { } location /api-docs/ { - proxy_pass http://server:5000/api-docs/; + proxy_pass http://server:52345/api-docs/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -54,7 +54,7 @@ server { } location /api/ { - proxy_pass http://server:5000/api/; + proxy_pass http://server:52345/api/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -67,7 +67,7 @@ server { } location /api-docs/ { - proxy_pass http://server:5000/api-docs/; + proxy_pass http://server:52345/api-docs/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/docker/staging/server.Dockerfile b/docker/staging/server.Dockerfile index 998bbe4fe..c80f468f8 100755 --- a/docker/staging/server.Dockerfile +++ b/docker/staging/server.Dockerfile @@ -10,6 +10,6 @@ RUN npm install COPY ./server ./ -EXPOSE 5000 +EXPOSE 52345 CMD ["node", "index.js"] \ No newline at end of file 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; diff --git a/server/controllers/settingsController.js b/server/controllers/settingsController.js index 54643dfbb..b557e8dd6 100755 --- a/server/controllers/settingsController.js +++ b/server/controllers/settingsController.js @@ -11,16 +11,28 @@ class SettingsController { } getAppSettings = async (req, res, next) => { - try { - const settings = { ...(await this.settingsService.getSettings()) }; - delete settings.jwtSecret; - return res.success({ - msg: this.stringService.getAppSettings, - data: settings, - }); - } catch (error) { - next(handleError(error, SERVICE_NAME, "getAppSettings")); + const dbSettings = await this.settingsService.getDBSettings(); + const sanitizedSettings = { ...dbSettings }; + + const returnSettings = { + pagespeedKeySet: false, + emailPasswordSet: false, + }; + + if (typeof sanitizedSettings.pagespeedApiKey !== "undefined") { + 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: returnSettings, + }); }; updateAppSettings = async (req, res, next) => { diff --git a/server/db/models/AppSettings.js b/server/db/models/AppSettings.js index 6415b229d..06ba8c1df 100755 --- a/server/db/models/AppSettings.js +++ b/server/db/models/AppSettings.js @@ -2,64 +2,31 @@ import mongoose from "mongoose"; const AppSettingsSchema = mongoose.Schema( { - apiBaseUrl: { - type: String, - required: true, - default: "http://localhost:5000/api/v1", + checkTTL: { + type: Number, + default: 30, }, - logLevel: { + language: { type: String, - default: "debug", - enum: ["debug", "none", "error", "warn"], - }, - clientHost: { - type: String, - required: true, - default: "http://localhost:5173", - }, - jwtSecret: { - type: String, - required: true, - default: "my_secret", - }, - dbType: { - type: String, - required: true, - default: "MongoDB", - }, - dbConnectionString: { - type: String, - required: true, - default: "mongodb://localhost:27017/uptime_db", - }, - redisUrl: { - type: String, - default: "redis://127.0.0.1:6379", - }, - jwtTTL: { - type: String, - required: true, - default: "2h", + default: "gb", }, pagespeedApiKey: { type: String, - default: "", }, systemEmailHost: { type: String, - default: "smtp.gmail.com", }, systemEmailPort: { type: Number, - default: 465, }, systemEmailAddress: { type: String, - default: "", }, systemEmailPassword: { type: String, - default: "", + }, + systemEmailUser: { + type: String, }, singleton: { type: Boolean, @@ -73,4 +40,4 @@ const AppSettingsSchema = mongoose.Schema( } ); -export default mongoose.model("AppSettings", AppSettingsSchema); \ No newline at end of file +export default mongoose.model("AppSettings", AppSettingsSchema); diff --git a/server/db/mongo/MongoDB.js b/server/db/mongo/MongoDB.js index dd9826de5..b657caec4 100755 --- a/server/db/mongo/MongoDB.js +++ b/server/db/mongo/MongoDB.js @@ -76,7 +76,8 @@ import * as diagnosticModule from "./modules/diagnosticModule.js"; class MongoDB { static SERVICE_NAME = "MongoDB"; - constructor() { + constructor({ appSettings }) { + this.appSettings = appSettings; Object.assign(this, userModule); Object.assign(this, inviteModule); Object.assign(this, recoveryModule); @@ -95,8 +96,7 @@ class MongoDB { connect = async () => { try { const connectionString = - process.env.DB_CONNECTION_STRING || "mongodb://localhost:27017/uptime_db"; - console.log("Connecting to MongoDB with connection string:", connectionString); + this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db"; await mongoose.connect(connectionString); // If there are no AppSettings, create one await AppSettings.findOneAndUpdate( diff --git a/server/db/mongo/modules/settingsModule.js b/server/db/mongo/modules/settingsModule.js index 3b5f68e19..d3568ad32 100755 --- a/server/db/mongo/modules/settingsModule.js +++ b/server/db/mongo/modules/settingsModule.js @@ -14,11 +14,22 @@ const getAppSettings = async () => { const updateAppSettings = async (newSettings) => { try { - const settings = await AppSettings.findOneAndUpdate( - {}, - { $set: newSettings }, - { new: true } - ); + const update = { $set: { ...newSettings } }; + + if (newSettings.pagespeedApiKey === "") { + update.$unset = { pagespeedApiKey: "" }; + delete update.$set.pagespeedApiKey; + } + + if (newSettings.systemEmailPassword === "") { + update.$unset = { systemEmailPassword: "" }; + delete update.$set.systemEmailPassword; + } + + const settings = await AppSettings.findOneAndUpdate({}, update, { + new: true, + upsert: true, + }); return settings; } catch (error) { error.service = SERVICE_NAME; diff --git a/server/index.js b/server/index.js index 5fa10bcab..f299b32e9 100755 --- a/server/index.js +++ b/server/index.js @@ -100,8 +100,6 @@ const openApiSpec = JSON.parse( let server; -const PORT = 5000; - const shutdown = async () => { if (isShuttingDown) { return; @@ -139,19 +137,21 @@ const shutdown = async () => { // Need to wrap server setup in a function to handle async nature of JobQueue const startApp = async () => { const app = express(); - const allowedOrigin = process.env.CLIENT_HOST; // Create and Register Primary services const translationService = new TranslationService(logger); const stringService = new StringService(translationService); ServiceRegistry.register(StringService.SERVICE_NAME, stringService); - // Create DB - const db = new MongoDB(); - await db.connect(); - // Create services const settingsService = new SettingsService(AppSettings); - await settingsService.loadSettings(); + const appSettings = settingsService.loadSettings(); + + // Create DB + const db = new MongoDB({ appSettings }); + await db.connect(); + + // Set allowed origin + const allowedOrigin = appSettings.clientHost; const networkService = new NetworkService( axios, @@ -215,8 +215,9 @@ const startApp = async () => { await translationService.initialize(); - server = app.listen(PORT, () => { - logger.info({ message: `server started on port:${PORT}` }); + const port = appSettings.port || 52345; + server = app.listen(port, () => { + logger.info({ message: `Server started on port:${port}` }); }); process.on("SIGUSR2", shutdown); @@ -237,7 +238,7 @@ const startApp = async () => { ServiceRegistry.get(SettingsService.SERVICE_NAME), ServiceRegistry.get(JobQueue.SERVICE_NAME), ServiceRegistry.get(StringService.SERVICE_NAME), - ServiceRegistry.get(EmailService.SERVICE_NAME), + ServiceRegistry.get(EmailService.SERVICE_NAME) ); const settingsController = new SettingsController( 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); diff --git a/server/service/settingsService.js b/server/service/settingsService.js index 6c7beb456..290e645e9 100755 --- a/server/service/settingsService.js +++ b/server/service/settingsService.js @@ -1,27 +1,26 @@ const SERVICE_NAME = "SettingsService"; -import dotenv from "dotenv"; -dotenv.config(); const envConfig = { + nodeEnv: process.env.NODE_ENV, logLevel: process.env.LOG_LEVEL, - clientHost: process.env.CLIENT_HOST, - jwtSecret: process.env.JWT_SECRET, - dbType: process.env.DB_TYPE, - dbConnectionString: process.env.DB_CONNECTION_STRING, - redisUrl: process.env.REDIS_URL, - jwtTTL: process.env.TOKEN_TTL, - pagespeedApiKey: process.env.PAGESPEED_API_KEY, systemEmailHost: process.env.SYSTEM_EMAIL_HOST, systemEmailPort: process.env.SYSTEM_EMAIL_PORT, systemEmailUser: process.env.SYSTEM_EMAIL_USER, systemEmailAddress: process.env.SYSTEM_EMAIL_ADDRESS, systemEmailPassword: process.env.SYSTEM_EMAIL_PASSWORD, + jwtSecret: process.env.JWT_SECRET, + jwtTTL: process.env.TOKEN_TTL, + clientHost: process.env.CLIENT_HOST, + dbConnectionString: process.env.DB_CONNECTION_STRING, + redisUrl: process.env.REDIS_URL, + callbackUrl: process.env.CALLBACK_URL, + port: process.env.PORT, + pagespeedApiKey: process.env.PAGESPEED_API_KEY, + uprockApiKey: process.env.UPROCK_API_KEY, }; /** * SettingsService * * This service is responsible for loading and managing the application settings. - * It gives priority to environment variables and will only load settings - * from the database if they are not set in the environment. */ class SettingsService { static SERVICE_NAME = SERVICE_NAME; @@ -34,40 +33,17 @@ class SettingsService { this.settings = { ...envConfig }; } /** - * Load settings from the database and merge with environment settings. - * If there are any settings that weren't set by environment variables, use user settings from the database. - * @returns {Promise} The merged settings. - * @throws Will throw an error if settings are not found in the database or if settings have not been loaded. - */ async loadSettings() { - try { - const dbSettings = await this.appSettings.findOne(); - if (!this.settings) { - throw new Error("Settings not found"); - } - - // If there are any settings that weren't set by environment variables, use user settings from DB - for (const key in envConfig) { - if ( - typeof envConfig?.[key] === "undefined" && - typeof dbSettings?.[key] !== "undefined" - ) { - this.settings[key] = dbSettings[key]; - } - } - - await this.appSettings.updateOne({}, { $set: this.settings }, { upsert: true }); - return this.settings; - } catch (error) { - error.service === undefined ? (error.service = SERVICE_NAME) : null; - error.method === undefined ? (error.method = "loadSettings") : null; - throw error; - } + * Load settings from env settings + * @returns {Object>} The settings. + */ + loadSettings() { + return this.settings; } /** * Reload settings by calling loadSettings. * @returns {Promise} The reloaded settings. */ - async reloadSettings() { + reloadSettings() { return this.loadSettings(); } /** @@ -81,6 +57,21 @@ class SettingsService { } return this.settings; } + + async getDBSettings() { + 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; + } } export default SettingsService; diff --git a/server/validation/joi.js b/server/validation/joi.js index bd402fb8a..997d35d65 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -424,21 +424,14 @@ 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(""), systemEmailPort: joi.number().allow(""), systemEmailAddress: joi.string().allow(""), systemEmailPassword: joi.string().allow(""), + systemEmailUser: joi.string().allow(""), }); //****************************************