Merge latest changes from develop branch

This commit is contained in:
mohadeseh safari
2025-05-12 11:27:08 -04:00
47 changed files with 1699 additions and 1245 deletions

View File

@@ -18,7 +18,7 @@ Fixes #123
const { t } = useTranslation();
<div>{t('add')}</div>
```
- [ ] 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.

View File

@@ -156,7 +156,7 @@ const Select = ({
};
Select.propTypes = {
id: PropTypes.string.isRequired,
id: PropTypes.string,
name: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,

View File

@@ -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,

View File

@@ -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")}
</Button>
{bulkPath && (
<Button

View File

@@ -52,42 +52,42 @@ import { clearAuthState } from "../../Features/Auth/authSlice";
import { toggleSidebar } from "../../Features/UI/uiSlice";
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
const menu = [
{ name: "Uptime", path: "uptime", icon: <Monitors /> },
{ name: "Pagespeed", path: "pagespeed", icon: <PageSpeed /> },
{ name: "Infrastructure", path: "infrastructure", icon: <Integrations /> },
const getMenu = (t) => [
{ name: t("menu.uptime"), path: "uptime", icon: <Monitors /> },
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
{
name: "Distributed uptime",
name: t("menu.distributedUptime"),
path: "distributed-uptime",
icon: <DistributedUptimeIcon />,
},
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
{ name: "Status pages", path: "status", icon: <StatusPages /> },
{ name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
// { name: "Integrations", path: "integrations", icon: <Integrations /> },
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
{ name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
// { name: t("menu.integrations"), path: "integrations", icon: <Integrations /> },
{
name: "Settings",
name: t("menu.settings"),
icon: <Settings />,
path: "settings",
},
];
const otherMenuItems = [
{ name: "Support", path: "support", icon: <Support /> },
const getOtherMenuItems = (t) => [
{ name: t("menu.support"), path: "support", icon: <Support /> },
{
name: "Discussions",
name: t("menu.discussions"),
path: "discussions",
icon: <Discussions />,
},
{ name: "Docs", path: "docs", icon: <Docs /> },
{ name: "Changelog", path: "changelog", icon: <ChangeLog /> },
{ name: t("menu.docs"), path: "docs", icon: <Docs /> },
{ name: t("menu.changelog"), path: "changelog", icon: <ChangeLog /> },
];
const accountMenuItems = [
{ name: "Profile", path: "account/profile", icon: <UserSvg /> },
{ name: "Password", path: "account/password", icon: <LockSvg /> },
{ name: "Team", path: "account/team", icon: <TeamSvg /> },
const getAccountMenuItems = (t) => [
{ name: t("menu.profile"), path: "account/profile", icon: <UserSvg /> },
{ name: t("menu.password"), path: "account/password", icon: <LockSvg /> },
{ name: t("menu.team"), path: "account/team", icon: <TeamSvg /> },
];
/* 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}
</Typography>
<Typography sx={{ textTransform: "capitalize" }}>
{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}
</Typography>
</Box>
<Stack

View File

@@ -2,11 +2,11 @@ import { useTheme } from "@emotion/react";
import TabPanel from "@mui/lab/TabPanel";
import { Button, ButtonGroup, Stack, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import TextInput from "../../Inputs/TextInput";
import { credentials } from "../../../Validation/validation";
import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import { useSelector } from "react-redux";
import Select from "../../Inputs/Select";
import { GenericDialog } from "../../Dialog/genericDialog";
import DataTable from "../../Table/";
@@ -21,6 +21,7 @@ import { useGetInviteToken } from "../../../Hooks/inviteHooks";
const TeamPanel = () => {
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 (
<Stack>
@@ -47,16 +48,16 @@ const TeamPanel = () => {
{row.firstName + " " + row.lastName}
</Typography>
<Typography>
Created {new Date(row.createdAt).toLocaleDateString()}
{t("teamPanel.table.created")} {new Date(row.createdAt).toLocaleDateString()}
</Typography>
</Stack>
);
},
},
{ 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}
>
<Typography component="h1">Team members</Typography>
<Typography component="h1">{t("teamPanel.teamMembers")}</Typography>
<Stack
direction="row"
justifyContent="space-between"
@@ -214,21 +215,21 @@ const TeamPanel = () => {
filled={(filter === "all").toString()}
onClick={() => setFilter("all")}
>
All
{t("teamPanel.filter.all")}
</Button>
<Button
variant="group"
filled={(filter === "admin").toString()}
onClick={() => setFilter("admin")}
>
Super admin
{t("roles.superAdmin")}
</Button>
<Button
variant="group"
filled={(filter === "user").toString()}
onClick={() => setFilter("user")}
>
Member
{t("teamPanel.filter.member")}
</Button>
</ButtonGroup>
</Stack>
@@ -237,22 +238,20 @@ const TeamPanel = () => {
color="accent"
onClick={() => setIsOpen(true)}
>
Invite a team member
{t("teamPanel.inviteTeamMember")}
</Button>
</Stack>
<DataTable
headers={headers}
data={data}
config={{ emptyView: "There are no team members with this role" }}
config={{ emptyView: t("teamPanel.noMembers") }}
/>
</Stack>
<GenericDialog
title={"Invite new team member"}
description={
"When you add a new team member, they will get access to all monitors."
}
title={t("teamPanel.inviteNewTeamMember")}
description={t("teamPanel.inviteDescription")}
open={isOpen}
onClose={closeInviteModal}
theme={theme}
@@ -261,7 +260,7 @@ const TeamPanel = () => {
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 = () => {
/>
<Select
id="team-member-role"
placeholder="Select role"
placeholder={t("teamPanel.selectRole")}
isHidden={true}
value={toInvite.role[0]}
onChange={(event) =>
@@ -279,11 +278,11 @@ const TeamPanel = () => {
}))
}
items={[
{ _id: "admin", name: "Admin" },
{ _id: "user", name: "User" },
{ _id: "admin", name: t("roles.admin") },
{ _id: "user", name: t("roles.teamMember") },
]}
/>
{token && <Typography>Invite link</Typography>}
{token && <Typography>{t("teamPanel.inviteLink")}</Typography>}
{token && (
<TextInput
id="invite-token"
@@ -302,7 +301,7 @@ const TeamPanel = () => {
color="error"
onClick={closeInviteModal}
>
Cancel
{t("teamPanel.cancel")}
</Button>
<Button
variant="contained"
@@ -311,7 +310,7 @@ const TeamPanel = () => {
loading={isSendingInvite}
disabled={isDisabled}
>
Get token
{t("teamPanel.getToken")}
</Button>
<Button
variant="contained"
@@ -320,7 +319,7 @@ const TeamPanel = () => {
loading={isSendingInvite}
disabled={isDisabled}
>
E-mail token
{t("teamPanel.emailToken")}
</Button>
</Stack>
</GenericDialog>

View File

@@ -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,

View File

@@ -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 };

View File

@@ -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 };

View File

@@ -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 (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsAbout")}</Typography>
</Box>
<Box>
<Typography component="h2">Checkmate {2.0}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
{t("settingsDevelopedBy")}
</Typography>
<Link
level="secondary"
url="https://github.com/bluewave-labs/checkmate"
label="https://github.com/bluewave-labs/checkmate"
/>
</Box>
</ConfigBox>
);
};
export default SettingsAbout;

View File

@@ -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 (
<>
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
<Typography sx={HEADER_SX}>{t("settingsDemoMonitorsDescription")}</Typography>
</Box>
<Box>
<Typography>{t("settingsAddDemoMonitors")}</Typography>
<Button
variant="contained"
color="accent"
loading={isLoading}
onClick={() => {
const syntheticEvent = {
target: {
name: "demo",
},
};
handleChange(syntheticEvent);
}}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsAddDemoMonitorsButton")}
</Button>
</Box>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsSystemReset")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsSystemResetDescription")}
</Typography>
</Box>
<Box>
<Typography>{t("settingsRemoveAllMonitors")}</Typography>
<Button
variant="contained"
color="error"
loading={isLoading}
onClick={() => setIsOpen(true)}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsRemoveAllMonitorsButton")}
</Button>
</Box>
<Dialog
open={isOpen}
theme={theme}
title={t("settingsRemoveAllMonitorsDialogTitle")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
onConfirm={() => {
const syntheticEvent = {
target: {
name: "deleteMonitors",
},
};
handleChange(syntheticEvent);
setIsOpen(false);
}}
isLoading={isLoading}
/>
</ConfigBox>
</>
);
};
SettingsDemoMonitors.propTypes = {
handleChange: PropTypes.func,
HEADER_SX: PropTypes.object,
};
export default SettingsDemoMonitors;

View File

@@ -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 (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsEmail")}</Typography>
<Typography sx={HEADER_SX}>{t("settingsEmailDescription")}</Typography>
</Box>
<Box>
<Stack gap={theme.spacing(10)}>
<Box>
<Typography>{t("settingsEmailHost")}</Typography>
<TextInput
name="systemEmailHost"
placeholder="smtp.gmail.com"
value={settingsData?.settings?.systemEmailHost ?? ""}
onChange={handleChange}
/>
</Box>
<Box>
<Typography>{t("settingsEmailPort")}</Typography>
<TextInput
name="systemEmailPort"
placeholder="425"
type="number"
value={settingsData?.settings?.systemEmailPort ?? ""}
onChange={handleChange}
/>
</Box>
<Box>
<Typography>{t("settingsEmailUser")}</Typography>
<TextInput
name="systemEmailUser"
placeholder="Leave empty if not required"
value={settingsData?.settings?.systemEmailUser ?? ""}
onChange={handleChange}
/>
</Box>
<Box>
<Typography>{t("settingsEmailAddress")}</Typography>
<TextInput
name="systemEmailAddress"
placeholder="uptime@bluewavelabs.ca"
value={settingsData?.settings?.systemEmailAddress ?? ""}
onChange={handleChange}
/>
</Box>
{(isPasswordSet === false || hasBeenReset === true) && (
<Box>
<Typography>{t("settingsEmailPassword")}</Typography>
<TextInput
name="systemEmailPassword"
type="password"
placeholder="123 456 789 101112"
value={password}
onChange={handlePasswordChange}
endAdornment={<PasswordEndAdornment />}
/>
</Box>
)}
{isPasswordSet === true && hasBeenReset === false && (
<Box>
<Typography>{t("settingsEmailFieldResetLabel")}</Typography>
<Button
onClick={() => {
setPassword("");
setSettingsData({
...settingsData,
settings: { ...settingsData.settings, systemEmailPassword: "" },
});
setHasBeenReset(true);
}}
variant="contained"
color="error"
sx={{ mt: theme.spacing(4) }}
>
{t("reset")}
</Button>
</Box>
)}
</Stack>
</Box>
</ConfigBox>
);
};
SettingsEmail.propTypes = {
settingsData: PropTypes.object,
setSettingsData: PropTypes.func,
handleChange: PropTypes.func,
HEADER_SX: PropTypes.object,
isPasswordSet: PropTypes.bool,
};
export default SettingsEmail;

View File

@@ -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 (
<ConfigBox>
<Box>
<Typography component="h1">{t("pageSpeedApiKeyFieldTitle")}</Typography>
<Typography sx={HEADING_SX}>{t("pageSpeedApiKeyFieldDescription")}</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
{(isApiKeySet === false || hasBeenReset === true) && (
<TextInput
name="pagespeedApiKey"
label={t("pageSpeedApiKeyFieldLabel")}
value={apiKey}
type={"password"}
onChange={handleChange}
optionalLabel="(Optional)"
endAdornment={<PasswordEndAdornment />}
/>
)}
{isApiKeySet === true && hasBeenReset === false && (
<Box>
<Typography>{t("pageSpeedApiKeyFieldResetLabel")}</Typography>
<Button
onClick={() => {
setApiKey("");
setSettingsData({
...settingsData,
settings: { ...settingsData.settings, pagespeedApiKey: "" },
});
setHasBeenReset(true);
}}
variant="contained"
color="error"
sx={{ mt: theme.spacing(4) }}
>
{t("reset")}
</Button>
</Box>
)}
</Stack>
</ConfigBox>
);
};
SettingsPagespeed.propTypes = {
HEADING_SX: PropTypes.object,
settingsData: PropTypes.object,
setSettingsData: PropTypes.func,
isApiKeySet: PropTypes.bool,
};
export default SettingsPagespeed;

View File

@@ -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 (
<ConfigBox>
<Box>
<Typography
component="h1"
sx={HEADING_SX}
>
{t("settingsHistoryAndMonitoring")}
</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsHistoryAndMonitoringDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
name="checkTTL"
label={t("settingsTTLLabel")}
optionalLabel={t("settingsTTLOptionalLabel")}
value={settingsData?.settings?.checkTTL ?? ""}
onChange={handleChange}
type="number"
error={errors.checkTTL ? true : false}
helperText={errors.checkTTL}
/>
<Box>
<Typography>{t("settingsClearAllStats")}</Typography>
<Button
variant="contained"
color="error"
onClick={() => setIsOpen(true)}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsClearAllStatsButton")}
</Button>
</Box>
</Stack>
<Dialog
open={isOpen}
theme={theme}
title={t("settingsClearAllStatsDialogTitle")}
description={t("settingsClearAllStatsDialogDescription")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")}
onConfirm={() => {
const syntheticEvent = {
target: {
name: "deleteStats",
},
};
handleChange(syntheticEvent);
setIsOpen(false);
}}
isLoading={false}
/>
</ConfigBox>
);
};
SettingsStats.propTypes = {
HEADING_SX: PropTypes.object,
handleChange: PropTypes.func,
settingsData: PropTypes.object,
errors: PropTypes.object,
};
export default SettingsStats;

View File

@@ -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 (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsGeneralSettings")}</Typography>
<Typography sx={HEADING_SX}>
<Typography component="span">{t("settingsDisplayTimezone")}</Typography>-{" "}
{t("settingsDisplayTimezoneDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
label={t("settingsDisplayTimezone")}
name="timezone"
value={timezone}
onChange={handleChange}
items={timezones}
/>
</Stack>
</ConfigBox>
);
};
SettingsTimeZone.propTypes = {
HEADING_SX: PropTypes.object,
handleChange: PropTypes.func,
timezone: PropTypes.string,
};
export default SettingsTimeZone;

View File

@@ -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 (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsAppearance")}</Typography>
<Typography sx={HEADING_SX}>{t("settingsAppearanceDescription")}</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
name="mode"
label={t("settingsThemeMode")}
value={mode}
onChange={handleChange}
items={[
{ _id: "light", name: "Light" },
{ _id: "dark", name: "Dark" },
]}
></Select>
<Select
name="language"
label={t("settingsLanguage")}
value={language}
onChange={handleChange}
items={languages.map((lang) => ({ _id: lang, name: lang.toUpperCase() }))}
></Select>
</Stack>
</ConfigBox>
);
};
SettingsUI.propTypes = {
HEADING_SX: PropTypes.object,
handleChange: PropTypes.func,
mode: PropTypes.string,
language: PropTypes.string,
};
export default SettingsUI;

View File

@@ -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 (
<Box
className="settings"
style={{
paddingBottom: 0,
}}
>
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<Typography variant="h1">Settings</Typography>
<SettingsTimeZone
HEADING_SX={HEADING_SX}
handleChange={handleChange}
timezone={timezone}
/>
<SettingsUI
HEADING_SX={HEADING_SX}
handleChange={handleChange}
mode={mode}
language={language}
/>
<SettingsPagespeed
HEADING_SX={HEADING_SX}
settingsData={settingsData}
setSettingsData={setSettingsData}
isApiKeySet={settingsData?.pagespeedKeySet ?? false}
/>
<SettingsStats
HEADING_SX={HEADING_SX}
settingsData={settingsData}
handleChange={handleChange}
errors={errors}
/>
<SettingsDemoMonitors
HEADER_SX={HEADING_SX}
handleChange={handleChange}
isLoading={isSettingsLoading || isSaving || isDeletingMonitorStats}
/>
<SettingsEmail
HEADER_SX={HEADING_SX}
handleChange={handleChange}
settingsData={settingsData}
setSettingsData={setSettingsData}
isPasswordSet={settingsData?.emailPasswordSet ?? false}
/>
<SettingsAbout />
<Stack
component="form"
gap={theme.spacing(12)}
noValidate
spellCheck="false"
direction="row"
justifyContent="flex-end"
>
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsGeneralSettings")}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
<Typography component="span">{t("settingsDisplayTimezone")}</Typography>-{" "}
{t("settingsDisplayTimezoneDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
id="display-timezones"
label={t("settingsDisplayTimezone")}
value={timezone}
onChange={(e) => {
dispatch(setTimezone({ timezone: e.target.value }));
}}
items={timezones}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsAppearance")}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
{t("settingsAppearanceDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
id="theme-mode"
label={t("settingsThemeMode")}
value={mode}
onChange={(e) => {
dispatch(setMode(e.target.value));
}}
items={[
{ _id: "light", name: t("settingsThemeModeLight") },
{ _id: "dark", name: t("settingsThemeModeDark") },
]}
></Select>
<Select
id="language"
label={t("settingsLanguage")}
value={language}
onChange={(e) => {
dispatch(setLanguage(e.target.value));
i18n.changeLanguage(e.target.value);
}}
items={languages.map((lang) => ({ _id: lang, name: lang.toUpperCase() }))}
></Select>
</Stack>
</ConfigBox>
{/* {isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDistributedUptime")}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
{t("settingsDistributedUptimeDescription")}
</Typography>
</Box>
<Box>
<Switch
id="enableDistributedUptime"
color="accent"
checked={distributedUptimeEnabled}
onChange={(e) => {
dispatch(setDistributedUptimeEnabled(e.target.checked));
}}
/>
{distributedUptimeEnabled === true
? t("settingsEnabled")
: t("settingsDisabled")}
</Box>
</ConfigBox>
)} */}
<ConfigBox>
<Box>
<Typography component="h1">{t("pageSpeedApiKeyFieldTitle")}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
{t("pageSpeedApiKeyFieldDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
id="pagespeedApiKey"
label={t("pageSpeedApiKeyFieldLabel")}
value={form.pagespeedApiKey}
type={apiKeyFieldType}
onChange={handleChange}
disabled={isApiKeySet}
optionalLabel="(Optional)"
error={!!errors.pagespeedApiKey}
helperText={errors.pagespeedApiKey}
endAdornment={
!isApiKeySet && (
<PasswordEndAdornment
fieldType={apiKeyFieldType}
setFieldType={setApiKeyFieldType}
/>
)
}
/>
{isApiKeySet && (
<Box>
<Typography>{t("pageSpeedApiKeyFieldResetLabel")}</Typography>
<Button onClick={handleResetApiKey} variant="contained" color="error" sx={{ mt: theme.spacing(4) }}>
{t("reset")}
</Button>
</Box>
)}
</Stack>
</ConfigBox>
{/* {isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsWallet")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsWalletDescription")}
</Typography>
</Box>
<Box
sx={{
display: "flex",
flexWrap: "wrap",
justifyContent: "flex-start",
gap: 2,
}}
>
<Stack
direction="row"
spacing={2}
>
<WalletMultiButton />
<WalletDisconnectButton />
</Stack>
</Box>
</ConfigBox>
)} */}
{isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsHistoryAndMonitoring")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsHistoryAndMonitoringDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
id="ttl"
label={t("settingsTTLLabel")}
optionalLabel={t("settingsTTLOptionalLabel")}
value={form.ttl}
onChange={handleChange}
error={errors.ttl ? true : false}
helperText={errors.ttl}
/>
<Box>
<Typography>{t("settingsClearAllStats")}</Typography>
<Button
variant="contained"
color="error"
onClick={() =>
setIsOpen({ ...deleteStatsMonitorsInitState, deleteStats: true })
}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsClearAllStatsButton")}
</Button>
</Box>
</Stack>
<Dialog
open={isOpen.deleteStats}
theme={theme}
title={t("settingsClearAllStatsDialogTitle")}
description={t("settingsClearAllStatsDialogDescription")}
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")}
onConfirm={handleClearStats}
isLoading={isLoading || authIsLoading || checksIsLoading}
/>
</ConfigBox>
)}
{isAdmin && (
<>
{/* Demo Monitors Section */}
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsDemoMonitorsDescription")}
</Typography>
</Box>
<Box>
<Typography>{t("settingsAddDemoMonitors")}</Typography>
<Button
variant="contained"
color="accent"
loading={isLoading || authIsLoading || checksIsLoading}
onClick={handleInsertDemoMonitors}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsAddDemoMonitorsButton")}
</Button>
</Box>
</ConfigBox>
{/* System Reset Section */}
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsSystemReset")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsSystemResetDescription")}
</Typography>
</Box>
<Box>
<Typography>{t("settingsRemoveAllMonitors")}</Typography>
<Button
variant="contained"
color="error"
loading={isLoading || authIsLoading || checksIsLoading}
onClick={() =>
setIsOpen({ ...deleteStatsMonitorsInitState, deleteMonitors: true })
}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsRemoveAllMonitorsButton")}
</Button>
</Box>
<Dialog
open={isOpen.deleteMonitors}
theme={theme}
title={t("settingsRemoveAllMonitorsDialogTitle")}
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
onConfirm={handleDeleteAllMonitors}
isLoading={isLoading || authIsLoading || checksIsLoading}
/>
</ConfigBox>
</>
)}
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsAbout")}</Typography>
</Box>
<Box>
<Typography component="h2">Checkmate {version}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
{t("settingsDevelopedBy")}
</Typography>
<Link
level="secondary"
url="https://github.com/bluewave-labs/checkmate"
label="https://github.com/bluewave-labs/checkmate"
/>
</Box>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
<Button
loading={isSaving || isDeletingMonitorStats || isSettingsLoading}
disabled={Object.keys(errors).length > 0}
variant="contained"
color="accent"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
onClick={handleSave}
>
<Button
loading={isLoading || authIsLoading || checksIsLoading}
disabled={Object.keys(errors).length > 0}
variant="contained"
color="accent"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
onClick={handleSave}
>
{t("settingsSave")}
</Button>
</Stack>
{t("settingsSave")}
</Button>
</Stack>
</Box>
</Stack>
);
};
Settings.propTypes = {
isAdmin: PropTypes.bool,
};
export default Settings;

View File

@@ -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,

View File

@@ -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);

View File

@@ -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 <SkeletonLayout shouldRender={shouldRender} />;
return (
<Stack
@@ -14,15 +16,15 @@ const StatusBoxes = ({ shouldRender, monitorsSummary }) => {
justifyContent="space-between"
>
<StatusBox
title="up"
title={t("monitorStatus.up")}
value={monitorsSummary?.upMonitors ?? 0}
/>
<StatusBox
title="down"
title={t("monitorStatus.down")}
value={monitorsSummary?.downMonitors ?? 0}
/>
<StatusBox
title="paused"
title={t("monitorStatus.paused")}
value={monitorsSummary?.pausedMonitors ?? 0}
/>
</Stack>
@@ -31,6 +33,7 @@ const StatusBoxes = ({ shouldRender, monitorsSummary }) => {
StatusBoxes.propTypes = {
monitorsSummary: PropTypes.object,
shouldRender: PropTypes.bool,
};
export default StatusBoxes;

View File

@@ -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")}
</Button>
</Box>
);
@@ -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);

View File

@@ -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",

View File

@@ -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 })}, {" "}
</Typography>
<Typography
component="span"
@@ -181,7 +183,7 @@ const Greeting = ({ type = "" }) => {
lineHeight={1}
color={theme.palette.primary.contrastTextTertiary}
>
{append} Heres an overview of your {type} monitors.
{t("greeting.append", { defaultValue: append })} {t("greeting.overview", { type: t(`menu.${type}`) })}
</Typography>
</Box>
);

View File

@@ -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,
};

View File

@@ -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 <a1>Terms of Service</a1> and <a2>Privacy Policy</a2>.",
"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 <a1>Terms of Service</a1> and <a2>Privacy Policy</a2>.",
"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>template</template> or <sample>sample</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>template</template> or <sample>sample</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"
}
}

View File

@@ -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": "Вы можете скачать наш <template>шаблон</template> или <sample>пример</sample>",
"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": "Команда"
}
}

View File

@@ -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": "<template>Şablonumuzu</template> veya <sample>örneğimizi</sample> 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"
}
}

View File

@@ -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:

View File

@@ -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;
# }
}

View File

@@ -8,6 +8,6 @@ RUN npm install
COPY ./server/ ./
EXPOSE 5000
EXPOSE 52345
CMD ["node", "index.js"]

View File

@@ -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

View File

@@ -8,6 +8,6 @@ RUN npm install
COPY ./server/ ./
EXPOSE 5000
EXPOSE 52345
CMD ["node", "index.js"]

View File

@@ -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

View File

@@ -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;

View File

@@ -10,6 +10,6 @@ RUN npm install
COPY ./server ./
EXPOSE 5000
EXPOSE 52345
CMD ["node", "index.js"]

View File

@@ -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:

View File

@@ -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;

View File

@@ -10,6 +10,6 @@ RUN npm install
COPY ./server ./
EXPOSE 5000
EXPOSE 52345
CMD ["node", "index.js"]

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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);
export default mongoose.model("AppSettings", AppSettingsSchema);

View File

@@ -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(

View File

@@ -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;

View File

@@ -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(

View File

@@ -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<string>} 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);

View File

@@ -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<Object>} The merged settings.
* @throws Will throw an error if settings are not found in the database or if settings have not been loaded.
*/ async loadSettings() {
try {
const dbSettings = await this.appSettings.findOne();
if (!this.settings) {
throw new Error("Settings not found");
}
// If there are any settings that weren't set by environment variables, use user settings from DB
for (const key in envConfig) {
if (
typeof envConfig?.[key] === "undefined" &&
typeof dbSettings?.[key] !== "undefined"
) {
this.settings[key] = dbSettings[key];
}
}
await this.appSettings.updateOne({}, { $set: this.settings }, { upsert: true });
return this.settings;
} catch (error) {
error.service === undefined ? (error.service = SERVICE_NAME) : null;
error.method === undefined ? (error.method = "loadSettings") : null;
throw error;
}
* Load settings from env settings
* @returns {Object>} The settings.
*/
loadSettings() {
return this.settings;
}
/**
* Reload settings by calling loadSettings.
* @returns {Promise<Object>} The reloaded settings.
*/
async reloadSettings() {
reloadSettings() {
return this.loadSettings();
}
/**
@@ -81,6 +57,21 @@ class SettingsService {
}
return this.settings;
}
async getDBSettings() {
let settings = await this.appSettings
.findOne({ singleton: true })
.select("-__v -_id -createdAt -updatedAt -singleton")
.lean();
if (settings === null) {
await this.appSettings.create({});
settings = await this.appSettings
.findOne({ singleton: true })
.select("-__v -_id -createdAt -updatedAt -singleton")
.lean();
}
return settings;
}
}
export default SettingsService;

View File

@@ -424,21 +424,14 @@ const editMaintenanceByIdWindowBodyValidation = joi.object({
// SettingsValidation
//****************************************
const updateAppSettingsBodyValidation = joi.object({
apiBaseUrl: joi.string().allow(""),
logLevel: joi.string().valid("debug", "none", "error", "warn").allow(""),
clientHost: joi.string().allow(""),
dbType: joi.string().allow(""),
dbConnectionString: joi.string().allow(""),
redisHost: joi.string().allow(""),
redisPort: joi.number().allow(null, ""),
redisUrl: joi.string().allow(""),
jwtTTL: joi.string().allow(""),
checkTTL: joi.number().allow(""),
pagespeedApiKey: joi.string().allow(""),
language: joi.string().allow(""),
systemEmailHost: joi.string().allow(""),
systemEmailPort: joi.number().allow(""),
systemEmailAddress: joi.string().allow(""),
systemEmailPassword: joi.string().allow(""),
systemEmailUser: joi.string().allow(""),
});
//****************************************