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 && (
},
- { name: "Pagespeed", path: "pagespeed", icon: },
- { name: "Infrastructure", path: "infrastructure", icon: },
+const getMenu = (t) => [
+ { name: t("menu.uptime"), path: "uptime", icon: },
+ { name: t("menu.pagespeed"), path: "pagespeed", icon: },
+ { name: t("menu.infrastructure"), path: "infrastructure", icon: },
{
- name: "Distributed uptime",
+ name: t("menu.distributedUptime"),
path: "distributed-uptime",
icon: ,
},
- { name: "Incidents", path: "incidents", icon: },
+ { name: t("menu.incidents"), path: "incidents", icon: },
- { name: "Status pages", path: "status", icon: },
- { name: "Maintenance", path: "maintenance", icon: },
- // { name: "Integrations", path: "integrations", icon: },
+ { name: t("menu.statusPages"), path: "status", icon: },
+ { name: t("menu.maintenance"), path: "maintenance", icon: },
+ // { name: t("menu.integrations"), path: "integrations", icon: },
{
- name: "Settings",
+ name: t("menu.settings"),
icon: ,
path: "settings",
},
];
-const otherMenuItems = [
- { name: "Support", path: "support", icon: },
+const getOtherMenuItems = (t) => [
+ { name: t("menu.support"), path: "support", icon: },
{
- name: "Discussions",
+ name: t("menu.discussions"),
path: "discussions",
icon: ,
},
- { name: "Docs", path: "docs", icon: },
- { name: "Changelog", path: "changelog", icon: },
+ { name: t("menu.docs"), path: "docs", icon: },
+ { name: t("menu.changelog"), path: "changelog", icon: },
];
-const accountMenuItems = [
- { name: "Profile", path: "account/profile", icon: },
- { name: "Password", path: "account/password", icon: },
- { name: "Team", path: "account/team", icon: },
+const getAccountMenuItems = (t) => [
+ { name: t("menu.profile"), path: "account/profile", icon: },
+ { name: t("menu.password"), path: "account/password", icon: },
+ { name: t("menu.team"), path: "account/team", icon: },
];
/* TODO this could be a key in nested Path would be the link */
@@ -121,6 +121,10 @@ function Sidebar() {
const dispatch = useDispatch();
const { t } = useTranslation();
const authState = useSelector((state) => state.auth);
+
+ const menu = getMenu(t);
+ const otherMenuItems = getOtherMenuItems(t);
+ const accountMenuItems = getAccountMenuItems(t);
const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
const [open, setOpen] = useState({ Dashboard: false, Account: false, Other: false });
const [anchorEl, setAnchorEl] = useState(null);
@@ -728,7 +732,11 @@ function Sidebar() {
{authState.user?.firstName} {authState.user?.lastName}
- {authState.user?.role}
+ {authState.user?.role?.includes("superadmin") ? t("roles.superAdmin") :
+ authState.user?.role?.includes("admin") ? t("roles.admin") :
+ authState.user?.role?.includes("user") ? t("roles.teamMember") :
+ authState.user?.role?.includes("demo") ? t("roles.demoUser") :
+ authState.user?.role}
{
const theme = useTheme();
+ const { t } = useTranslation();
const SPACING_GAP = theme.spacing(12);
const [toInvite, setToInvite] = useState({
@@ -39,7 +40,7 @@ const TeamPanel = () => {
const headers = [
{
id: "name",
- content: "Name",
+ content: t("teamPanel.table.name"),
render: (row) => {
return (
@@ -47,16 +48,16 @@ const TeamPanel = () => {
{row.firstName + " " + row.lastName}
- Created {new Date(row.createdAt).toLocaleDateString()}
+ {t("teamPanel.table.created")} {new Date(row.createdAt).toLocaleDateString()}
);
},
},
- { id: "email", content: "Email", render: (row) => row.email },
+ { id: "email", content: t("teamPanel.table.email"), render: (row) => row.email },
{
id: "role",
- content: "Role",
+ content: t("teamPanel.table.role"),
render: (row) => row.role,
},
];
@@ -78,10 +79,10 @@ const TeamPanel = () => {
useEffect(() => {
const ROLE_MAP = {
- superadmin: "Super admin",
- admin: "Admin",
- user: "Team member",
- demo: "Demo User",
+ superadmin: t("roles.superAdmin"),
+ admin: t("roles.admin"),
+ user: t("roles.teamMember"),
+ demo: t("roles.demoUser"),
};
let team = members;
if (filter !== "all")
@@ -98,7 +99,7 @@ const TeamPanel = () => {
role: member.role.map((role) => ROLE_MAP[role]).join(","),
}));
setData(team);
- }, [filter, members]);
+ }, [members, filter, t]);
useEffect(() => {
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
@@ -197,7 +198,7 @@ const TeamPanel = () => {
spellCheck="false"
gap={SPACING_GAP}
>
- Team members
+ {t("teamPanel.teamMembers")}
{
filled={(filter === "all").toString()}
onClick={() => setFilter("all")}
>
- All
+ {t("teamPanel.filter.all")}
@@ -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 = () => {
/>
diff --git a/client/src/Components/Table/index.jsx b/client/src/Components/Table/index.jsx
index 199e548d7..b6d31bebe 100644
--- a/client/src/Components/Table/index.jsx
+++ b/client/src/Components/Table/index.jsx
@@ -65,7 +65,6 @@ const DataTable = ({
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
- fontSize: "13px",
},
"& :is(td)": {
backgroundColor: theme.palette.primary.main,
diff --git a/client/src/Hooks/useDeleteMonitorStats.js b/client/src/Hooks/useDeleteMonitorStats.js
new file mode 100644
index 000000000..64138929f
--- /dev/null
+++ b/client/src/Hooks/useDeleteMonitorStats.js
@@ -0,0 +1,24 @@
+import { useState } from "react";
+import { networkService } from "../main";
+import { createToast } from "../Utils/toastUtils";
+import { useTranslation } from "react-i18next";
+
+const UseDeleteMonitorStats = () => {
+ const { t } = useTranslation();
+ const [isLoading, setIsLoading] = useState(false);
+ const deleteMonitorStats = async ({ teamId }) => {
+ setIsLoading(true);
+ try {
+ const res = await networkService.deleteChecksByTeamId({ teamId });
+ createToast({ body: t("settingsStatsCleared") });
+ } catch (error) {
+ createToast({ body: t("settingsFailedToClearStats") });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [deleteMonitorStats, isLoading];
+};
+
+export { UseDeleteMonitorStats };
diff --git a/client/src/Hooks/useFetchSettings.js b/client/src/Hooks/useFetchSettings.js
new file mode 100644
index 000000000..f7651e789
--- /dev/null
+++ b/client/src/Hooks/useFetchSettings.js
@@ -0,0 +1,55 @@
+import { useState, useEffect } from "react";
+import { networkService } from "../main";
+import { createToast } from "../Utils/toastUtils";
+import { useTranslation } from "react-i18next";
+
+const useFetchSettings = ({ setSettingsData }) => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+ useEffect(() => {
+ const fetchSettings = async () => {
+ setIsLoading(true);
+ try {
+ const response = await networkService.getAppSettings();
+ setSettingsData(response?.data?.data);
+ } catch (error) {
+ createToast({ body: "Failed to fetch settings" });
+ setError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchSettings();
+ }, []);
+
+ return [isLoading, error];
+};
+
+const useSaveSettings = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+ const { t } = useTranslation();
+
+ const saveSettings = async (settings) => {
+ setIsLoading(true);
+ try {
+ await networkService.updateAppSettings({ settings });
+ if (settings.checkTTL) {
+ await networkService.updateChecksTTL({
+ ttl: settings.checkTTL,
+ });
+ }
+ createToast({ body: t("settingsSuccessSaved") });
+ } catch (error) {
+ createToast({ body: t("settingsFailedToSave") });
+
+ setError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [isLoading, error, saveSettings];
+};
+
+export { useFetchSettings, useSaveSettings };
diff --git a/client/src/Pages/Settings/SettingsAbout.jsx b/client/src/Pages/Settings/SettingsAbout.jsx
new file mode 100644
index 000000000..b2b5df308
--- /dev/null
+++ b/client/src/Pages/Settings/SettingsAbout.jsx
@@ -0,0 +1,32 @@
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import ConfigBox from "../../Components/ConfigBox";
+// Utils
+import { useTheme } from "@emotion/react";
+import { useTranslation } from "react-i18next";
+import Link from "../../Components/Link";
+
+const SettingsAbout = () => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+
+
+ {t("settingsAbout")}
+
+
+ Checkmate {2.0}
+
+ {t("settingsDevelopedBy")}
+
+
+
+
+ );
+};
+
+export default SettingsAbout;
diff --git a/client/src/Pages/Settings/SettingsDemoMonitors.jsx b/client/src/Pages/Settings/SettingsDemoMonitors.jsx
new file mode 100644
index 000000000..1a592e776
--- /dev/null
+++ b/client/src/Pages/Settings/SettingsDemoMonitors.jsx
@@ -0,0 +1,90 @@
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import Button from "@mui/material/Button";
+import ConfigBox from "../../Components/ConfigBox";
+// Utils
+import { useTheme } from "@emotion/react";
+import { PropTypes } from "prop-types";
+import { useTranslation } from "react-i18next";
+import Dialog from "../../Components/Dialog";
+import { useState } from "react";
+
+const SettingsDemoMonitors = ({ HEADER_SX, handleChange, isLoading }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ // Local state
+ const [isOpen, setIsOpen] = useState(false);
+ return (
+ <>
+
+
+ {t("settingsDemoMonitors")}
+ {t("settingsDemoMonitorsDescription")}
+
+
+ {t("settingsAddDemoMonitors")}
+
+
+
+
+
+ {t("settingsSystemReset")}
+
+ {t("settingsSystemResetDescription")}
+
+
+
+ {t("settingsRemoveAllMonitors")}
+
+
+
+ >
+ );
+};
+
+SettingsDemoMonitors.propTypes = {
+ handleChange: PropTypes.func,
+ HEADER_SX: PropTypes.object,
+};
+
+export default SettingsDemoMonitors;
diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx
new file mode 100644
index 000000000..ecc2a51f2
--- /dev/null
+++ b/client/src/Pages/Settings/SettingsEmail.jsx
@@ -0,0 +1,126 @@
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import ConfigBox from "../../Components/ConfigBox";
+import TextInput from "../../Components/Inputs/TextInput";
+import Button from "@mui/material/Button";
+import Stack from "@mui/material/Stack";
+// Utils
+import { useTheme } from "@emotion/react";
+import { PropTypes } from "prop-types";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
+const SettingsEmail = ({
+ HEADER_SX,
+ handleChange,
+ settingsData,
+ setSettingsData,
+ isPasswordSet,
+}) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ const [password, setPassword] = useState("");
+ const [hasBeenReset, setHasBeenReset] = useState(false);
+
+ const handlePasswordChange = (e) => {
+ setPassword(e.target.value);
+ setSettingsData({
+ ...settingsData,
+ settings: { ...settingsData.settings, systemEmailPassword: e.target.value },
+ });
+ };
+
+ return (
+
+
+ {t("settingsEmail")}
+ {t("settingsEmailDescription")}
+
+
+
+
+ {t("settingsEmailHost")}
+
+
+
+ {t("settingsEmailPort")}
+
+
+
+ {t("settingsEmailUser")}
+
+
+
+ {t("settingsEmailAddress")}
+
+
+ {(isPasswordSet === false || hasBeenReset === true) && (
+
+ {t("settingsEmailPassword")}
+ }
+ />
+
+ )}
+ {isPasswordSet === true && hasBeenReset === false && (
+
+ {t("settingsEmailFieldResetLabel")}
+
+
+ )}
+
+
+
+ );
+};
+
+SettingsEmail.propTypes = {
+ settingsData: PropTypes.object,
+ setSettingsData: PropTypes.func,
+ handleChange: PropTypes.func,
+ HEADER_SX: PropTypes.object,
+ isPasswordSet: PropTypes.bool,
+};
+
+export default SettingsEmail;
diff --git a/client/src/Pages/Settings/SettingsPagespeed.jsx b/client/src/Pages/Settings/SettingsPagespeed.jsx
new file mode 100644
index 000000000..95b159fa5
--- /dev/null
+++ b/client/src/Pages/Settings/SettingsPagespeed.jsx
@@ -0,0 +1,87 @@
+import Box from "@mui/material/Box";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import Button from "@mui/material/Button";
+import ConfigBox from "../../Components/ConfigBox";
+import TextInput from "../../Components/Inputs/TextInput";
+import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
+// Utils
+import { useTheme } from "@emotion/react";
+import { PropTypes } from "prop-types";
+import { useState, useEffect } from "react";
+import { useTranslation } from "react-i18next";
+
+const SettingsPagespeed = ({
+ HEADING_SX,
+ settingsData,
+ setSettingsData,
+ isApiKeySet,
+}) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ // Local state
+ const [apiKey, setApiKey] = useState("");
+ const [hasBeenReset, setHasBeenReset] = useState(false);
+
+ // Handler
+ const handleChange = (e) => {
+ setApiKey(e.target.value);
+ setSettingsData({
+ ...settingsData,
+ settings: { ...settingsData.settings, pagespeedApiKey: e.target.value },
+ });
+ };
+
+ return (
+
+
+ {t("pageSpeedApiKeyFieldTitle")}
+ {t("pageSpeedApiKeyFieldDescription")}
+
+
+ {(isApiKeySet === false || hasBeenReset === true) && (
+ }
+ />
+ )}
+
+ {isApiKeySet === true && hasBeenReset === false && (
+
+ {t("pageSpeedApiKeyFieldResetLabel")}
+
+
+ )}
+
+
+ );
+};
+
+SettingsPagespeed.propTypes = {
+ HEADING_SX: PropTypes.object,
+ settingsData: PropTypes.object,
+ setSettingsData: PropTypes.func,
+ isApiKeySet: PropTypes.bool,
+};
+
+export default SettingsPagespeed;
diff --git a/client/src/Pages/Settings/SettingsStats.jsx b/client/src/Pages/Settings/SettingsStats.jsx
new file mode 100644
index 000000000..961f13665
--- /dev/null
+++ b/client/src/Pages/Settings/SettingsStats.jsx
@@ -0,0 +1,84 @@
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import Button from "@mui/material/Button";
+import Stack from "@mui/material/Stack";
+import ConfigBox from "../../Components/ConfigBox";
+import TextInput from "../../Components/Inputs/TextInput";
+import Dialog from "../../Components/Dialog";
+
+// Utils
+import { useTheme } from "@emotion/react";
+import { PropTypes } from "prop-types";
+import { useTranslation } from "react-i18next";
+import { useState } from "react";
+
+const SettingsStats = ({ HEADING_SX, handleChange, settingsData, errors }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const [isOpen, setIsOpen] = useState(false);
+ return (
+
+
+
+ {t("settingsHistoryAndMonitoring")}
+
+
+ {t("settingsHistoryAndMonitoringDescription")}
+
+
+
+
+
+ {t("settingsClearAllStats")}
+
+
+
+
+ );
+};
+
+SettingsStats.propTypes = {
+ HEADING_SX: PropTypes.object,
+ handleChange: PropTypes.func,
+ settingsData: PropTypes.object,
+ errors: PropTypes.object,
+};
+
+export default SettingsStats;
diff --git a/client/src/Pages/Settings/SettingsTimeZone.jsx b/client/src/Pages/Settings/SettingsTimeZone.jsx
new file mode 100644
index 000000000..e6ba1a77b
--- /dev/null
+++ b/client/src/Pages/Settings/SettingsTimeZone.jsx
@@ -0,0 +1,43 @@
+import Box from "@mui/material/Box";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import ConfigBox from "../../Components/ConfigBox";
+import Select from "../../Components/Inputs/Select";
+import timezones from "../../Utils/timezones.json";
+
+// Utils
+import { useTheme } from "@emotion/react";
+import { PropTypes } from "prop-types";
+import { useTranslation } from "react-i18next";
+const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ return (
+
+
+ {t("settingsGeneralSettings")}
+
+ {t("settingsDisplayTimezone")}-{" "}
+ {t("settingsDisplayTimezoneDescription")}
+
+
+
+
+
+
+ );
+};
+
+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")}
-
-
-
-
-
-
-
- {t("settingsAppearance")}
-
- {t("settingsAppearanceDescription")}
-
-
-
-
-
-
-
- {/* {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")}
-
-
-
-
- )}
- {isAdmin && (
- <>
- {/* Demo Monitors Section */}
-
-
- {t("settingsDemoMonitors")}
-
- {t("settingsDemoMonitorsDescription")}
-
-
-
- {t("settingsAddDemoMonitors")}
-
-
-
-
- {/* System Reset Section */}
-
-
- {t("settingsSystemReset")}
-
- {t("settingsSystemResetDescription")}
-
-
-
- {t("settingsRemoveAllMonitors")}
-
-
-
- >
- )}
-
-
-
- {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 template 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 template 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": "Şablonumuzu 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