mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-04-30 13:45:12 -05:00
@@ -11,9 +11,6 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Distribution deploy - Monolithic Multiarch
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
docker-build-and-push-server:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push multi-arch Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/dist-arm/server.Dockerfile
|
||||
push: true
|
||||
tags: ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch
|
||||
platforms: linux/amd64,linux/arm64
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
|
||||
|
||||
# - name: Build Server Docker image
|
||||
# run: |
|
||||
# docker build \
|
||||
# -t ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch \
|
||||
# -f ./docker/dist-arm/server.Dockerfile \
|
||||
# --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
# .
|
||||
|
||||
# - name: Push Server Docker image
|
||||
# run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Distribution deploy - Monolithic
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
docker-build-and-push-server:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Server Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate:backend-dist-mono \
|
||||
-f ./docker/dist-mono/server.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push Server Docker image
|
||||
run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist-mono
|
||||
Generated
+1394
-509
File diff suppressed because it is too large
Load Diff
+8
-5
@@ -15,21 +15,21 @@
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@fontsource/roboto": "^5.0.13",
|
||||
"@hello-pangea/dnd": "^18.0.0",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
"@mui/x-data-grid": "7.29.0",
|
||||
"@mui/x-date-pickers": "7.29.0",
|
||||
"@mui/icons-material": "6.4.11",
|
||||
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
|
||||
"@mui/material": "6.4.11",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
"@mui/x-data-grid": "7.29.0",
|
||||
"@mui/x-date-pickers": "7.29.0",
|
||||
"@reduxjs/toolkit": "2.7.0",
|
||||
"axios": "^1.7.4",
|
||||
"dayjs": "1.11.13",
|
||||
"flag-icons": "7.3.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"i18next": "^24.2.2",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"joi": "17.13.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"maplibre-gl": "5.3.1",
|
||||
"mui-color-input": "^6.0.0",
|
||||
"react": "18.3.1",
|
||||
@@ -63,6 +63,9 @@
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"prettier": "^3.3.3",
|
||||
"vite": "^5.2.0"
|
||||
"vite": "^5.4.19"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-arm64-musl": "4.41.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import Dot from "../Dot";
|
||||
* @param {number} params.percentage - The percentage to display.
|
||||
* @returns {React.ElementType} Returns a div element with the host details.
|
||||
*/
|
||||
const Host = ({ url, title, percentageColor, percentage }) => {
|
||||
const Host = ({ url, title, percentageColor, percentage, showURL }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack>
|
||||
@@ -40,7 +40,7 @@ const Host = ({ url, title, percentageColor, percentage }) => {
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<span style={{ opacity: 0.6 }}>{url}</span>
|
||||
{showURL && <span style={{ opacity: 0.6 }}>{url}</span>}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -50,6 +50,7 @@ Host.propTypes = {
|
||||
percentageColor: PropTypes.string,
|
||||
percentage: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
showURL: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Host;
|
||||
|
||||
@@ -155,6 +155,7 @@ const Search = ({
|
||||
renderOption={(props, option) => {
|
||||
const { key, ...optionProps } = props;
|
||||
const hasSecondaryLabel = secondaryLabel && option[secondaryLabel] !== undefined;
|
||||
const port = option["port"];
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
@@ -169,7 +170,9 @@ const Search = ({
|
||||
}
|
||||
>
|
||||
{option[filteredBy] +
|
||||
(hasSecondaryLabel ? ` (${option[secondaryLabel]})` : "")}
|
||||
(hasSecondaryLabel
|
||||
? ` (${option[secondaryLabel]}${port ? `: ${port}` : ""})`
|
||||
: "")}
|
||||
</ListItem>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Status from "./status";
|
||||
import Skeleton from "./skeleton";
|
||||
import Button from "@mui/material/Button";
|
||||
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
|
||||
import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
|
||||
import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
|
||||
import EmailIcon from "@mui/icons-material/Email";
|
||||
|
||||
// Utils
|
||||
import PropTypes from "prop-types";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { usePauseMonitor } from "../../Hooks/useMonitorControls";
|
||||
import { useSendTestEmail } from "../../Hooks/useSendTestEmail";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* MonitorDetailsControlHeader component displays the control header for monitor details.
|
||||
* It includes status display, pause/resume button, and a configure button for admins.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.path - The base path for navigation
|
||||
* @param {boolean} [props.isLoading=false] - Flag indicating if the data is loading
|
||||
* @param {boolean} [props.isAdmin=false] - Flag indicating if the user is an admin
|
||||
* @param {Object} props.monitor - The monitor object containing details
|
||||
* @param {Function} props.triggerUpdate - Function to trigger an update
|
||||
* @returns {JSX.Element} The rendered component
|
||||
*/
|
||||
const MonitorDetailsControlHeader = ({
|
||||
path,
|
||||
isLoading = false,
|
||||
isAdmin = false,
|
||||
monitor,
|
||||
triggerUpdate,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [pauseMonitor, isPausing, error] = usePauseMonitor({
|
||||
monitorId: monitor?._id,
|
||||
triggerUpdate,
|
||||
});
|
||||
|
||||
const [isSending, emailError, sendTestEmail] = useSendTestEmail();
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Status monitor={monitor} />
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
loading={isSending}
|
||||
startIcon={<EmailIcon />}
|
||||
onClick={() => {
|
||||
sendTestEmail();
|
||||
}}
|
||||
>
|
||||
{t("sendTestEmail")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={(e) => {
|
||||
navigate(`/incidents/${monitor?._id}`);
|
||||
}}
|
||||
>
|
||||
{t("menu.incidents")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
loading={isPausing}
|
||||
startIcon={
|
||||
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
|
||||
}
|
||||
onClick={() => {
|
||||
pauseMonitor();
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive ? "Pause" : "Resume"}
|
||||
</Button>
|
||||
|
||||
{isAdmin && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
startIcon={<SettingsOutlinedIcon />}
|
||||
onClick={() => navigate(`/${path}/configure/${monitor._id}`)}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorDetailsControlHeader.propTypes = {
|
||||
path: PropTypes.string,
|
||||
isLoading: PropTypes.bool,
|
||||
isAdmin: PropTypes.bool,
|
||||
monitor: PropTypes.object,
|
||||
triggerUpdate: PropTypes.func,
|
||||
};
|
||||
|
||||
export default MonitorDetailsControlHeader;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
|
||||
const SkeletonLayout = () => {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Skeleton
|
||||
height={40}
|
||||
variant="rounded"
|
||||
width="15%"
|
||||
/>
|
||||
<Skeleton
|
||||
height={40}
|
||||
variant="rounded"
|
||||
width="15%"
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -0,0 +1,56 @@
|
||||
// Components
|
||||
import Stack from "@mui/material/Stack";
|
||||
import PulseDot from "../../Components/Animated/PulseDot";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Dot from "../../Components/Dot";
|
||||
// Utils
|
||||
import { formatDurationRounded } from "../../Utils/timeUtils";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
|
||||
|
||||
/**
|
||||
* Status component displays the status information of a monitor.
|
||||
* It includes the monitor's name, URL, and check interval.
|
||||
*
|
||||
* @component
|
||||
* @param {Object} props - Component props
|
||||
* @param {Object} props.monitor - The monitor object containing details
|
||||
* @param {string} props.monitor.name - The name of the monitor
|
||||
* @param {string} props.monitor.url - The URL of the monitor
|
||||
* @param {number} props.monitor.interval - The interval at which the monitor checks
|
||||
* @returns {JSX.Element} The rendered component
|
||||
*/
|
||||
const Status = ({ monitor }) => {
|
||||
const theme = useTheme();
|
||||
const { statusColor, determineState } = useUtils();
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Typography variant="h1">{monitor?.name}</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
<Typography
|
||||
variant="h2"
|
||||
style={{ fontFamily: "monospace", fontWeight: "bolder" }}
|
||||
>
|
||||
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Dot />
|
||||
<Typography>
|
||||
Checking every {formatDurationRounded(monitor?.interval)}.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Status.propTypes = {
|
||||
monitor: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Status;
|
||||
@@ -20,6 +20,7 @@ const initialState = {
|
||||
collapsed: false,
|
||||
},
|
||||
mode: initialMode,
|
||||
showURL: false,
|
||||
greeting: { index: 0, lastUpdate: null },
|
||||
timezone: "America/Toronto",
|
||||
distributedUptimeEnabled: false,
|
||||
@@ -46,6 +47,9 @@ const uiSlice = createSlice({
|
||||
setMode: (state, action) => {
|
||||
state.mode = action.payload;
|
||||
},
|
||||
setShowURL: (state, action) => {
|
||||
state.showURL = action.payload;
|
||||
},
|
||||
setGreeting(state, action) {
|
||||
state.greeting.index = action.payload.index;
|
||||
state.greeting.lastUpdate = action.payload.lastUpdate;
|
||||
@@ -67,6 +71,7 @@ export const {
|
||||
setRowsPerPage,
|
||||
toggleSidebar,
|
||||
setMode,
|
||||
setShowURL,
|
||||
setGreeting,
|
||||
setTimezone,
|
||||
setDistributedUptimeEnabled,
|
||||
|
||||
@@ -3,13 +3,12 @@ import { networkService } from "../main";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createToast } from "../Utils/toastUtils";
|
||||
|
||||
export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange }) => {
|
||||
export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange, trigger }) => {
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [monitor, setMonitor] = useState(undefined);
|
||||
const [monitorStats, setMonitorStats] = useState(undefined);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
try {
|
||||
@@ -29,7 +28,7 @@ export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange }) => {
|
||||
}
|
||||
};
|
||||
fetchMonitors();
|
||||
}, [dateRange, monitorId, navigate]);
|
||||
}, [dateRange, monitorId, navigate, trigger]);
|
||||
return [monitor, monitorStats, isLoading, networkError];
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useState } from "react";
|
||||
import { networkService } from "../main";
|
||||
import { createToast } from "../Utils/toastUtils";
|
||||
|
||||
const usePauseMonitor = ({ monitorId, triggerUpdate }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(undefined);
|
||||
const pauseMonitor = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.pauseMonitorById({ monitorId });
|
||||
createToast({
|
||||
body: res.data.data.isActive
|
||||
? "Monitor resumed successfully"
|
||||
: "Monitor paused successfully",
|
||||
});
|
||||
triggerUpdate();
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [pauseMonitor, isLoading, error];
|
||||
};
|
||||
|
||||
export { usePauseMonitor };
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useCallback } from "react";
|
||||
import { useTheme } from "@mui/material";
|
||||
|
||||
const useMonitorUtils = () => {
|
||||
|
||||
const getMonitorWithPercentage = useCallback((monitor, theme) => {
|
||||
let uptimePercentage = "";
|
||||
let percentageColor = "";
|
||||
@@ -36,7 +38,16 @@ const useMonitorUtils = () => {
|
||||
return monitor?.status == true ? "up" : "down";
|
||||
}, []);
|
||||
|
||||
return { getMonitorWithPercentage, determineState };
|
||||
const theme = useTheme();
|
||||
|
||||
const statusColor = {
|
||||
up: theme.palette.success.lowContrast,
|
||||
down: theme.palette.error.lowContrast,
|
||||
paused: theme.palette.warning.lowContrast,
|
||||
pending: theme.palette.warning.lowContrast,
|
||||
};
|
||||
|
||||
return { getMonitorWithPercentage, determineState, statusColor };
|
||||
};
|
||||
|
||||
export { useMonitorUtils };
|
||||
|
||||
@@ -2,27 +2,48 @@ import { useState } from "react";
|
||||
import { networkService } from "../main";
|
||||
import { useSelector } from "react-redux";
|
||||
import { createToast } from "../Utils/toastUtils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const useSendTestEmail = () => {
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const user = useSelector((state) => state.auth.user);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sendTestEmail = async () => {
|
||||
/**
|
||||
* Send a test email with optional email configuration
|
||||
* @param {Object} emailConfig - Optional email configuration parameters
|
||||
*/
|
||||
const sendTestEmail = async (emailConfig = null) => {
|
||||
try {
|
||||
setIsSending(true);
|
||||
setError(null);
|
||||
|
||||
const response = await networkService.sendTestEmail({ to: user.email });
|
||||
// Send the test email with or without configuration
|
||||
const response = await networkService.sendTestEmail({
|
||||
to: user.email,
|
||||
emailConfig
|
||||
});
|
||||
|
||||
if (typeof response?.data?.data?.messageId !== "undefined") {
|
||||
createToast({
|
||||
body: "Test email sent successfully",
|
||||
body: t("settingsTestEmailSuccess", "Test email sent successfully"),
|
||||
});
|
||||
} else {
|
||||
throw new Error("Failed to send test email");
|
||||
throw new Error(response?.data?.error || t("settingsTestEmailFailed", "Failed to send test email"));
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
createToast({
|
||||
body: t("failedToSendEmail"),
|
||||
});
|
||||
setError(error);
|
||||
createToast({
|
||||
body: t("settingsTestEmailFailedWithReason", "Failed to send test email: {{reason}}", {
|
||||
reason: error.message || t("settingsTestEmailUnknownError", "Unknown error")
|
||||
}),
|
||||
variant: "error"
|
||||
});
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">{t("settingsGeneralSettings")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("settingsGeneralSettings")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("infrastructureCreateGeneralSettingsDescription")}
|
||||
</Typography>
|
||||
@@ -368,7 +368,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("distributedUptimeCreateIncidentDescription")}
|
||||
</Typography>
|
||||
@@ -385,7 +385,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("infrastructureCustomizeAlerts")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("infrastructureCustomizeAlerts")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("infrastructureAlertNotificationDescription")}
|
||||
</Typography>
|
||||
@@ -432,7 +432,7 @@ const CreateInfrastructureMonitor = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Select
|
||||
|
||||
@@ -98,9 +98,6 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
|
||||
const mem = (monitor?.checks[0]?.memory.usage_percent ?? 0) * 100;
|
||||
const disk = (monitor?.checks[0]?.disk[0]?.usage_percent ?? 0) * 100;
|
||||
const status = determineState(monitor);
|
||||
const uptimePercentage = ((monitor?.uptimePercentage ?? 0) * 100)
|
||||
.toFixed(2)
|
||||
.toString();
|
||||
const percentageColor =
|
||||
monitor.uptimePercentage < 0.25
|
||||
? theme.palette.error.main
|
||||
@@ -117,7 +114,6 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
|
||||
mem,
|
||||
disk,
|
||||
status,
|
||||
uptimePercentage,
|
||||
percentageColor,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -137,7 +137,7 @@ const CreateMaintenance = () => {
|
||||
const response = await networkService.getMonitorsByTeamId({
|
||||
teamId: user.teamId,
|
||||
limit: null,
|
||||
types: ["http", "ping", "pagespeed"],
|
||||
types: ["http", "ping", "pagespeed", "port"],
|
||||
});
|
||||
const monitors = response.data.data.monitors;
|
||||
setMonitors(monitors);
|
||||
|
||||
@@ -310,7 +310,7 @@ const PageSpeedConfigure = () => {
|
||||
</Stack>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("settingsGeneralSettings")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("settingsGeneralSettings")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("pageSpeedConfigureSettingsDescription")}
|
||||
</Typography>
|
||||
@@ -349,7 +349,7 @@ const PageSpeedConfigure = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("distributedUptimeCreateIncidentDescription")}
|
||||
</Typography>
|
||||
@@ -405,7 +405,7 @@ const PageSpeedConfigure = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Select
|
||||
|
||||
@@ -216,7 +216,7 @@ const CreatePageSpeed = () => {
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("settingsGeneralSettings")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("settingsGeneralSettings")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("distributedUptimeCreateSelectURL")}
|
||||
</Typography>
|
||||
@@ -251,7 +251,7 @@ const CreatePageSpeed = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("distributedUptimeCreateChecks")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("distributedUptimeCreateChecks")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("distributedUptimeCreateChecksDescription")}
|
||||
</Typography>
|
||||
@@ -300,7 +300,7 @@ const CreatePageSpeed = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("distributedUptimeCreateIncidentDescription")}
|
||||
</Typography>
|
||||
@@ -320,7 +320,7 @@ const CreatePageSpeed = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Select
|
||||
|
||||
@@ -12,7 +12,7 @@ const SettingsAbout = () => {
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsAbout")}</Typography>
|
||||
<Typography component="h1" variant="h2">{t("settingsAbout")}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">Checkmate {2.1}</Typography>
|
||||
|
||||
@@ -23,7 +23,7 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
|
||||
<>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
|
||||
<Typography component="h1" variant="h2">{t("settingsDemoMonitors")}</Typography>
|
||||
<Typography sx={HEADER_SX}>{t("settingsDemoMonitorsDescription")}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
@@ -48,7 +48,7 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsSystemReset")}</Typography>
|
||||
<Typography component="h1" variant="h2">{t("settingsSystemReset")}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
{t("settingsSystemResetDescription")}
|
||||
</Typography>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
|
||||
import { useSendTestEmail } from "../../Hooks/useSendTestEmail";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
|
||||
const SettingsEmail = ({
|
||||
isAdmin,
|
||||
@@ -29,7 +30,7 @@ const SettingsEmail = ({
|
||||
const [hasBeenReset, setHasBeenReset] = useState(false);
|
||||
|
||||
// Network
|
||||
const [isSending, error, sendTestEmail] = useSendTestEmail();
|
||||
const [isSending, , sendTestEmail] = useSendTestEmail(); // Using empty placeholder for unused error variable
|
||||
|
||||
// Handlers
|
||||
const handlePasswordChange = (e) => {
|
||||
@@ -40,6 +41,33 @@ const SettingsEmail = ({
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle sending test email with current form values
|
||||
*/
|
||||
const handleSendTestEmail = () => {
|
||||
// Collect current form values
|
||||
const emailConfig = {
|
||||
systemEmailHost: settingsData?.settings?.systemEmailHost,
|
||||
systemEmailPort: settingsData?.settings?.systemEmailPort,
|
||||
systemEmailUser: settingsData?.settings?.systemEmailUser,
|
||||
systemEmailAddress: settingsData?.settings?.systemEmailAddress,
|
||||
systemEmailPassword: password || settingsData?.settings?.systemEmailPassword,
|
||||
systemEmailConnectionHost: settingsData?.settings?.systemEmailConnectionHost
|
||||
};
|
||||
|
||||
// Basic validation
|
||||
if (!emailConfig.systemEmailHost || !emailConfig.systemEmailPort) {
|
||||
createToast({
|
||||
body: t("settingsEmailRequiredFields", "Email host and port are required"),
|
||||
variant: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Send test email with current form values
|
||||
sendTestEmail(emailConfig);
|
||||
};
|
||||
|
||||
if (!isAdmin) {
|
||||
return null;
|
||||
}
|
||||
@@ -47,7 +75,7 @@ const SettingsEmail = ({
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsEmail")}</Typography>
|
||||
<Typography component="h1" variant="h2">{t("settingsEmail")}</Typography>
|
||||
<Typography sx={HEADER_SX}>{t("settingsEmailDescription")}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
@@ -122,14 +150,23 @@ const SettingsEmail = ({
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<Typography>{t("settingsEmailConnectionHost")}</Typography>
|
||||
<TextInput
|
||||
name="systemEmailConnectionHost"
|
||||
placeholder="bluewavelabs.ca"
|
||||
value={settingsData?.settings?.systemEmailConnectionHost ?? ""}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
loading={isSending}
|
||||
onClick={sendTestEmail}
|
||||
onClick={handleSendTestEmail}
|
||||
>
|
||||
Send test e-mail
|
||||
{t("settingsTestEmail", "Send test e-mail")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -41,7 +41,7 @@ const SettingsPagespeed = ({
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("pageSpeedApiKeyFieldTitle")}</Typography>
|
||||
<Typography component="h1" variant="h2">{t("pageSpeedApiKeyFieldTitle")}</Typography>
|
||||
<Typography sx={HEADING_SX}>{t("pageSpeedApiKeyFieldDescription")}</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
|
||||
@@ -26,6 +26,7 @@ const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors
|
||||
<Box>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h2"
|
||||
sx={HEADING_SX}
|
||||
>
|
||||
{t("settingsHistoryAndMonitoring")}
|
||||
|
||||
@@ -15,7 +15,7 @@ const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsGeneralSettings")}</Typography>
|
||||
<Typography component="h1" variant="h2">{t("settingsGeneralSettings")}</Typography>
|
||||
<Typography sx={HEADING_SX}>
|
||||
<Typography component="span">{t("settingsDisplayTimezone")}</Typography>-{" "}
|
||||
{t("settingsDisplayTimezoneDescription")}
|
||||
|
||||
@@ -16,7 +16,7 @@ const SettingsUI = ({ HEADING_SX, handleChange, mode, language }) => {
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsAppearance")}</Typography>
|
||||
<Typography component="h1" variant="h2">{t("settingsAppearance")}</Typography>
|
||||
<Typography sx={HEADING_SX}>{t("settingsAppearanceDescription")}</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
|
||||
@@ -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";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const SettingsURL = ({ HEADING_SX, handleChange, showURL }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsURLTitle")}</Typography>
|
||||
<Typography sx={HEADING_SX}>{t("settingsURLDescription")}</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Select
|
||||
name="showURL"
|
||||
label={t("settingsURLSelectTitle")}
|
||||
value={showURL}
|
||||
onChange={handleChange}
|
||||
items={[
|
||||
{ _id: true, name: t("settingsURLEnabled") },
|
||||
{ _id: false, name: t("settingURLDisabled") },
|
||||
]}
|
||||
></Select>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
);
|
||||
};
|
||||
|
||||
SettingsURL.propTypes = {
|
||||
HEADING_SX: PropTypes.object,
|
||||
handleChange: PropTypes.func,
|
||||
showURL: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default SettingsURL;
|
||||
@@ -3,6 +3,7 @@ import Typography from "@mui/material/Typography";
|
||||
import Breadcrumbs from "../../Components/Breadcrumbs";
|
||||
import SettingsTimeZone from "./SettingsTimeZone";
|
||||
import SettingsUI from "./SettingsUI";
|
||||
import SettingsURL from "./SettingsURL";
|
||||
import SettingsPagespeed from "./SettingsPagespeed";
|
||||
import SettingsDemoMonitors from "./SettingsDemoMonitors";
|
||||
import SettingsAbout from "./SettingsAbout";
|
||||
@@ -15,7 +16,7 @@ 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 { setTimezone, setMode, setLanguage, setShowURL } from "../../Features/UI/uiSlice";
|
||||
import SettingsStats from "./SettingsStats";
|
||||
import {
|
||||
deleteMonitorChecksByTeamId,
|
||||
@@ -31,7 +32,7 @@ const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }];
|
||||
|
||||
const Settings = () => {
|
||||
// Redux state
|
||||
const { mode, language, timezone } = useSelector((state) => state.ui);
|
||||
const { mode, language, timezone, showURL } = useSelector((state) => state.ui);
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
|
||||
// Local state
|
||||
@@ -57,6 +58,11 @@ const Settings = () => {
|
||||
const handleChange = async (e) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
// Special case for showURL until handled properly in the backend
|
||||
if (name === "showURL") {
|
||||
dispatch(setShowURL(value));
|
||||
return;
|
||||
}
|
||||
// Build next state early
|
||||
const newSettingsData = {
|
||||
...settingsData,
|
||||
@@ -127,6 +133,7 @@ const Settings = () => {
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
console.log(settingsData.settings);
|
||||
const { error } = settingsValidation.validate(settingsData.settings, {
|
||||
abortEarly: false,
|
||||
});
|
||||
@@ -164,6 +171,11 @@ const Settings = () => {
|
||||
setSettingsData={setSettingsData}
|
||||
isApiKeySet={settingsData?.pagespeedKeySet ?? false}
|
||||
/>
|
||||
<SettingsURL
|
||||
HEADING_SX={HEADING_SX}
|
||||
handleChange={handleChange}
|
||||
showURL={showURL}
|
||||
/>
|
||||
<SettingsStats
|
||||
isAdmin={isAdmin}
|
||||
HEADING_SX={HEADING_SX}
|
||||
|
||||
@@ -9,7 +9,7 @@ const ConfigStack = ({ title, description, children }) => {
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">{title}</Typography>
|
||||
<Typography component="h2" variant="h2">{title}</Typography>
|
||||
<Typography component="p">{description}</Typography>
|
||||
</Stack>
|
||||
{children}
|
||||
|
||||
@@ -34,7 +34,7 @@ const TabSettings = ({
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<ConfigBox>
|
||||
<Stack>
|
||||
<Typography component="h2">{t("access")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("access")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("statusPageCreateSettings")}
|
||||
</Typography>
|
||||
@@ -51,7 +51,7 @@ const TabSettings = ({
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">{t("basicInformation")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("basicInformation")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("statusPageCreateBasicInfoDescription")}
|
||||
</Typography>
|
||||
@@ -82,7 +82,7 @@ const TabSettings = ({
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">{t("timezone")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("timezone")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("statusPageCreateSelectTimeZoneDescription")}
|
||||
</Typography>
|
||||
@@ -100,7 +100,7 @@ const TabSettings = ({
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">{t("settingsAppearance")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("settingsAppearance")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("statusPageCreateAppearanceDescription")}
|
||||
</Typography>
|
||||
|
||||
@@ -15,7 +15,7 @@ const useMonitorsFetch = () => {
|
||||
const response = await networkService.getMonitorsByTeamId({
|
||||
teamId: user.teamId,
|
||||
limit: null, // donot return any checks for the monitors
|
||||
types: ["http"], // status page is available only for the uptime type
|
||||
types: ["http", "ping"], // status page is available for uptime and ping monitors
|
||||
});
|
||||
setMonitors(response.data.data.monitors);
|
||||
} catch (error) {
|
||||
|
||||
@@ -184,7 +184,6 @@ const CreateStatusPage = () => {
|
||||
size: null,
|
||||
};
|
||||
}
|
||||
|
||||
setForm((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
@@ -194,6 +193,8 @@ const CreateStatusPage = () => {
|
||||
monitors: statusPageMonitors.map((monitor) => monitor._id),
|
||||
color: statusPage?.color,
|
||||
logo: newLogo,
|
||||
showCharts: statusPage?.showCharts ?? true,
|
||||
showUptimePercentage: statusPage?.showUptimePercentage ?? true
|
||||
};
|
||||
});
|
||||
setSelectedMonitors(statusPageMonitors);
|
||||
|
||||
@@ -8,9 +8,15 @@ import { StatusLabel } from "../../../../../Components/Label";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const MonitorsList = ({ isLoading = false, shouldRender = true, monitors = [] }) => {
|
||||
const theme = useTheme();
|
||||
const { determineState } = useUtils();
|
||||
|
||||
const { showURL } = useSelector((state) => state.ui);
|
||||
|
||||
return (
|
||||
<>
|
||||
{monitors?.map((monitor) => {
|
||||
@@ -27,6 +33,7 @@ const MonitorsList = ({ isLoading = false, shouldRender = true, monitors = [] })
|
||||
title={monitor.name}
|
||||
percentageColor={monitor.percentageColor}
|
||||
percentage={monitor.percentage}
|
||||
showURL={showURL}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const PublicStatus = () => {
|
||||
const { url } = useParams();
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ const BulkImport = () => {
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("bulkImport.selectFileTips")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("bulkImport.selectFileTips")}</Typography>
|
||||
<Typography component="p">
|
||||
<Trans
|
||||
i18nKey="bulkImport.selectFileDescription"
|
||||
|
||||
@@ -34,6 +34,10 @@ import SkeletonLayout from "./skeleton";
|
||||
import "./index.css";
|
||||
import Dialog from "../../../Components/Dialog";
|
||||
import NotificationIntegrationModal from "../../../Components/NotificationIntegrationModal/Components/NotificationIntegrationModal";
|
||||
import { usePauseMonitor } from "../../../Hooks/useMonitorControls";
|
||||
import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
|
||||
import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
|
||||
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils";
|
||||
|
||||
/**
|
||||
* Parses a URL string and returns a URL object.
|
||||
@@ -83,11 +87,19 @@ const Configure = () => {
|
||||
include: "ok",
|
||||
};
|
||||
|
||||
const [trigger, setTrigger] = useState(false);
|
||||
const triggerUpdate = () => {
|
||||
setTrigger(!trigger);
|
||||
};
|
||||
const [pauseMonitor, isPausing, error] = usePauseMonitor({
|
||||
monitorId: monitor?._id,
|
||||
triggerUpdate,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
const action = await dispatch(getUptimeMonitorById({ monitorId }));
|
||||
|
||||
if (getUptimeMonitorById.fulfilled.match(action)) {
|
||||
const monitor = action.payload.data;
|
||||
setMonitor(monitor);
|
||||
@@ -100,7 +112,7 @@ const Configure = () => {
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [monitorId, navigate]);
|
||||
}, [monitorId, navigate, trigger]);
|
||||
|
||||
const handleChange = (event, name) => {
|
||||
let { checked, value, id } = event.target;
|
||||
@@ -158,23 +170,6 @@ const Configure = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
try {
|
||||
const action = await dispatch(pauseUptimeMonitor({ monitorId }));
|
||||
if (pauseUptimeMonitor.fulfilled.match(action)) {
|
||||
const monitor = action.payload.data;
|
||||
setMonitor(monitor);
|
||||
const state = action?.payload?.data.isActive === false ? "paused" : "resumed";
|
||||
createToast({ body: `Monitor ${state} successfully.` });
|
||||
} else if (pauseUptimeMonitor.rejected.match(action)) {
|
||||
throw new Error(action.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error pausing monitor: " + monitorId);
|
||||
createToast({ body: "Failed to pause monitor" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
const action = await dispatch(updateUptimeMonitor({ monitor: monitor }));
|
||||
@@ -219,17 +214,7 @@ const Configure = () => {
|
||||
setIsNotificationModalOpen(false);
|
||||
};
|
||||
|
||||
const statusColor = {
|
||||
true: theme.palette.success.main,
|
||||
false: theme.palette.error.main,
|
||||
undefined: theme.palette.warning.main,
|
||||
};
|
||||
|
||||
const statusMsg = {
|
||||
true: "Your site is up.",
|
||||
false: "Your site is down.",
|
||||
undefined: "Pending...",
|
||||
};
|
||||
const { determineState, statusColor } = useMonitorUtils();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -274,7 +259,7 @@ const Configure = () => {
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Tooltip
|
||||
title={statusMsg[monitor?.status ?? undefined]}
|
||||
title={t(`statusMsg.${[determineState(monitor)]}`)}
|
||||
disableInteractive
|
||||
slotProps={{
|
||||
popper: {
|
||||
@@ -290,7 +275,7 @@ const Configure = () => {
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<PulseDot color={statusColor[monitor?.status ?? undefined]} />
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Typography
|
||||
@@ -324,42 +309,26 @@ const Configure = () => {
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box
|
||||
justifyContent="space-between"
|
||||
sx={{
|
||||
alignSelf: "flex-end",
|
||||
ml: "auto",
|
||||
display: "flex",
|
||||
gap: theme.spacing(2),
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
loading={isLoading}
|
||||
sx={{
|
||||
pl: theme.spacing(4),
|
||||
pr: theme.spacing(6),
|
||||
mr: theme.spacing(6),
|
||||
"& svg": {
|
||||
mr: theme.spacing(2),
|
||||
width: 22,
|
||||
height: 22,
|
||||
"& path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
strokeWidth: 1.7,
|
||||
},
|
||||
},
|
||||
loading={isPausing}
|
||||
startIcon={
|
||||
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
|
||||
}
|
||||
onClick={() => {
|
||||
pauseMonitor();
|
||||
}}
|
||||
onClick={handlePause}
|
||||
>
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseIcon />
|
||||
{t("pause")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ResumeIcon />
|
||||
{t("resume")}
|
||||
</>
|
||||
)}
|
||||
{monitor?.isActive ? t("pause") : t("resume")}
|
||||
</Button>
|
||||
<Button
|
||||
loading={isLoading}
|
||||
@@ -374,7 +343,7 @@ const Configure = () => {
|
||||
</Stack>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("settingsGeneralSettings")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("settingsGeneralSettings")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("distributedUptimeCreateSelectURL")}
|
||||
</Typography>
|
||||
@@ -420,7 +389,7 @@ const Configure = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">
|
||||
<Typography component="h2" variant="h2">
|
||||
{t("distributedUptimeCreateIncidentNotification")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
@@ -488,7 +457,7 @@ const Configure = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("ignoreTLSError")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("ignoreTLSError")}</Typography>
|
||||
<Typography component="p">{t("ignoreTLSErrorDescription")}</Typography>
|
||||
</Box>
|
||||
<Stack>
|
||||
@@ -508,7 +477,7 @@ const Configure = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">
|
||||
<Typography component="h2" variant="h2">
|
||||
{t("distributedUptimeCreateAdvancedSettings")}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -261,7 +261,7 @@ const CreateMonitor = () => {
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("distributedUptimeCreateChecks")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("distributedUptimeCreateChecks")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("distributedUptimeCreateChecksDescription")}
|
||||
</Typography>
|
||||
@@ -342,7 +342,7 @@ const CreateMonitor = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("settingsGeneralSettings")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("settingsGeneralSettings")}</Typography>
|
||||
<Typography component="p">{t("uptimeCreateSelectURL")}</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
@@ -386,7 +386,7 @@ const CreateMonitor = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">
|
||||
<Typography component="h2" variant="h2">
|
||||
{t("distributedUptimeCreateIncidentNotification")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
@@ -417,7 +417,7 @@ const CreateMonitor = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("ignoreTLSError")}</Typography>
|
||||
<Typography component="h2" variant="h2">{t("ignoreTLSError")}</Typography>
|
||||
<Typography component="p">{t("ignoreTLSErrorDescription")}</Typography>
|
||||
</Box>
|
||||
<Stack>
|
||||
@@ -437,7 +437,7 @@ const CreateMonitor = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">
|
||||
<Typography component="h2" variant="h2">
|
||||
{t("distributedUptimeCreateAdvancedSettings")}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -75,7 +75,7 @@ const ChartBoxes = ({
|
||||
<Typography component="span">
|
||||
{hoveredUptimeData !== null
|
||||
? Math.floor(hoveredUptimeData?.originalAvgResponseTime ?? 0)
|
||||
: Math.floor(monitorData?.groupedUptimePercentage ?? 0 * 100)}
|
||||
: Math.floor((monitorData?.groupedUptimePercentage ?? 0) * 100)}
|
||||
<Typography component="span">
|
||||
{hoveredUptimeData !== null ? " ms" : " %"}
|
||||
</Typography>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Components
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader";
|
||||
import MonitorStatusHeader from "../../../Components/MonitorStatusHeader";
|
||||
import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
|
||||
import ChartBoxes from "./Components/ChartBoxes";
|
||||
@@ -36,9 +37,9 @@ const UptimeDetails = () => {
|
||||
|
||||
// Local state
|
||||
const [dateRange, setDateRange] = useState("recent");
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [trigger, setTrigger] = useState(false);
|
||||
|
||||
// Utils
|
||||
const dateFormat =
|
||||
@@ -52,6 +53,7 @@ const UptimeDetails = () => {
|
||||
useFetchUptimeMonitorDetails({
|
||||
monitorId,
|
||||
dateRange,
|
||||
trigger,
|
||||
});
|
||||
|
||||
const monitor = monitorData?.monitor;
|
||||
@@ -73,6 +75,10 @@ const UptimeDetails = () => {
|
||||
});
|
||||
|
||||
// Handlers
|
||||
const triggerUpdate = () => {
|
||||
setTrigger(!trigger);
|
||||
};
|
||||
|
||||
const handlePageChange = (_, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
@@ -101,11 +107,12 @@ const UptimeDetails = () => {
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorStatusHeader
|
||||
<MonitorDetailsControlHeader
|
||||
path={"uptime"}
|
||||
isAdmin={isAdmin}
|
||||
shouldRender={!monitorIsLoading}
|
||||
isLoading={monitorIsLoading}
|
||||
monitor={monitor}
|
||||
triggerUpdate={triggerUpdate}
|
||||
/>
|
||||
<GenericFallback>
|
||||
<Typography>{t("distributedUptimeDetailsNoMonitorHistory")}</Typography>
|
||||
@@ -117,11 +124,12 @@ const UptimeDetails = () => {
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorStatusHeader
|
||||
<MonitorDetailsControlHeader
|
||||
path={"uptime"}
|
||||
isAdmin={isAdmin}
|
||||
isLoading={monitorIsLoading}
|
||||
monitor={monitor}
|
||||
triggerUpdate={triggerUpdate}
|
||||
/>
|
||||
<UptimeStatusBoxes
|
||||
isLoading={monitorIsLoading}
|
||||
|
||||
@@ -1047,9 +1047,12 @@ class NetworkService {
|
||||
form.url && fd.append("url", form.url);
|
||||
form.timezone && fd.append("timezone", form.timezone);
|
||||
form.color && fd.append("color", form.color);
|
||||
form.showCharts && fd.append("showCharts", form.showCharts);
|
||||
form.showUptimePercentage &&
|
||||
fd.append("showUptimePercentage", form.showUptimePercentage);
|
||||
if (form.showCharts !== undefined) {
|
||||
fd.append("showCharts", String(form.showCharts));
|
||||
}
|
||||
if (form.showUptimePercentage !== undefined) {
|
||||
fd.append("showUptimePercentage", String(form.showUptimePercentage));
|
||||
}
|
||||
form.monitors &&
|
||||
form.monitors.forEach((monitorId) => {
|
||||
fd.append("monitors[]", monitorId);
|
||||
@@ -1154,11 +1157,28 @@ class NetworkService {
|
||||
}
|
||||
|
||||
// ************************************
|
||||
// Send tes t email
|
||||
// Send test email
|
||||
// ************************************
|
||||
async sendTestEmail(config) {
|
||||
const { to } = config;
|
||||
return this.axiosInstance.post(`/monitors/test-email`, { to });
|
||||
// Extract recipient and email configuration
|
||||
const { to, emailConfig } = config;
|
||||
|
||||
// If emailConfig is provided, use the new endpoint with direct parameters
|
||||
if (emailConfig) {
|
||||
return this.axiosInstance.post(`/settings/test-email`, {
|
||||
to,
|
||||
systemEmailHost: emailConfig.systemEmailHost,
|
||||
systemEmailPort: emailConfig.systemEmailPort,
|
||||
systemEmailAddress: emailConfig.systemEmailAddress,
|
||||
systemEmailPassword: emailConfig.systemEmailPassword,
|
||||
// Only include these if they are present
|
||||
...(emailConfig.systemEmailConnectionHost && { systemEmailConnectionHost: emailConfig.systemEmailConnectionHost }),
|
||||
...(emailConfig.systemEmailUser && { systemEmailUser: emailConfig.systemEmailUser })
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback to original behavior for backward compatibility
|
||||
return this.axiosInstance.post(`/settings/test-email`, { to });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@ const lastnameSchema = joi
|
||||
.messages({
|
||||
"string.empty": "Surname is required",
|
||||
"string.max": "Surname must be less than 50 characters",
|
||||
"string.pattern.base": "Surname must contain only letters, spaces, apostrophes, or hyphens"
|
||||
"string.pattern.base":
|
||||
"Surname must contain only letters, spaces, apostrophes, or hyphens",
|
||||
});
|
||||
|
||||
const passwordSchema = joi
|
||||
@@ -103,67 +104,69 @@ const credentials = joi.object({
|
||||
});
|
||||
|
||||
const monitorValidation = joi.object({
|
||||
url: joi
|
||||
.when("type", {
|
||||
is: "docker",
|
||||
then: joi
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^[a-z0-9]{64}$/),
|
||||
otherwise: joi
|
||||
.string()
|
||||
.trim()
|
||||
.custom((value, helpers) => {
|
||||
// Regex from https://gist.github.com/dperini/729294
|
||||
var urlRegex = new RegExp(
|
||||
"^" +
|
||||
// protocol identifier (optional)
|
||||
// short syntax // still required
|
||||
"(?:(?:https?|ftp):\\/\\/)?" +
|
||||
// user:pass BasicAuth (optional)
|
||||
"(?:" +
|
||||
// IP address dotted notation octets
|
||||
// excludes loopback network 0.0.0.0
|
||||
// excludes reserved space >= 224.0.0.0
|
||||
// excludes network & broadcast addresses
|
||||
// (first & last IP address of each class)
|
||||
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
|
||||
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
|
||||
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
|
||||
"|" +
|
||||
// host & domain names, may end with dot
|
||||
// can be replaced by a shortest alternative
|
||||
// (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+
|
||||
"(?:" +
|
||||
"(?:" +
|
||||
"[a-z0-9\\u00a1-\\uffff]" +
|
||||
"[a-z0-9\\u00a1-\\uffff_-]{0,62}" +
|
||||
")?" +
|
||||
"[a-z0-9\\u00a1-\\uffff]\\." +
|
||||
")+" +
|
||||
// TLD identifier name, may end with dot
|
||||
"(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" +
|
||||
")" +
|
||||
// port number (optional)
|
||||
"(?::\\d{2,5})?" +
|
||||
// resource path (optional)
|
||||
"(?:[/?#]\\S*)?" +
|
||||
"$",
|
||||
"i"
|
||||
);
|
||||
if (!urlRegex.test(value)) {
|
||||
return helpers.error("string.invalidUrl");
|
||||
}
|
||||
url: joi.when("type", {
|
||||
is: "docker",
|
||||
then: joi
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^[a-z0-9]{64}$/)
|
||||
.messages({
|
||||
"string.empty": "This field is required.",
|
||||
"string.pattern.base": "Please enter a valid 64-character Docker container ID.",
|
||||
}),
|
||||
otherwise: joi
|
||||
.string()
|
||||
.trim()
|
||||
.custom((value, helpers) => {
|
||||
// Regex from https://gist.github.com/dperini/729294
|
||||
var urlRegex = new RegExp(
|
||||
"^" +
|
||||
// protocol identifier (optional)
|
||||
// short syntax // still required
|
||||
"(?:(?:https?|ftp):\\/\\/)?" +
|
||||
// user:pass BasicAuth (optional)
|
||||
"(?:" +
|
||||
// IP address dotted notation octets
|
||||
// excludes loopback network 0.0.0.0
|
||||
// excludes reserved space >= 224.0.0.0
|
||||
// excludes network & broadcast addresses
|
||||
// (first & last IP address of each class)
|
||||
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
|
||||
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
|
||||
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
|
||||
"|" +
|
||||
// host & domain names, may end with dot
|
||||
// can be replaced by a shortest alternative
|
||||
// (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+
|
||||
"(?:" +
|
||||
"(?:" +
|
||||
"[a-z0-9\\u00a1-\\uffff]" +
|
||||
"[a-z0-9\\u00a1-\\uffff_-]{0,62}" +
|
||||
")?" +
|
||||
"[a-z0-9\\u00a1-\\uffff]\\." +
|
||||
")+" +
|
||||
// TLD identifier name, may end with dot
|
||||
"(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" +
|
||||
")" +
|
||||
// port number (optional)
|
||||
"(?::\\d{2,5})?" +
|
||||
// resource path (optional)
|
||||
"(?:[/?#]\\S*)?" +
|
||||
"$",
|
||||
"i"
|
||||
);
|
||||
if (!urlRegex.test(value)) {
|
||||
return helpers.error("string.invalidUrl");
|
||||
}
|
||||
|
||||
return value;
|
||||
}),
|
||||
})
|
||||
.messages({
|
||||
"string.empty": "This field is required.",
|
||||
"string.uri": "The URL you provided is not valid.",
|
||||
"string.invalidUrl": "Please enter a valid URL with optional port",
|
||||
"string.pattern.base": "Please enter a valid container ID.",
|
||||
}),
|
||||
return value;
|
||||
})
|
||||
.messages({
|
||||
"string.empty": "This field is required.",
|
||||
"string.uri": "The URL you provided is not valid.",
|
||||
"string.invalidUrl": "Please enter a valid URL with optional port",
|
||||
}),
|
||||
}),
|
||||
port: joi
|
||||
.number()
|
||||
.integer()
|
||||
@@ -171,7 +174,7 @@ const monitorValidation = joi.object({
|
||||
.max(65535)
|
||||
.when("type", {
|
||||
is: "port",
|
||||
then: joi.required().messages({
|
||||
then: joi.number().messages({
|
||||
"number.base": "Port must be a number.",
|
||||
"number.min": "Port must be at least 1.",
|
||||
"number.max": "Port must be at most 65535.",
|
||||
@@ -265,6 +268,7 @@ const statusPageValidation = joi.object({
|
||||
showUptimePercentage: joi.boolean(),
|
||||
showCharts: joi.boolean(),
|
||||
});
|
||||
|
||||
const settingsValidation = joi.object({
|
||||
checkTTL: joi.number().required().messages({
|
||||
"string.empty": "Please enter a value",
|
||||
@@ -278,6 +282,7 @@ const settingsValidation = joi.object({
|
||||
systemEmailAddress: joi.string().allow(""),
|
||||
systemEmailPassword: joi.string().allow(""),
|
||||
systemEmailUser: joi.string().allow(""),
|
||||
systemEmailConnectionHost: joi.string().allow(""),
|
||||
});
|
||||
|
||||
const dayjsValidator = (value, helpers) => {
|
||||
@@ -307,6 +312,7 @@ const advancedSettingsValidation = joi.object({
|
||||
systemEmailPort: joi.number().allow(null, ""),
|
||||
systemEmailAddress: joi.string().allow(""),
|
||||
systemEmailPassword: joi.string().allow(""),
|
||||
systemEmailConnectionHost: joi.string().allow(""),
|
||||
jwtTTLNum: joi.number().messages({
|
||||
"number.base": "JWT TTL is required.",
|
||||
}),
|
||||
|
||||
+29
-10
@@ -75,6 +75,11 @@
|
||||
"settingsThemeModeLight": "Light",
|
||||
"settingsThemeModeDark": "Dark",
|
||||
"settingsLanguage": "Language",
|
||||
"settingsURLTitle": "Monitor IP/URL on Status Page",
|
||||
"settingsURLDescription": "Display the IP address or URL of monitor on the public Status page. If it's disabled, only the monitor name will be shown to protect sensitive information.",
|
||||
"settingsURLSelectTitle": "Display IP/URL on status page",
|
||||
"settingsURLEnabled": "Enabled",
|
||||
"settingURLDisabled": "Disabled",
|
||||
"settingsDistributedUptime": "Distributed uptime",
|
||||
"settingsDistributedUptimeDescription": "Enable/disable distributed uptime monitoring.",
|
||||
"settingsEnabled": "Enabled",
|
||||
@@ -112,13 +117,20 @@
|
||||
"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",
|
||||
"settingsEmail": "Email",
|
||||
"settingsEmailDescription": "Configure the email settings for your system. This is used to send notifications and alerts.",
|
||||
"settingsEmailHost": "Email host - Hostname or IP address of the SMTP server",
|
||||
"settingsEmailPort": "Email port - Port to connect to",
|
||||
"settingsEmailUser": "Email user - Username for authentication, overrides email address if specified",
|
||||
"settingsEmailAddress": "Email address - Used for authentication",
|
||||
"settingsEmailPassword": "Email password - Password for authentication",
|
||||
"settingsEmailConnectionHost": "Email connection host - Hostname to use in the HELO/EHLO greeting",
|
||||
"settingsTestEmail": "Send test e-mail",
|
||||
"settingsTestEmailSuccess": "Test email sent successfully",
|
||||
"settingsTestEmailFailed": "Failed to send test email",
|
||||
"settingsTestEmailFailedWithReason": "Failed to send test email: {{reason}}",
|
||||
"settingsTestEmailUnknownError": "Unknown error",
|
||||
"settingsEmailRequiredFields": "Email host and port are required",
|
||||
"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.",
|
||||
@@ -521,8 +533,6 @@
|
||||
"logOut": "Log out"
|
||||
},
|
||||
"navControls": "Controls",
|
||||
"statusBreadCrumbsStatusPages": "Status Pages",
|
||||
"statusBreadCrumbsDetails": "Details",
|
||||
"incidentsPageTitle": "Incidents",
|
||||
"passwordPanel": {
|
||||
"passwordChangedSuccess": "Your password was changed successfully.",
|
||||
@@ -535,5 +545,14 @@
|
||||
"passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.",
|
||||
"saving": "Saving..."
|
||||
},
|
||||
"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."
|
||||
"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.",
|
||||
"sendTestEmail": "Send test email",
|
||||
"emailSent": "Email sent successfully",
|
||||
"failedToSendEmail": "Failed to send email",
|
||||
"statusMsg" : {
|
||||
"paused": "Monitoring is paused.",
|
||||
"up": "Your site is up.",
|
||||
"down": "Your site is down.",
|
||||
"pending": "Pending..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,11 @@
|
||||
"settingsThemeModeLight": "Светлая",
|
||||
"settingsThemeModeDark": "Тёмная",
|
||||
"settingsLanguage": "Язык",
|
||||
"settingsURLTitle": "Отображать IP/URL на странице статуса",
|
||||
"settingsURLDescription": "Отображать IP-адрес или URL-адрес монитора на общедоступной странице статуса. Если отключено, будет отображаться только имя монитора для защиты конфиденциальной информации.",
|
||||
"settingsURLSelectTitle": "Показать IP/URL на странице статуса",
|
||||
"settingsURLEnabled": "Включено",
|
||||
"settingURLDisabled": "Отключено",
|
||||
"settingsDistributedUptime": "Distributed uptime",
|
||||
"settingsDistributedUptimeDescription": "Включить/выключить distributed uptime monitoring.",
|
||||
"settingsEnabled": "Включено",
|
||||
@@ -115,6 +120,21 @@
|
||||
"settingsRemoveAllMonitorsDialogConfirm": "Да, удалить все мониторы",
|
||||
"settingsWallet": "Кошелёк",
|
||||
"settingsWalletDescription": "Подключите свой кошелек здесь. Это необходимо для того, чтобы монитор Distributed Uptime мог подключиться к нескольким узлам по всему миру.",
|
||||
"settingsEmail": "Настройки электронной почты",
|
||||
"settingsEmailDescription": "Настройте параметры электронной почты",
|
||||
"settingsEmailHost": "Хост электронной почты - имя хоста или IP-адрес SMTP-сервера",
|
||||
"settingsEmailPort": "Порт электронной почты - порт для подключения",
|
||||
"settingsEmailAddress": "Адрес электронной почты - используется для аутентификации",
|
||||
"settingsEmailPassword": "Пароль электронной почты - пароль для аутентификации",
|
||||
"settingsEmailUser": "Пользователь электронной почты - имя пользователя для аутентификации, переопределяет адрес электронной почты, если указан",
|
||||
"settingsEmailFieldResetLabel": "Пароль установлен. Нажмите «Сбросить», чтобы изменить его.",
|
||||
"settingsEmailConnectionHost": "Хост подключения электронной почты - имя хоста, используемое в приветствии HELO/EHLO",
|
||||
"settingsTestEmail": "Отправить тестовое письмо",
|
||||
"settingsTestEmailSuccess": "Тестовое письмо успешно отправлено",
|
||||
"settingsTestEmailFailed": "Не удалось отправить тестовое письмо",
|
||||
"settingsTestEmailFailedWithReason": "Не удалось отправить тестовое письмо: {{reason}}",
|
||||
"settingsTestEmailUnknownError": "Неизвестная ошибка",
|
||||
"settingsEmailRequiredFields": "Требуются хост и порт электронной почты",
|
||||
"settingsAbout": "О",
|
||||
"settingsDevelopedBy": "Developed by Bluewave Labs.",
|
||||
"settingsSave": "Сохранить",
|
||||
@@ -152,7 +172,6 @@
|
||||
"http": "HTTP",
|
||||
"monitor": "монитор",
|
||||
"aboutus": "О Нас",
|
||||
|
||||
"now": "Сейчас",
|
||||
"delete": "Удалить",
|
||||
"configure": "Настроить",
|
||||
@@ -409,6 +428,10 @@
|
||||
"pageSpeedWarning": "Предупреждение: Вы не добавили ключ API Google PageSpeed. Без него монитор PageSpeed не будет работать.",
|
||||
"pageSpeedLearnMoreLink": "Нажмите здесь, чтобы узнать",
|
||||
"pageSpeedAddApiKey": "как добавить ваш ключ API.",
|
||||
"pageSpeedApiKeyFieldTitle": "Ключ Google PageSpeed API",
|
||||
"pageSpeedApiKeyFieldLabel": "Ключ PageSpeed API",
|
||||
"pageSpeedApiKeyFieldDescription": "Введите свой ключ Google PageSpeed API, чтобы включить мониторинг скорости страниц. Нажмите «Сбросить», чтобы обновить ключ.",
|
||||
"pageSpeedApiKeyFieldResetLabel": "Ключ API установлен. Нажмите «Сбросить», чтобы изменить его.",
|
||||
"createNew": "Создать новый",
|
||||
"greeting": {
|
||||
"prepend": "Привет",
|
||||
|
||||
@@ -73,6 +73,11 @@
|
||||
"settingsThemeModeLight": "Açık",
|
||||
"settingsThemeModeDark": "Koyu",
|
||||
"settingsLanguage": "Dil",
|
||||
"settingsURLTitle": "Durum Sayfasında IP/URL'yi Göster",
|
||||
"settingsURLDescription": "Monitörün IP adresini veya URL'sini genel Durum sayfasında göster. Devre dışı bırakıldığında, hassas bilgileri korumak için yalnızca monitör adı gösterilecektir.",
|
||||
"settingsURLSelectTitle": "Durum sayfasında IP/URL'yi göster",
|
||||
"settingsURLEnabled": "Etkin",
|
||||
"settingURLDisabled": "Devre Dışı",
|
||||
"settingsDistributedUptime": "Dağıtılmış çalışma süresi",
|
||||
"settingsDistributedUptimeDescription": "Dağıtılmış çalışma süresi izlemeyi etkinleştirin/devre dışı bırakın.",
|
||||
"settingsEnabled": "Etkin",
|
||||
@@ -99,6 +104,20 @@
|
||||
"settingsRemoveAllMonitorsDialogConfirm": "Evet, tüm monitörleri kaldır",
|
||||
"settingsWallet": "Cüzdan",
|
||||
"settingsWalletDescription": "Cüzdanınızı buradan bağlayın. Bu, Dağıtılmış Çalışma Süresi monitörünün küresel olarak birden çok düğüme bağlanması için gereklidir.",
|
||||
"settingsTestEmail": "Test e-postası gönder",
|
||||
"settingsTestEmailSuccess": "Test e-postası başarıyla gönderildi",
|
||||
"settingsTestEmailFailed": "Test e-postası gönderilemedi",
|
||||
"settingsTestEmailFailedWithReason": "Test e-postası gönderilemedi: {{reason}}",
|
||||
"settingsTestEmailUnknownError": "Bilinmeyen hata",
|
||||
"settingsEmail": "E-posta",
|
||||
"settingsEmailDescription": "Sisteminiz için e-posta ayarlarını yapılandırın. Bu, bildirim ve uyarıları göndermek için kullanılır.",
|
||||
"settingsEmailHost": "E-posta sunucusu - SMTP sunucusunun ana bilgisayar adı veya IP adresi",
|
||||
"settingsEmailPort": "E-posta portu - Bağlanılacak port",
|
||||
"settingsEmailUser": "E-posta kullanıcısı - Kimlik doğrulama için kullanıcı adı, belirtilirse e-posta adresini geçersiz kılar",
|
||||
"settingsEmailAddress": "E-posta adresi - Kimlik doğrulama için kullanılır",
|
||||
"settingsEmailPassword": "E-posta şifresi - Kimlik doğrulama için şifre",
|
||||
"settingsEmailConnectionHost": "E-posta bağlantı sunucusu - HELO/EHLO selamlamasında kullanılacak ana bilgisayar adı",
|
||||
"settingsEmailRequiredFields": "E-posta sunucusu ve port gereklidir",
|
||||
"settingsAbout": "Hakkında",
|
||||
"settingsDevelopedBy": "Bluewave Labs tarafından geliştirilmiştir.",
|
||||
"settingsSave": "Kaydet",
|
||||
@@ -124,7 +143,6 @@
|
||||
"http": "HTTP",
|
||||
"monitor": "monitör",
|
||||
"aboutus": "Hakkımızda",
|
||||
|
||||
"now": "Şimdi",
|
||||
"delete": "Sil",
|
||||
"configure": "Yapılandır",
|
||||
@@ -397,7 +415,9 @@
|
||||
"pageSpeedWarning": "Uyarı: Google PageSpeed API anahtarı eklemediniz. Bu olmadan, PageSpeed monitörü çalışmayacaktır.",
|
||||
"pageSpeedLearnMoreLink": "Daha fazla bilgi için tıklayın",
|
||||
"pageSpeedAddApiKey": "API anahtarınızı nasıl ekleyeceğinizi öğrenin.",
|
||||
"pageSpeedApiKeyFieldDescription": "Sayfa hızı izlemeyi etkinleştirmek için Google PageSpeed API anahtarınızı girin. Anahtarı güncellemek için Sıfırla'ya tıklayın.",
|
||||
"pageSpeedApiKeyFieldTitle": "Google PageSpeed API anahtarı",
|
||||
"pageSpeedApiKeyFieldLabel": "PageSpeed API anahtarı",
|
||||
"pageSpeedApiKeyFieldDescription": "PageSpeed izlemeyi etkinleştirmek için Google PageSpeed API anahtarınızı girin. Anahtarı güncellemek için Sıfırla'ya tıklayın.",
|
||||
"pageSpeedApiKeyFieldResetLabel": "API anahtarı ayarlandı. Değiştirmek için Sıfırla'ya tıklayın.",
|
||||
"reset": "Sıfırla",
|
||||
"invalidFileFormat": "",
|
||||
@@ -513,4 +533,4 @@
|
||||
"saving": "Kaydediliyor..."
|
||||
},
|
||||
"uptimeCreateSelectURL": "İzlenecek URL veya IP adresini girin (örn. https://example.com/ veya 192.168.1.100) ve kontrol panelinde görünecek net bir görüntü adı ekleyin."
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import svgr from "vite-plugin-svgr";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: "/",
|
||||
plugins: [svgr(), react()],
|
||||
optimizeDeps: {
|
||||
include: ["@mui/material/Tooltip", "@emotion/styled"],
|
||||
|
||||
+2
-1
@@ -8,5 +8,6 @@ dist/redis/data/*
|
||||
prod/mongo/data/*
|
||||
prod/redis/data/*
|
||||
dist/docker-compose-test.yaml
|
||||
/dist-mono/mongo/data/*
|
||||
/dist-mono/redis/data/*
|
||||
*.env
|
||||
prod/certbot/*
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Change directory to root Server directory for correct Docker Context
|
||||
cd "$(dirname "$0")"
|
||||
cd ../..
|
||||
|
||||
# Ensure buildx builder exists and is used
|
||||
if ! docker buildx inspect mybuilder &>/dev/null; then
|
||||
docker buildx create --name mybuilder --use
|
||||
else
|
||||
docker buildx use mybuilder
|
||||
fi
|
||||
|
||||
# Define service names and Dockerfiles
|
||||
services=("mono_server_arm")
|
||||
dockerfiles=(
|
||||
"./docker/dist-arm/server.Dockerfile"
|
||||
)
|
||||
|
||||
# Static image name for GitHub Container Registry (GHCR)
|
||||
image="ghcr.io/bluewave-labs/checkmate:backend-dist-multi-arch"
|
||||
platforms="linux/amd64,linux/arm64"
|
||||
repo_url="https://github.com/bluewave-labs/checkmate"
|
||||
|
||||
# Loop through each service and build
|
||||
for i in "${!services[@]}"; do
|
||||
service="${services[$i]}"
|
||||
dockerfile="${dockerfiles[$i]}"
|
||||
|
||||
echo "🚀 Building multi-arch image for $service..."
|
||||
docker buildx build \
|
||||
--platform "$platforms" \
|
||||
-f "$dockerfile" \
|
||||
-t "$image" \
|
||||
--label "org.opencontainers.image.source=$repo_url" \
|
||||
--push \
|
||||
.
|
||||
|
||||
echo "✅ $image pushed for platforms: $platforms"
|
||||
done
|
||||
|
||||
echo "🎉 All multi-arch images built and pushed successfully"
|
||||
@@ -0,0 +1,54 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
server:
|
||||
image: ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch
|
||||
restart: always
|
||||
ports:
|
||||
- "52345:52345"
|
||||
environment:
|
||||
- UPTIME_APP_API_BASE_URL=http://localhost:52345/api/v1
|
||||
- UPTIME_APP_CLIENT_HOST=http://localhost
|
||||
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db?replicaSet=rs0
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- CLIENT_HOST=http://localhost
|
||||
- JWT_SECRET=my_secret
|
||||
depends_on:
|
||||
- redis
|
||||
- mongodb
|
||||
redis:
|
||||
image: redis:7
|
||||
container_name: checkmate-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- ./redis/data:/data
|
||||
restart: unless-stopped
|
||||
mongodb:
|
||||
image: mongo:4.4.18
|
||||
container_name: checkmate-mongodb
|
||||
restart: always
|
||||
command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- ./mongo/data:/data/db
|
||||
|
||||
mongo-init:
|
||||
image: mongo:4.4.18
|
||||
depends_on:
|
||||
- mongodb
|
||||
entrypoint: >
|
||||
bash -c "
|
||||
echo 'Waiting for MongoDB to be ready...' &&
|
||||
until mongo --host mongodb --eval 'db.adminCommand(\"ping\")' > /dev/null 2>&1; do
|
||||
sleep 2
|
||||
done &&
|
||||
echo 'MongoDB is up. Initiating replica set...' &&
|
||||
mongo --host mongodb --eval '
|
||||
rs.initiate({
|
||||
_id: \"rs0\",
|
||||
members: [{ _id: 0, host: \"mongodb:27017\" }]
|
||||
})
|
||||
' || echo 'Replica set may already be initialized.'
|
||||
"
|
||||
@@ -0,0 +1,25 @@
|
||||
FROM node:20-alpine AS frontend-build
|
||||
|
||||
WORKDIR /app/client
|
||||
|
||||
COPY client/package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY client ./
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine AS backend
|
||||
|
||||
WORKDIR /app/server
|
||||
|
||||
COPY server ./
|
||||
|
||||
COPY --from=frontend-build /app/client/dist ./public
|
||||
|
||||
RUN npm ci
|
||||
|
||||
RUN chmod +x ./scripts/inject-vars.sh
|
||||
|
||||
EXPOSE 52345
|
||||
|
||||
CMD ./scripts/inject-vars.sh && node ./index.js
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Change directory to root Server directory for correct Docker Context
|
||||
cd "$(dirname "$0")"
|
||||
cd ../..
|
||||
|
||||
# Define service names and their corresponding Dockerfiles in parallel arrays
|
||||
services=("mono_mongo" "mono_redis" "mono_server")
|
||||
dockerfiles=(
|
||||
"./docker/dist-mono/mongoDB.Dockerfile"
|
||||
"./docker/dist-mono/redis.Dockerfile"
|
||||
"./docker/dist-mono/server.Dockerfile"
|
||||
)
|
||||
|
||||
# Loop through each service and build the corresponding image
|
||||
for i in "${!services[@]}"; do
|
||||
service="${services[$i]}"
|
||||
dockerfile="${dockerfiles[$i]}"
|
||||
|
||||
docker build -f "$dockerfile" -t "$service" .
|
||||
|
||||
# Check if the build succeeded
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error building $service image. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "All images built successfully"
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
services:
|
||||
server:
|
||||
image: ghcr.io/bluewave-labs/checkmate:backend-dist-mono
|
||||
restart: always
|
||||
ports:
|
||||
- "52345:52345"
|
||||
environment:
|
||||
- UPTIME_APP_API_BASE_URL=http://localhost:52345/api/v1
|
||||
- UPTIME_APP_CLIENT_HOST=http://localhost
|
||||
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db?replicaSet=rs0
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- CLIENT_HOST=http://localhost
|
||||
- JWT_SECRET=my_secret
|
||||
depends_on:
|
||||
- redis
|
||||
- mongodb
|
||||
redis:
|
||||
image: ghcr.io/bluewave-labs/checkmate:redis-dist
|
||||
restart: always
|
||||
volumes:
|
||||
- ./redis/data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
mongodb:
|
||||
image: ghcr.io/bluewave-labs/checkmate:mongo-dist
|
||||
restart: always
|
||||
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
|
||||
volumes:
|
||||
- ./mongo/data:/data/db
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
start_interval: 1s
|
||||
retries: 30
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
FROM mongo
|
||||
EXPOSE 27017
|
||||
CMD ["mongod"]
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
FROM redis
|
||||
EXPOSE 6379
|
||||
@@ -0,0 +1,25 @@
|
||||
FROM node:20-alpine AS frontend-build
|
||||
|
||||
WORKDIR /app/client
|
||||
|
||||
COPY client/package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY client ./
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine AS app
|
||||
|
||||
WORKDIR /app/server
|
||||
|
||||
COPY server ./
|
||||
|
||||
COPY --from=frontend-build /app/client/dist ./public
|
||||
|
||||
RUN npm install
|
||||
|
||||
RUN chmod +x ./scripts/inject-vars.sh
|
||||
|
||||
EXPOSE 52345
|
||||
|
||||
CMD ./scripts/inject-vars.sh && node ./index.js
|
||||
@@ -2,8 +2,10 @@ node_modules
|
||||
.env
|
||||
*.log
|
||||
*.sh
|
||||
!scripts/inject-vars.sh
|
||||
.nyc_output
|
||||
coverage
|
||||
.clinic
|
||||
node_modules
|
||||
.vscode/*
|
||||
public
|
||||
@@ -595,8 +595,8 @@ class MonitorController {
|
||||
{ new: true }
|
||||
);
|
||||
monitor.isActive === true
|
||||
? await this.jobQueue.deleteJob(monitor)
|
||||
: await this.jobQueue.addJob(monitor._id, monitor);
|
||||
? await this.jobQueue.addJob(monitor._id, monitor)
|
||||
: await this.jobQueue.deleteJob(monitor);
|
||||
|
||||
return res.success({
|
||||
msg: monitor.isActive
|
||||
|
||||
@@ -23,7 +23,7 @@ class JobQueueController {
|
||||
|
||||
getJobs = async (req, res, next) => {
|
||||
try {
|
||||
const jobs = await this.jobQueue.getJobStats();
|
||||
const jobs = await this.jobQueue.getJobs();
|
||||
return res.success({
|
||||
msg: this.stringService.queueGetMetrics,
|
||||
data: jobs,
|
||||
@@ -60,7 +60,7 @@ class JobQueueController {
|
||||
|
||||
flushQueue = async (req, res, next) => {
|
||||
try {
|
||||
const result = await this.jobQueue.flushQueue();
|
||||
const result = await this.jobQueue.flushQueues();
|
||||
return res.success({
|
||||
msg: this.stringService.jobQueueFlush,
|
||||
data: result,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { updateAppSettingsBodyValidation } from "../validation/joi.js";
|
||||
import { handleValidationError, handleError } from "./controllerUtils.js";
|
||||
|
||||
import { sendTestEmailBodyValidation } from "../validation/joi.js";
|
||||
const SERVICE_NAME = "SettingsController";
|
||||
|
||||
class SettingsController {
|
||||
constructor(db, settingsService, stringService) {
|
||||
constructor({ db, settingsService, stringService, emailService }) {
|
||||
this.db = db;
|
||||
this.settingsService = settingsService;
|
||||
this.stringService = stringService;
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
getAppSettings = async (req, res, next) => {
|
||||
@@ -55,6 +56,59 @@ class SettingsController {
|
||||
next(handleError(error, SERVICE_NAME, "updateAppSettings"));
|
||||
}
|
||||
};
|
||||
|
||||
sendTestEmail = async (req, res, next) => {
|
||||
try {
|
||||
await sendTestEmailBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
to,
|
||||
systemEmailHost,
|
||||
systemEmailPort,
|
||||
systemEmailAddress,
|
||||
systemEmailPassword,
|
||||
systemEmailUser,
|
||||
systemEmailConnectionHost,
|
||||
} = req.body;
|
||||
|
||||
const subject = this.stringService.testEmailSubject;
|
||||
const context = { testName: "Monitoring System" };
|
||||
|
||||
const messageId = await this.emailService.buildAndSendEmail(
|
||||
"testEmailTemplate",
|
||||
context,
|
||||
to,
|
||||
subject,
|
||||
{
|
||||
systemEmailHost,
|
||||
systemEmailPort,
|
||||
systemEmailUser,
|
||||
systemEmailAddress,
|
||||
systemEmailPassword,
|
||||
systemEmailConnectionHost,
|
||||
}
|
||||
);
|
||||
|
||||
if (!messageId) {
|
||||
return res.error({
|
||||
msg: "Failed to send test email.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.sendTestEmail,
|
||||
data: { messageId },
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default SettingsController;
|
||||
|
||||
@@ -28,6 +28,10 @@ const AppSettingsSchema = mongoose.Schema(
|
||||
systemEmailUser: {
|
||||
type: String,
|
||||
},
|
||||
systemEmailConnectionHost: {
|
||||
type: String,
|
||||
default: "localhost",
|
||||
},
|
||||
singleton: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
||||
@@ -43,7 +43,7 @@ const createCheck = async (checkData) => {
|
||||
|
||||
const createChecks = async (checks) => {
|
||||
try {
|
||||
await Check.insertMany(checks);
|
||||
await Check.insertMany(checks, { ordered: false });
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "createCheck";
|
||||
|
||||
@@ -128,9 +128,8 @@ const createDistributedChecks = async (checksData) => {
|
||||
};
|
||||
});
|
||||
|
||||
// Execute bulk operation
|
||||
await DistributedUptimeCheck.bulkWrite(bulkOps, {
|
||||
ordered: false, // Allow parallel processing
|
||||
ordered: false,
|
||||
});
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
|
||||
@@ -51,7 +51,7 @@ const createHardwareCheck = async (hardwareCheckData) => {
|
||||
|
||||
const createHardwareChecks = async (hardwareChecks) => {
|
||||
try {
|
||||
await HardwareCheck.insertMany(hardwareChecks);
|
||||
await HardwareCheck.insertMany(hardwareChecks, { ordered: false });
|
||||
return true;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
|
||||
@@ -490,6 +490,7 @@ const buildMonitorsByTeamIdPipeline = ({ matchStage, field, order }) => {
|
||||
_id: 1,
|
||||
name: 1,
|
||||
type: 1,
|
||||
port: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -26,7 +26,7 @@ const createPageSpeedCheck = async (pageSpeedCheckData) => {
|
||||
};
|
||||
const createPageSpeedChecks = async (pageSpeedChecks) => {
|
||||
try {
|
||||
await PageSpeedCheck.insertMany(pageSpeedChecks);
|
||||
await PageSpeedCheck.insertMany(pageSpeedChecks, { ordered: false });
|
||||
return true;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
|
||||
+42
-29
@@ -46,7 +46,8 @@ import DiagnosticRoutes from "./routes/diagnosticRoute.js";
|
||||
import DiagnosticController from "./controllers/diagnosticController.js";
|
||||
|
||||
//JobQueue service and dependencies
|
||||
import JobQueue from "./service/jobQueue.js";
|
||||
import JobQueue from "./service/JobQueue/JobQueue.js";
|
||||
import JobQueueHelper from "./service/JobQueue/JobQueueHelper.js";
|
||||
import { Queue, Worker } from "bullmq";
|
||||
|
||||
//Network service and dependencies
|
||||
@@ -98,6 +99,8 @@ const openApiSpec = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "openapi.json"), "utf8")
|
||||
);
|
||||
|
||||
const frontendPath = path.join(__dirname, "public");
|
||||
|
||||
let server;
|
||||
|
||||
const shutdown = async () => {
|
||||
@@ -112,17 +115,13 @@ const shutdown = async () => {
|
||||
service: SERVICE_NAME,
|
||||
method: "shutdown",
|
||||
});
|
||||
// flush Redis
|
||||
const redisService = ServiceRegistry.get(RedisService.SERVICE_NAME);
|
||||
await redisService.flushall();
|
||||
|
||||
await ServiceRegistry.get(RedisService.SERVICE_NAME).flushRedis();
|
||||
process.exit(1);
|
||||
}, SHUTDOWN_TIMEOUT);
|
||||
try {
|
||||
server.close();
|
||||
await ServiceRegistry.get(JobQueue.SERVICE_NAME).obliterate();
|
||||
await ServiceRegistry.get(JobQueue.SERVICE_NAME).shutdown();
|
||||
await ServiceRegistry.get(MongoDB.SERVICE_NAME).disconnect();
|
||||
await ServiceRegistry.get(RedisService.SERVICE_NAME).flushall();
|
||||
logger.info({ message: "Graceful shutdown complete" });
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
@@ -182,23 +181,23 @@ const startApp = async () => {
|
||||
stringService
|
||||
);
|
||||
|
||||
const redisService = await RedisService.createInstance({
|
||||
logger,
|
||||
IORedis,
|
||||
SettingsService: settingsService,
|
||||
});
|
||||
const redisService = new RedisService({ Redis: IORedis, logger });
|
||||
|
||||
const jobQueue = new JobQueue({
|
||||
db,
|
||||
statusService,
|
||||
networkService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
stringService,
|
||||
logger,
|
||||
const jobQueueHelper = new JobQueueHelper({
|
||||
redisService,
|
||||
Queue,
|
||||
Worker,
|
||||
redisService,
|
||||
logger,
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
});
|
||||
const jobQueue = await JobQueue.create({
|
||||
db,
|
||||
jobQueueHelper,
|
||||
logger,
|
||||
stringService,
|
||||
});
|
||||
|
||||
// Register services
|
||||
@@ -241,11 +240,12 @@ const startApp = async () => {
|
||||
ServiceRegistry.get(EmailService.SERVICE_NAME)
|
||||
);
|
||||
|
||||
const settingsController = new SettingsController(
|
||||
ServiceRegistry.get(MongoDB.SERVICE_NAME),
|
||||
ServiceRegistry.get(SettingsService.SERVICE_NAME),
|
||||
ServiceRegistry.get(StringService.SERVICE_NAME)
|
||||
);
|
||||
const settingsController = new SettingsController({
|
||||
db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
|
||||
settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME),
|
||||
stringService: ServiceRegistry.get(StringService.SERVICE_NAME),
|
||||
emailService: ServiceRegistry.get(EmailService.SERVICE_NAME),
|
||||
});
|
||||
|
||||
const checkController = new CheckController(
|
||||
ServiceRegistry.get(MongoDB.SERVICE_NAME),
|
||||
@@ -309,9 +309,8 @@ const startApp = async () => {
|
||||
);
|
||||
const notificationRoutes = new NotificationRoutes(notificationController);
|
||||
const diagnosticRoutes = new DiagnosticRoutes(diagnosticController);
|
||||
// Init job queue
|
||||
await jobQueue.initJobQueue();
|
||||
// Middleware
|
||||
app.use(express.static(frontendPath));
|
||||
app.use(responseHandler);
|
||||
app.use(
|
||||
cors({
|
||||
@@ -322,7 +321,17 @@ const startApp = async () => {
|
||||
})
|
||||
);
|
||||
app.use(express.json());
|
||||
app.use(helmet());
|
||||
app.use(
|
||||
helmet({
|
||||
hsts: false,
|
||||
contentSecurityPolicy: {
|
||||
useDefaults: true,
|
||||
directives: {
|
||||
upgradeInsecureRequests: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
compression({
|
||||
level: 6,
|
||||
@@ -357,6 +366,10 @@ const startApp = async () => {
|
||||
status: "OK",
|
||||
});
|
||||
});
|
||||
// FE routes
|
||||
app.get("*", (req, res) => {
|
||||
res.sendFile(path.join(frontendPath, "index.html"));
|
||||
});
|
||||
app.use(handleErrors);
|
||||
};
|
||||
|
||||
|
||||
Generated
+11
-4
@@ -26,8 +26,9 @@
|
||||
"mailersend": "^2.2.0",
|
||||
"mjml": "^5.0.0-alpha.4",
|
||||
"mongoose": "^8.3.3",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^6.9.14",
|
||||
"papaparse": "^5.5.2",
|
||||
"ping": "0.4.4",
|
||||
"sharp": "0.33.5",
|
||||
"ssl-checker": "2.0.10",
|
||||
@@ -5499,6 +5500,12 @@
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/papaparse": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.2.tgz",
|
||||
"integrity": "sha512-PZXg8UuAc4PcVwLosEEDYjPyfWnTEhOrUfdv+3Bx+NuAb+5NhDmXzg5fHWmdCh1mP5p7JAZfFr3IMQfcntNAdA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -7290,9 +7297,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.21.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
|
||||
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
|
||||
"version": "6.21.3",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
||||
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
|
||||
@@ -4,8 +4,8 @@ import multer from "multer";
|
||||
import { fetchMonitorCertificate } from "../controllers/controllerUtils.js";
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage() // Store file in memory as Buffer
|
||||
});
|
||||
storage: multer.memoryStorage(), // Store file in memory as Buffer
|
||||
});
|
||||
|
||||
class MonitorRoutes {
|
||||
constructor(monitorController) {
|
||||
|
||||
@@ -15,6 +15,11 @@ class SettingsRoutes {
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
this.settingsController.updateAppSettings
|
||||
);
|
||||
this.router.post(
|
||||
"/test-email",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
this.settingsController.sendTestEmail
|
||||
);
|
||||
}
|
||||
|
||||
getRouter() {
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
for i in $(env | grep UPTIME_APP_)
|
||||
do
|
||||
key=$(echo $i | cut -d '=' -f 1)
|
||||
value=$(echo $i | cut -d '=' -f 2-)
|
||||
echo $key=$value
|
||||
find ./public/assets -type f \( -name '*.js' -o -name '*.css' \) -exec sed -i "s|${key}|${value}|g" '{}' +
|
||||
done
|
||||
@@ -0,0 +1,310 @@
|
||||
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware", "distributed"];
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
const HEALTH_CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes
|
||||
const QUEUE_LOOKUP = {
|
||||
hardware: "hardware",
|
||||
http: "uptime",
|
||||
ping: "uptime",
|
||||
port: "uptime",
|
||||
docker: "uptime",
|
||||
pagespeed: "pagespeed",
|
||||
distributed_http: "distributed",
|
||||
};
|
||||
const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`;
|
||||
|
||||
class JobQueue {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ db, jobQueueHelper, logger, stringService }) {
|
||||
this.db = db;
|
||||
this.jobQueueHelper = jobQueueHelper;
|
||||
this.stringService = stringService;
|
||||
this.logger = logger;
|
||||
this.queues = {};
|
||||
this.workers = [];
|
||||
}
|
||||
|
||||
static async create({ db, jobQueueHelper, logger, stringService }) {
|
||||
const instance = new JobQueue({ db, jobQueueHelper, logger, stringService });
|
||||
await instance.init();
|
||||
return instance;
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
await this.initQueues();
|
||||
await this.initWorkers();
|
||||
const monitors = await this.db.getAllMonitors();
|
||||
await Promise.all(
|
||||
monitors
|
||||
.filter((monitor) => monitor.isActive)
|
||||
.map(async (monitor) => {
|
||||
try {
|
||||
await this.addJob(monitor._id, monitor);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "initJobQueue",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.healthCheckInterval = setInterval(async () => {
|
||||
try {
|
||||
const queueHealthChecks = await this.checkQueueHealth();
|
||||
const queueIsStuck = queueHealthChecks.some((healthCheck) => healthCheck.stuck);
|
||||
if (queueIsStuck) {
|
||||
this.logger.warn({
|
||||
message: "Queue is stuck",
|
||||
service: SERVICE_NAME,
|
||||
method: "periodicHealthCheck",
|
||||
details: queueHealthChecks,
|
||||
});
|
||||
await this.flushQueues();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "periodicHealthCheck",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}, HEALTH_CHECK_INTERVAL);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "initJobQueue",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async initQueues() {
|
||||
const readyPromises = [];
|
||||
|
||||
for (const queueName of QUEUE_NAMES) {
|
||||
const q = this.jobQueueHelper.createQueue(queueName);
|
||||
this.queues[queueName] = q;
|
||||
readyPromises.push(q.waitUntilReady());
|
||||
}
|
||||
await Promise.all(readyPromises);
|
||||
this.logger.info({
|
||||
message: "Queues ready",
|
||||
service: SERVICE_NAME,
|
||||
method: "initQueues",
|
||||
});
|
||||
}
|
||||
|
||||
async initWorkers() {
|
||||
const workerReadyPromises = [];
|
||||
|
||||
for (const queueName of QUEUE_NAMES) {
|
||||
const worker = this.jobQueueHelper.createWorker(queueName, this.queues[queueName]);
|
||||
this.workers.push(worker);
|
||||
workerReadyPromises.push(worker.waitUntilReady());
|
||||
}
|
||||
await Promise.all(workerReadyPromises);
|
||||
this.logger.info({
|
||||
message: "Workers ready",
|
||||
service: SERVICE_NAME,
|
||||
method: "initWorkers",
|
||||
});
|
||||
}
|
||||
|
||||
async addJob(jobName, monitor) {
|
||||
this.logger.info({
|
||||
message: `Adding job ${monitor?.url ?? "No URL"}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "addJob",
|
||||
});
|
||||
|
||||
const queueName = QUEUE_LOOKUP[monitor.type];
|
||||
const queue = this.queues[queueName];
|
||||
if (typeof queue === "undefined") {
|
||||
throw new Error(`Queue for ${monitor.type} not found`);
|
||||
}
|
||||
const jobTemplate = {
|
||||
name: jobName,
|
||||
data: monitor,
|
||||
opts: {
|
||||
attempts: 1,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 1000,
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: false,
|
||||
timeout: 1 * 60 * 1000,
|
||||
},
|
||||
};
|
||||
|
||||
const schedulerId = getSchedulerId(monitor);
|
||||
await queue.upsertJobScheduler(
|
||||
schedulerId,
|
||||
{ every: monitor?.interval ?? 60000 },
|
||||
jobTemplate
|
||||
);
|
||||
}
|
||||
|
||||
async deleteJob(monitor) {
|
||||
try {
|
||||
const queue = this.queues[QUEUE_LOOKUP[monitor.type]];
|
||||
const schedulerId = getSchedulerId(monitor);
|
||||
const wasDeleted = await queue.removeJobScheduler(schedulerId);
|
||||
|
||||
if (wasDeleted === true) {
|
||||
this.logger.info({
|
||||
message: this.stringService.jobQueueDeleteJob,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteJob",
|
||||
details: `Deleted job ${monitor._id}`,
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
this.logger.error({
|
||||
message: this.stringService.jobQueueDeleteJob,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteJob",
|
||||
details: `Failed to delete job ${monitor._id}`,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "deleteJob") : null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getJobs() {
|
||||
try {
|
||||
let stats = {};
|
||||
await Promise.all(
|
||||
QUEUE_NAMES.map(async (name) => {
|
||||
const queue = this.queues[name];
|
||||
const jobs = await queue.getJobs();
|
||||
const ret = await Promise.all(
|
||||
jobs.map(async (job) => {
|
||||
const state = await job.getState();
|
||||
return { url: job.data.url, state, progress: job.progress };
|
||||
})
|
||||
);
|
||||
stats[name] = { jobs: ret };
|
||||
})
|
||||
);
|
||||
return stats;
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getJobStats") : null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getMetrics() {
|
||||
try {
|
||||
let metrics = {};
|
||||
|
||||
await Promise.all(
|
||||
QUEUE_NAMES.map(async (name) => {
|
||||
const queue = this.queues[name];
|
||||
const [waiting, active, failed, delayed, repeatableJobs] = await Promise.all([
|
||||
queue.getWaitingCount(),
|
||||
queue.getActiveCount(),
|
||||
queue.getFailedCount(),
|
||||
queue.getDelayedCount(),
|
||||
queue.getRepeatableJobs(),
|
||||
]);
|
||||
|
||||
metrics[name] = {
|
||||
waiting,
|
||||
active,
|
||||
failed,
|
||||
delayed,
|
||||
repeatableJobs: repeatableJobs.length,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return metrics;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMetrics",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async checkQueueHealth() {
|
||||
const res = [];
|
||||
for (const queueName of QUEUE_NAMES) {
|
||||
const q = this.queues[queueName];
|
||||
await q.waitUntilReady();
|
||||
|
||||
const lastJobProcessedTime = q.lastJobProcessedTime;
|
||||
const currentTime = Date.now();
|
||||
const timeDiff = currentTime - lastJobProcessedTime;
|
||||
|
||||
// Check for jobs
|
||||
const jobCounts = await q.getJobCounts();
|
||||
const hasJobs = Object.values(jobCounts).some((count) => count > 0);
|
||||
|
||||
res.push({
|
||||
queueName,
|
||||
timeSinceLastJob: timeDiff,
|
||||
stuck: hasJobs && timeDiff > 10000,
|
||||
jobCounts,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async flushQueues() {
|
||||
try {
|
||||
this.logger.warn({
|
||||
message: "Flushing queues",
|
||||
method: "flushQueues",
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
for (const worker of this.workers) {
|
||||
await worker.close();
|
||||
}
|
||||
this.workers = [];
|
||||
|
||||
for (const queue of Object.values(this.queues)) {
|
||||
await queue.obliterate();
|
||||
}
|
||||
this.queue = {};
|
||||
await this.init();
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: `${error.message} - Flushing redis manually`,
|
||||
service: SERVICE_NAME,
|
||||
method: "flushQueues",
|
||||
});
|
||||
return await this.jobQueueHelper.flushRedis();
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
this.healthCheckInterval = null;
|
||||
}
|
||||
for (const worker of this.workers) {
|
||||
await worker.close();
|
||||
}
|
||||
for (const queue of Object.values(this.queues)) {
|
||||
await queue.obliterate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default JobQueue;
|
||||
@@ -0,0 +1,318 @@
|
||||
const SERVICE_NAME = "JobQueueHelper";
|
||||
|
||||
class JobQueueHelper {
|
||||
constructor({
|
||||
redisService,
|
||||
Queue,
|
||||
Worker,
|
||||
logger,
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
}) {
|
||||
this.db = db;
|
||||
this.redisService = redisService;
|
||||
this.Queue = Queue;
|
||||
this.Worker = Worker;
|
||||
this.logger = logger;
|
||||
this.networkService = networkService;
|
||||
this.statusService = statusService;
|
||||
this.notificationService = notificationService;
|
||||
}
|
||||
|
||||
createQueue(queueName) {
|
||||
const connection = this.redisService.getNewConnection();
|
||||
const q = new this.Queue(queueName, {
|
||||
connection,
|
||||
});
|
||||
q.lastJobProcessedTime = Date.now();
|
||||
q.on("cleaned", (jobs, type) => {
|
||||
this.logger.debug({
|
||||
message: `Queue ${queueName} is cleaned with jobs: ${jobs} and type: ${type}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:cleaned",
|
||||
});
|
||||
});
|
||||
q.on("error", (err) => {
|
||||
this.logger.error({
|
||||
message: `Queue ${queueName} is error with msg: ${err}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:error",
|
||||
});
|
||||
});
|
||||
q.on("ioredis:close", () => {
|
||||
this.logger.debug({
|
||||
message: `Queue ${queueName} is ioredis:close`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:ioredis:close",
|
||||
});
|
||||
});
|
||||
q.on("paused", () => {
|
||||
this.logger.debug({
|
||||
message: `Queue ${queueName} is paused`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:paused",
|
||||
});
|
||||
});
|
||||
q.on("progress", (job, progress) => {
|
||||
this.logger.debug({
|
||||
message: `Queue ${queueName} is progress with msg: ${progress}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:progress",
|
||||
});
|
||||
});
|
||||
q.on("removed", (job) => {
|
||||
this.logger.debug({
|
||||
message: `Queue ${queueName} is removed with msg: ${job}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:removed",
|
||||
});
|
||||
});
|
||||
q.on("resumed", () => {
|
||||
this.logger.debug({
|
||||
message: `Queue ${queueName} is resumed`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:resumed",
|
||||
});
|
||||
});
|
||||
q.on("waiting", () => {
|
||||
this.logger.debug({
|
||||
message: `Queue ${queueName} is waiting`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:waiting",
|
||||
});
|
||||
});
|
||||
return q;
|
||||
}
|
||||
|
||||
createWorker(queueName, queue) {
|
||||
const connection = this.redisService.getNewConnection({
|
||||
maxRetriesPerRequest: null,
|
||||
});
|
||||
const worker = new this.Worker(queueName, this.createJobHandler(queue), {
|
||||
connection,
|
||||
concurrency: 50,
|
||||
});
|
||||
worker.on("active", (job) => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is active`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:active",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("closed", () => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is closed`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:closed",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("closing", (msg) => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is closing with msg: ${msg}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:closing",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("completed", (job) => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is completed`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:completed",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("drained", () => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is drained`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:drained",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("error", (failedReason) => {
|
||||
this.logger.error({
|
||||
message: `Worker ${queueName} is error with msg: ${failedReason}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:error",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("failed", (job, error, prev) => {
|
||||
this.logger.error({
|
||||
message: `Worker ${queueName} is failed with msg: ${error.message}`,
|
||||
service: error?.service ?? SERVICE_NAME,
|
||||
method: error?.method ?? "createWorker:failed",
|
||||
stack: error?.stack,
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("ioredis:close", () => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is ioredis:close`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:ioredis:close",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("paused", () => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is paused`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:paused",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("progress", (job, progress) => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is progress with msg: ${progress}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:progress",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("ready", () => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is ready`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:ready",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("resumed", () => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is resumed`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:resumed",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("stalled", () => {
|
||||
this.logger.warn({
|
||||
message: `Worker ${queueName} is stalled`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:stalled",
|
||||
});
|
||||
});
|
||||
return worker;
|
||||
}
|
||||
|
||||
async isInMaintenanceWindow(monitorId) {
|
||||
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId);
|
||||
// Check for active maintenance window:
|
||||
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
|
||||
if (window.active) {
|
||||
const start = new Date(window.start);
|
||||
const end = new Date(window.end);
|
||||
const now = new Date();
|
||||
const repeatInterval = window.repeat || 0;
|
||||
|
||||
// If start is < now and end > now, we're in maintenance
|
||||
if (start <= now && end >= now) return true;
|
||||
|
||||
// If maintenance window was set in the past with a repeat,
|
||||
// we need to advance start and end to see if we are in range
|
||||
|
||||
while (start < now && repeatInterval !== 0) {
|
||||
start.setTime(start.getTime() + repeatInterval);
|
||||
end.setTime(end.getTime() + repeatInterval);
|
||||
if (start <= now && end >= now) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return acc;
|
||||
}, false);
|
||||
return maintenanceWindowIsActive;
|
||||
}
|
||||
|
||||
createJobHandler(q) {
|
||||
return async (job) => {
|
||||
try {
|
||||
// Update the last job processed time for this queue
|
||||
q.lastJobProcessedTime = Date.now();
|
||||
// Get all maintenance windows for this monitor
|
||||
await job.updateProgress(0);
|
||||
const monitorId = job.data._id;
|
||||
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId);
|
||||
// If a maintenance window is active, we're done
|
||||
|
||||
if (maintenanceWindowActive) {
|
||||
await job.updateProgress(100);
|
||||
this.logger.info({
|
||||
message: `Monitor ${monitorId} is in maintenance window`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the current status
|
||||
await job.updateProgress(30);
|
||||
const networkResponse = await this.networkService.getStatus(job);
|
||||
if (
|
||||
job.data.type === "distributed_http" ||
|
||||
job.data.type === "distributed_test"
|
||||
) {
|
||||
await job.updateProgress(100);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the network response is not found, we're done
|
||||
if (!networkResponse) {
|
||||
await job.updateProgress(100);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle status change
|
||||
await job.updateProgress(60);
|
||||
const { monitor, statusChanged, prevStatus } =
|
||||
await this.statusService.updateStatus(networkResponse);
|
||||
// Handle notifications
|
||||
await job.updateProgress(80);
|
||||
this.notificationService
|
||||
.handleNotifications({
|
||||
...networkResponse,
|
||||
monitor,
|
||||
prevStatus,
|
||||
statusChanged,
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "createJobHandler",
|
||||
details: `Error sending notifications for job ${job.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
await job.updateProgress(100);
|
||||
return true;
|
||||
} catch (error) {
|
||||
await job.updateProgress(100);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
async flushRedis() {
|
||||
try {
|
||||
const connection = this.redisService.getNewConnection();
|
||||
const flushResult = await connection.flushall();
|
||||
return flushResult;
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "flushRedis",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default JobQueueHelper;
|
||||
@@ -80,7 +80,16 @@ class BufferService {
|
||||
});
|
||||
continue;
|
||||
}
|
||||
await operation(buffer);
|
||||
try {
|
||||
await operation(buffer);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "flushBuffers",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
this.buffers[bufferName] = [];
|
||||
}
|
||||
this.logger.info({
|
||||
|
||||
@@ -11,6 +11,7 @@ const SERVICE_NAME = "EmailService";
|
||||
*/
|
||||
class EmailService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the EmailService, initializing template loaders and the email transporter.
|
||||
* @param {Object} settingsService - The settings service to get email configuration.
|
||||
@@ -88,17 +89,25 @@ class EmailService {
|
||||
* @param {string} subject - The subject of the email.
|
||||
* @returns {Promise<string>} A promise that resolves to the messageId of the sent email.
|
||||
*/
|
||||
buildAndSendEmail = async (template, context, to, subject) => {
|
||||
buildAndSendEmail = async (template, context, to, subject, transportConfig) => {
|
||||
// TODO - Consider an update transporter method so this only needs to be recreated when smtp settings change
|
||||
let config;
|
||||
if (typeof transportConfig !== "undefined") {
|
||||
config = transportConfig;
|
||||
} else {
|
||||
config = await this.settingsService.getDBSettings();
|
||||
}
|
||||
|
||||
const {
|
||||
systemEmailHost,
|
||||
systemEmailPort,
|
||||
systemEmailUser,
|
||||
systemEmailAddress,
|
||||
systemEmailPassword,
|
||||
} = await this.settingsService.getDBSettings();
|
||||
systemEmailConnectionHost,
|
||||
} = config;
|
||||
|
||||
const emailConfig = {
|
||||
const baseEmailConfig = {
|
||||
host: systemEmailHost,
|
||||
port: systemEmailPort,
|
||||
secure: true,
|
||||
@@ -109,6 +118,22 @@ class EmailService {
|
||||
connectionTimeout: 5000,
|
||||
};
|
||||
|
||||
const isSmtps = Number(systemEmailPort) === 465;
|
||||
|
||||
const emailConfig = !isSmtps
|
||||
? {
|
||||
...baseEmailConfig,
|
||||
name: systemEmailConnectionHost || "localhost",
|
||||
secure: false,
|
||||
pool: true,
|
||||
tls: { rejectUnauthorized: false },
|
||||
}
|
||||
: baseEmailConfig;
|
||||
|
||||
if (!isSmtps) {
|
||||
delete emailConfig.auth;
|
||||
}
|
||||
|
||||
this.transporter = this.nodemailer.createTransport(emailConfig);
|
||||
|
||||
const buildHtml = async (template, context) => {
|
||||
@@ -149,4 +174,5 @@ class EmailService {
|
||||
return info?.messageId;
|
||||
};
|
||||
}
|
||||
|
||||
export default EmailService;
|
||||
|
||||
@@ -1,814 +0,0 @@
|
||||
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware", "distributed"];
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
const JOBS_PER_WORKER = 5;
|
||||
const HEALTH_CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes
|
||||
const QUEUE_LOOKUP = {
|
||||
hardware: "hardware",
|
||||
http: "uptime",
|
||||
ping: "uptime",
|
||||
port: "uptime",
|
||||
docker: "uptime",
|
||||
pagespeed: "pagespeed",
|
||||
distributed_http: "distributed",
|
||||
};
|
||||
const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`;
|
||||
|
||||
class NewJobQueue {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({
|
||||
db,
|
||||
statusService,
|
||||
networkService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
stringService,
|
||||
logger,
|
||||
Queue,
|
||||
Worker,
|
||||
redisService,
|
||||
}) {
|
||||
this.connection = redisService.getConnection();
|
||||
this.queues = {};
|
||||
this.workers = {};
|
||||
this.lastJobProcessedTime = {};
|
||||
|
||||
this.db = db;
|
||||
this.networkService = networkService;
|
||||
this.statusService = statusService;
|
||||
this.notificationService = notificationService;
|
||||
this.settingsService = settingsService;
|
||||
this.logger = logger;
|
||||
this.Worker = Worker;
|
||||
this.stringService = stringService;
|
||||
|
||||
QUEUE_NAMES.forEach((name) => {
|
||||
const q = new Queue(name, { connection: this.connection });
|
||||
this.lastJobProcessedTime[q.name] = Date.now();
|
||||
q.on("error", (error) => {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "queue:error",
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
this.queues[name] = q;
|
||||
|
||||
this.workers[name] = [];
|
||||
});
|
||||
|
||||
this.healthCheckInterval = setInterval(async () => {
|
||||
try {
|
||||
const health = await this.checkQueueHealth();
|
||||
if (health.stuck === true) {
|
||||
this.logger.error({
|
||||
message: `Queue is stuck: ${health.stuckQueues.join(", ")}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "healthCheckInterval",
|
||||
});
|
||||
await this.flushQueue();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "periodicHealthCheck",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}, HEALTH_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes job queues by adding jobs for all active monitors
|
||||
* @async
|
||||
* @function initJobQueue
|
||||
* @description Retrieves all monitors from the database and adds jobs for active ones to their respective queues
|
||||
* @throws {Error} If there's an error retrieving monitors or adding jobs
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async initJobQueue() {
|
||||
await this.connection.flushall();
|
||||
const monitors = await this.db.getAllMonitors();
|
||||
await Promise.all(
|
||||
monitors
|
||||
.filter((monitor) => monitor.isActive)
|
||||
.map(async (monitor) => {
|
||||
try {
|
||||
await this.addJob(monitor._id, monitor);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: `Failed to add job for monitor ${monitor._id}:`,
|
||||
service: SERVICE_NAME,
|
||||
method: "initJobQueue",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a monitor is currently in a maintenance window
|
||||
* @async
|
||||
* @param {string} monitorId - The ID of the monitor to check
|
||||
* @returns {Promise<boolean>} Returns true if the monitor is in an active maintenance window, false otherwise
|
||||
* @throws {Error} If there's an error retrieving maintenance windows from the database
|
||||
* @description
|
||||
* Retrieves all maintenance windows for a monitor and checks if any are currently active.
|
||||
* A maintenance window is considered active if:
|
||||
* 1. The window is marked as active AND
|
||||
* 2. Either:
|
||||
* - Current time falls between start and end times
|
||||
* - For repeating windows: Current time falls between any repeated interval
|
||||
*/
|
||||
async isInMaintenanceWindow(monitorId) {
|
||||
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId);
|
||||
// Check for active maintenance window:
|
||||
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
|
||||
if (window.active) {
|
||||
const start = new Date(window.start);
|
||||
const end = new Date(window.end);
|
||||
const now = new Date();
|
||||
const repeatInterval = window.repeat || 0;
|
||||
|
||||
// If start is < now and end > now, we're in maintenance
|
||||
if (start <= now && end >= now) return true;
|
||||
|
||||
// If maintenance window was set in the past with a repeat,
|
||||
// we need to advance start and end to see if we are in range
|
||||
|
||||
while (start < now && repeatInterval !== 0) {
|
||||
start.setTime(start.getTime() + repeatInterval);
|
||||
end.setTime(end.getTime() + repeatInterval);
|
||||
if (start <= now && end >= now) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return acc;
|
||||
}, false);
|
||||
return maintenanceWindowIsActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a job processing handler for monitor checks
|
||||
* @function createJobHandler
|
||||
* @returns {Function} An async function that processes monitor check jobs
|
||||
* @description
|
||||
* Creates and returns a job handler that:
|
||||
* 1. Checks if monitor is in maintenance window
|
||||
* 2. If not in maintenance, performs network status check
|
||||
* 3. Updates monitor status in database
|
||||
* 4. Triggers notifications if status changed
|
||||
*
|
||||
* @param {Object} job - The job to process
|
||||
* @param {Object} job.data - The monitor data
|
||||
* @param {string} job.data._id - Monitor ID
|
||||
* @param {string} job.id - Job ID
|
||||
*
|
||||
* @throws {Error} Logs errors but doesn't throw them to prevent job failure
|
||||
* @returns {Promise<void>} Resolves when job processing is complete
|
||||
*/
|
||||
createJobHandler() {
|
||||
return async (job) => {
|
||||
try {
|
||||
// Update the last job processed time for this queue
|
||||
this.lastJobProcessedTime[job.queue.name] = Date.now();
|
||||
// Get all maintenance windows for this monitor
|
||||
await job.updateProgress(0);
|
||||
const monitorId = job.data._id;
|
||||
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId);
|
||||
// If a maintenance window is active, we're done
|
||||
|
||||
if (maintenanceWindowActive) {
|
||||
await job.updateProgress(100);
|
||||
this.logger.info({
|
||||
message: `Monitor ${monitorId} is in maintenance window`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the current status
|
||||
await job.updateProgress(30);
|
||||
const networkResponse = await this.networkService.getStatus(job);
|
||||
if (
|
||||
job.data.type === "distributed_http" ||
|
||||
job.data.type === "distributed_test"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Handle status change
|
||||
await job.updateProgress(60);
|
||||
const { monitor, statusChanged, prevStatus } =
|
||||
await this.statusService.updateStatus(networkResponse);
|
||||
// Handle notifications
|
||||
await job.updateProgress(80);
|
||||
this.notificationService
|
||||
.handleNotifications({
|
||||
...networkResponse,
|
||||
monitor,
|
||||
prevStatus,
|
||||
statusChanged,
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "createJobHandler",
|
||||
details: `Error sending notifications for job ${job.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
await job.updateProgress(100);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: error.service ?? SERVICE_NAME,
|
||||
method: error.method ?? "createJobHandler",
|
||||
details: `Error processing job ${job.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new worker for processing jobs in a queue
|
||||
* @param {Queue} queue - The BullMQ queue to create a worker for
|
||||
* @returns {Worker} A new BullMQ worker instance
|
||||
* @description
|
||||
* Creates and configures a new worker with:
|
||||
* - Queue-specific job handler
|
||||
* - Redis connection settings
|
||||
* - Default worker options
|
||||
* The worker processes jobs from the specified queue using the job handler
|
||||
* created by createJobHandler()
|
||||
*
|
||||
* @throws {Error} If worker creation fails or connection is invalid
|
||||
*/
|
||||
createWorker(queue) {
|
||||
try {
|
||||
const worker = new this.Worker(queue.name, this.createJobHandler(), {
|
||||
connection: this.connection,
|
||||
concurrency: 5,
|
||||
stalledInterval: 10000,
|
||||
maxStalledCount: 1,
|
||||
lockDuration: 60000,
|
||||
});
|
||||
|
||||
worker.on("failed", (job, err) => {
|
||||
this.logger.error({
|
||||
message: `Job ${job.id} failed: ${err.message}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "worker:failed",
|
||||
stack: err.stack,
|
||||
jobData: job.data,
|
||||
});
|
||||
});
|
||||
worker.on("error", (job, err) => {
|
||||
this.logger.error({
|
||||
message: `Job ${job.id} error: ${err.message}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "worker:error",
|
||||
stack: err.stack,
|
||||
jobData: job.data,
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("stalled", (jobId) => {
|
||||
this.logger.warn({
|
||||
message: `Job ${jobId} stalled`,
|
||||
service: SERVICE_NAME,
|
||||
method: "worker:stalled",
|
||||
});
|
||||
});
|
||||
|
||||
return worker;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker",
|
||||
stack: error.stack,
|
||||
});
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "createWorker";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets stats related to the workers
|
||||
* This is used for scaling workers right now
|
||||
* In the future we will likely want to scale based on server performance metrics
|
||||
* CPU Usage & memory usage, if too high, scale down workers.
|
||||
* When to scale up? If jobs are taking too long to complete?
|
||||
* @async
|
||||
* @returns {Promise<WorkerStats>} - Returns the worker stats
|
||||
*/
|
||||
async getWorkerStats(queue) {
|
||||
try {
|
||||
const jobs = await queue.getRepeatableJobs();
|
||||
const load = jobs.length / this.workers[queue.name].length;
|
||||
return { jobs, load };
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getWorkerStats") : null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales workers up or down based on queue load
|
||||
* @async
|
||||
* @param {Object} workerStats - Statistics about current worker load
|
||||
* @param {number} workerStats.load - Current load per worker
|
||||
* @param {Array} workerStats.jobs - Array of current jobs
|
||||
* @param {Queue} queue - The BullMQ queue to scale workers for
|
||||
* @returns {Promise<boolean>} True if scaling occurred, false if no scaling was needed
|
||||
* @throws {Error} If no workers array exists for the queue
|
||||
* @description
|
||||
* Scales workers based on these rules:
|
||||
* - Maintains minimum of 5 workers
|
||||
* - Adds workers if load exceeds JOBS_PER_WORKER
|
||||
* - Removes workers if load is below JOBS_PER_WORKER
|
||||
* - Creates initial workers if none exist
|
||||
* Worker scaling is calculated based on excess jobs or excess capacity
|
||||
*/
|
||||
async scaleWorkers(workerStats, queue) {
|
||||
const workers = this.workers[queue.name];
|
||||
if (workers === undefined) {
|
||||
throw new Error(`No workers found for ${queue.name}`);
|
||||
}
|
||||
|
||||
if (workers.length === 0) {
|
||||
// There are no workers, need to add one
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const worker = this.createWorker(queue);
|
||||
workers.push(worker);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (workerStats.load > JOBS_PER_WORKER) {
|
||||
// Find out how many more jobs we have than current workers can handle
|
||||
const excessJobs = workerStats.jobs.length - workers.length * JOBS_PER_WORKER;
|
||||
// Divide by jobs/worker to find out how many workers to add
|
||||
const workersToAdd = Math.ceil(excessJobs / JOBS_PER_WORKER);
|
||||
for (let i = 0; i < workersToAdd; i++) {
|
||||
const worker = this.createWorker(queue);
|
||||
workers.push(worker);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (workerStats.load < JOBS_PER_WORKER) {
|
||||
// Find out how much excess capacity we have
|
||||
const workerCapacity = workers.length * JOBS_PER_WORKER;
|
||||
const excessCapacity = workerCapacity - workerStats.jobs.length;
|
||||
// Calculate how many workers to remove
|
||||
let workersToRemove = Math.floor(excessCapacity / JOBS_PER_WORKER); // Make sure there are always at least 5
|
||||
while (workersToRemove > 0 && workers.length > 5) {
|
||||
const worker = workers.pop();
|
||||
workersToRemove--;
|
||||
await worker.close().catch((error) => {
|
||||
// Catch the error instead of throwing it
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "scaleWorkers",
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all jobs in the queue.
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<Array<Job>>}
|
||||
* @throws {Error} - Throws error if getting jobs fails
|
||||
*/
|
||||
async getJobs(queue) {
|
||||
try {
|
||||
const jobs = await queue.getRepeatableJobs();
|
||||
return jobs;
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getJobs") : null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves detailed statistics about jobs and workers for all queues
|
||||
* @async
|
||||
* @returns {Promise<Object>} Queue statistics object
|
||||
* @throws {Error} If there's an error retrieving job information
|
||||
* @description
|
||||
* Returns an object with statistics for each queue including:
|
||||
* - List of jobs with their URLs and current states
|
||||
* - Number of workers assigned to the queue
|
||||
*/
|
||||
async getJobStats() {
|
||||
try {
|
||||
let stats = {};
|
||||
await Promise.all(
|
||||
QUEUE_NAMES.map(async (name) => {
|
||||
const queue = this.queues[name];
|
||||
const jobs = await queue.getJobs();
|
||||
const ret = await Promise.all(
|
||||
jobs.map(async (job) => {
|
||||
const state = await job.getState();
|
||||
return { url: job.data.url, state, progress: job.progress };
|
||||
})
|
||||
);
|
||||
stats[name] = { jobs: ret, workers: this.workers[name].length };
|
||||
})
|
||||
);
|
||||
return stats;
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getJobStats") : null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds both immediate and repeatable jobs to the appropriate queue
|
||||
* @async
|
||||
* @param {string} jobName - Name identifier for the job
|
||||
* @param {Object} monitor - Job data and configuration
|
||||
* @param {string} monitor.type - Type of monitor/queue ('uptime', 'pagespeed', 'hardware')
|
||||
* @param {string} [monitor.url] - URL to monitor (optional)
|
||||
* @param {number} [monitor.interval=60000] - Repeat interval in milliseconds
|
||||
* @param {string} monitor._id - Monitor ID
|
||||
* @throws {Error} If queue not found for payload type
|
||||
* @throws {Error} If job addition fails
|
||||
* @description
|
||||
* 1. Identifies correct queue based on payload type
|
||||
* 2. Adds immediate job execution
|
||||
* 3. Adds repeatable job with specified interval
|
||||
* 4. Scales workers based on updated queue load
|
||||
* Jobs are configured with exponential backoff, single attempt,
|
||||
* and automatic removal on completion
|
||||
*/
|
||||
async addJob(jobName, monitor) {
|
||||
try {
|
||||
this.logger.info({
|
||||
message: `Adding job ${monitor?.url ?? "No URL"}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "addJob",
|
||||
});
|
||||
|
||||
// Find the correct queue
|
||||
|
||||
const queue = this.queues[QUEUE_LOOKUP[monitor.type]];
|
||||
if (queue === undefined) {
|
||||
throw new Error(`Queue for ${monitor.type} not found`);
|
||||
}
|
||||
|
||||
const jobTemplate = {
|
||||
name: jobName,
|
||||
data: monitor,
|
||||
opts: {
|
||||
attempts: 1,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 1000,
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: false,
|
||||
timeout: 1 * 60 * 1000,
|
||||
},
|
||||
};
|
||||
|
||||
const schedulerId = getSchedulerId(monitor);
|
||||
await queue.upsertJobScheduler(
|
||||
schedulerId,
|
||||
{ every: monitor?.interval ?? 60000 },
|
||||
jobTemplate
|
||||
);
|
||||
|
||||
const workerStats = await this.getWorkerStats(queue);
|
||||
await this.scaleWorkers(workerStats, queue);
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "addJob") : null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a repeatable job from its queue and adjusts worker scaling
|
||||
* @async
|
||||
* @param {Object} monitor - Monitor object containing job details
|
||||
* @param {string} monitor._id - ID of the monitor/job to delete
|
||||
* @param {string} monitor.type - Type of monitor determining queue selection
|
||||
* @param {number} monitor.interval - Job repeat interval in milliseconds
|
||||
* @throws {Error} If queue not found for monitor type
|
||||
* @throws {Error} If job deletion fails
|
||||
* @description
|
||||
* 1. Identifies correct queue based on monitor type
|
||||
* 2. Removes repeatable job using monitor ID and interval
|
||||
* 3. Logs success or failure of deletion
|
||||
* 4. Updates worker scaling based on new queue load
|
||||
* Returns void but logs operation result
|
||||
*/
|
||||
async deleteJob(monitor) {
|
||||
try {
|
||||
const queue = this.queues[QUEUE_LOOKUP[monitor.type]];
|
||||
const schedulerId = getSchedulerId(monitor);
|
||||
const wasDeleted = await queue.removeJobScheduler(schedulerId);
|
||||
|
||||
if (wasDeleted === true) {
|
||||
this.logger.info({
|
||||
message: this.stringService.jobQueueDeleteJob,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteJob",
|
||||
details: `Deleted job ${monitor._id}`,
|
||||
});
|
||||
const workerStats = await this.getWorkerStats(queue);
|
||||
await this.scaleWorkers(workerStats, queue);
|
||||
} else {
|
||||
this.logger.error({
|
||||
message: this.stringService.jobQueueDeleteJob,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteJob",
|
||||
details: `Failed to delete job ${monitor._id}`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "deleteJob") : null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves comprehensive metrics for all queues
|
||||
* @async
|
||||
* @returns {Promise<Object.<string, QueueMetrics>>} Object with metrics for each queue
|
||||
* @throws {Error} If metrics retrieval fails
|
||||
* @description
|
||||
* Collects the following metrics for each queue:
|
||||
* - Number of waiting jobs
|
||||
* - Number of active jobs
|
||||
* - Number of completed jobs
|
||||
* - Number of failed jobs
|
||||
* - Number of delayed jobs
|
||||
* - Number of repeatable jobs
|
||||
* - Number of active workers
|
||||
*
|
||||
* @typedef {Object} QueueMetrics
|
||||
* @property {number} waiting - Count of jobs waiting to be processed
|
||||
* @property {number} active - Count of jobs currently being processed
|
||||
* @property {number} completed - Count of successfully completed jobs
|
||||
* @property {number} failed - Count of failed jobs
|
||||
* @property {number} delayed - Count of delayed jobs
|
||||
* @property {number} repeatableJobs - Count of repeatable job patterns
|
||||
* @property {number} workers - Count of active workers for this queue
|
||||
*/
|
||||
async getMetrics() {
|
||||
try {
|
||||
let metrics = {};
|
||||
|
||||
await Promise.all(
|
||||
QUEUE_NAMES.map(async (name) => {
|
||||
const queue = this.queues[name];
|
||||
const workers = this.workers[name];
|
||||
const [waiting, active, completed, failed, delayed, repeatableJobs] =
|
||||
await Promise.all([
|
||||
queue.getWaitingCount(),
|
||||
queue.getActiveCount(),
|
||||
queue.getCompletedCount(),
|
||||
queue.getFailedCount(),
|
||||
queue.getDelayedCount(),
|
||||
queue.getRepeatableJobs(),
|
||||
]);
|
||||
|
||||
metrics[name] = {
|
||||
waiting,
|
||||
active,
|
||||
completed,
|
||||
failed,
|
||||
delayed,
|
||||
repeatableJobs: repeatableJobs.length,
|
||||
workers: workers.length,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return metrics;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMetrics",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* @returns {Promise<boolean>} - Returns true if obliteration is successful
|
||||
*/
|
||||
|
||||
async obliterate() {
|
||||
try {
|
||||
this.logger.info({
|
||||
message: "Attempting to obliterate job queue...",
|
||||
service: SERVICE_NAME,
|
||||
method: "obliterate",
|
||||
});
|
||||
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
this.healthCheckInterval = null;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
QUEUE_NAMES.map(async (name) => {
|
||||
const queue = this.queues[name];
|
||||
await queue.pause();
|
||||
const jobs = await this.getJobs(queue);
|
||||
|
||||
// Remove all repeatable jobs
|
||||
for (const job of jobs) {
|
||||
await queue.removeRepeatableByKey(job.key);
|
||||
await queue.remove(job.id);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Close workers
|
||||
await Promise.all(
|
||||
QUEUE_NAMES.map(async (name) => {
|
||||
const workers = this.workers[name];
|
||||
await Promise.all(
|
||||
workers.map(async (worker) => {
|
||||
await worker.close();
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
QUEUE_NAMES.forEach(async (name) => {
|
||||
const queue = this.queues[name];
|
||||
await queue.obliterate();
|
||||
});
|
||||
|
||||
const metrics = await this.getMetrics();
|
||||
this.logger.info({
|
||||
message: this.stringService.jobQueueObliterate,
|
||||
service: SERVICE_NAME,
|
||||
method: "obliterate",
|
||||
details: metrics,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "obliterate") : null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// **************************
|
||||
// Queue Health Checks
|
||||
// **************************
|
||||
async getKeyValuePairs() {
|
||||
try {
|
||||
// Get all keys
|
||||
const keys = await this.connection.keys("*");
|
||||
|
||||
if (keys.length === 0) {
|
||||
return {}; // Return an empty object if no keys are found
|
||||
}
|
||||
|
||||
// Get values for all keys
|
||||
const values = await this.connection.mget(keys);
|
||||
|
||||
// Combine keys and values into an object
|
||||
const keyValuePairs = keys.reduce((result, key, index) => {
|
||||
result[key] = values[index];
|
||||
return result;
|
||||
}, {});
|
||||
this.logger.info({
|
||||
message: "Redis key-value",
|
||||
service: SERVICE_NAME,
|
||||
method: "flushQueue",
|
||||
details: keyValuePairs,
|
||||
});
|
||||
return keyValuePairs;
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getKeyValuePairs") : null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async flushQueue() {
|
||||
try {
|
||||
const keyValuePairs = await this.getKeyValuePairs();
|
||||
this.logger.info({
|
||||
message: "Before flush",
|
||||
service: SERVICE_NAME,
|
||||
method: "flushQueue",
|
||||
details: keyValuePairs,
|
||||
});
|
||||
const flushResult = await this.connection.flushall();
|
||||
const keyValuePairsAfter = await this.getKeyValuePairs();
|
||||
this.logger.info({
|
||||
message: "After flush",
|
||||
service: SERVICE_NAME,
|
||||
method: "flushQueue",
|
||||
details: keyValuePairsAfter,
|
||||
});
|
||||
if (flushResult !== "OK") {
|
||||
throw new Error("Failed to flush queue");
|
||||
}
|
||||
await this.initJobQueue();
|
||||
return {
|
||||
keyValuePairs,
|
||||
flush: flushResult,
|
||||
keyValuePairsAfter,
|
||||
init: true,
|
||||
};
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "flushQueue") : null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets metrics for a specific queue
|
||||
* @async
|
||||
* @function getQueueHealthMetrics
|
||||
* @param {Queue} queue - The queue to get metrics for
|
||||
* @returns {Promise<Object>} Queue metrics
|
||||
*/
|
||||
async getQueueHealthMetrics(queue) {
|
||||
return await queue.getJobCounts();
|
||||
}
|
||||
|
||||
getQueueIdleTimes() {
|
||||
const now = Date.now();
|
||||
const idleTimes = {};
|
||||
Object.entries(this.lastJobProcessedTime).forEach(([queueName, lastProcessed]) => {
|
||||
idleTimes[queueName] = now - lastProcessed;
|
||||
});
|
||||
return idleTimes;
|
||||
}
|
||||
async checkQueueHealth() {
|
||||
try {
|
||||
const currentTime = Date.now();
|
||||
const stuckQueues = [];
|
||||
const idleTimes = this.getQueueIdleTimes();
|
||||
for (const queueName of QUEUE_NAMES) {
|
||||
const queue = this.queues[queueName];
|
||||
|
||||
const jobCounts = await this.getQueueHealthMetrics(queue);
|
||||
const hasJobs = Object.values(jobCounts).some((count) => count > 0);
|
||||
|
||||
const timeSinceLastProcessed = currentTime - this.lastJobProcessedTime[queueName];
|
||||
const isStuck = hasJobs && timeSinceLastProcessed > HEALTH_CHECK_INTERVAL;
|
||||
|
||||
if (isStuck) {
|
||||
stuckQueues.push(queueName);
|
||||
}
|
||||
}
|
||||
|
||||
const queueHealth = { stuck: false, stuckQueues, idleTimes };
|
||||
|
||||
if (stuckQueues.length > 0) {
|
||||
queueHealth.stuck = true;
|
||||
}
|
||||
|
||||
this.logger.info({
|
||||
message: "Queue health check",
|
||||
service: SERVICE_NAME,
|
||||
method: "checkQueueHealth",
|
||||
details: queueHealth,
|
||||
});
|
||||
return queueHealth;
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "checkQueueHealth") : null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NewJobQueue;
|
||||
@@ -258,8 +258,8 @@ class NetworkService {
|
||||
if (dbSettings?.pagespeedApiKey) {
|
||||
pagespeedUrl += `&key=${dbSettings.pagespeedApiKey}`;
|
||||
} else {
|
||||
this.logger.info({
|
||||
message: "Pagespeed API key not found",
|
||||
this.logger.warn({
|
||||
message: "Pagespeed API key not found, job not executed",
|
||||
service: this.SERVICE_NAME,
|
||||
method: "requestPagespeed",
|
||||
details: { url },
|
||||
@@ -437,12 +437,6 @@ class NetworkService {
|
||||
throw new Error(response.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: "Error in requestDistributedHttp",
|
||||
service: this.SERVICE_NAME,
|
||||
method: "requestDistributedHttp",
|
||||
stack: error.stack,
|
||||
});
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestDistributedHttp";
|
||||
throw error;
|
||||
|
||||
@@ -1,71 +1,58 @@
|
||||
const SERVICE_NAME = "RedisService";
|
||||
|
||||
class RedisService {
|
||||
static SERVICE_NAME = "RedisService";
|
||||
|
||||
constructor({ logger, IORedis, SettingsService }) {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
constructor({ Redis, logger }) {
|
||||
this.Redis = Redis;
|
||||
this.connections = new Set();
|
||||
this.logger = logger;
|
||||
this.IORedis = IORedis;
|
||||
this.SettingsService = SettingsService;
|
||||
this.connection = null;
|
||||
}
|
||||
|
||||
static async createInstance({ logger, IORedis, SettingsService }) {
|
||||
const instance = new RedisService({ logger, IORedis, SettingsService });
|
||||
await instance.connect();
|
||||
return instance;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
const settings = this.SettingsService.getSettings();
|
||||
const { redisUrl } = settings;
|
||||
this.connection = new this.IORedis(redisUrl, {
|
||||
maxRetriesPerRequest: null,
|
||||
getNewConnection(options = {}) {
|
||||
const connection = new this.Redis(process.env.REDIS_URL, {
|
||||
retryStrategy: (times) => {
|
||||
if (times >= 5) {
|
||||
throw new Error("Failed to connect to Redis");
|
||||
}
|
||||
this.logger.debug({
|
||||
message: "Retrying Redis connection",
|
||||
service: RedisService.SERVICE_NAME,
|
||||
details: { times },
|
||||
});
|
||||
return Math.min(times * 100, 2000);
|
||||
return null;
|
||||
},
|
||||
...options,
|
||||
});
|
||||
this.connections.add(connection);
|
||||
return connection;
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
let errorOccurred = false;
|
||||
|
||||
this.connection.on("ready", () => {
|
||||
if (!errorOccurred) {
|
||||
this.logger.info({
|
||||
message: "Redis connection established",
|
||||
service: RedisService.SERVICE_NAME,
|
||||
});
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
this.connection.on("error", (err) => {
|
||||
errorOccurred = true;
|
||||
async closeAllConnections() {
|
||||
const closePromises = Array.from(this.connections).map((conn) =>
|
||||
conn.quit().catch((err) => {
|
||||
this.logger.error({
|
||||
message: "Redis connection error",
|
||||
service: RedisService.SERVICE_NAME,
|
||||
error: err,
|
||||
message: "Error closing Redis connection",
|
||||
service: SERVICE_NAME,
|
||||
method: "closeAllConnections",
|
||||
details: { error: err },
|
||||
});
|
||||
setTimeout(() => reject(err), 5000);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(closePromises);
|
||||
this.connections.clear();
|
||||
this.logger.info({
|
||||
message: "All Redis connections closed",
|
||||
service: SERVICE_NAME,
|
||||
method: "closeAllConnections",
|
||||
});
|
||||
}
|
||||
async flushall() {
|
||||
this.logger.debug({
|
||||
message: "Flushing all Redis data",
|
||||
service: RedisService.SERVICE_NAME,
|
||||
});
|
||||
await this.connection.flushall();
|
||||
}
|
||||
|
||||
getConnection() {
|
||||
return this.connection;
|
||||
async flushRedis() {
|
||||
this.logger.info({
|
||||
message: "Flushing Redis",
|
||||
service: SERVICE_NAME,
|
||||
method: "flushRedis",
|
||||
});
|
||||
const flushPromises = Array.from(this.connections).map((conn) => conn.flushall());
|
||||
await Promise.all(flushPromises);
|
||||
this.logger.info({
|
||||
message: "Redis flushed",
|
||||
service: SERVICE_NAME,
|
||||
method: "flushRedis",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -121,10 +121,10 @@ class StatusService {
|
||||
try {
|
||||
const { monitorId, status, code } = networkResponse;
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
|
||||
|
||||
// Update running stats
|
||||
this.updateRunningStats({ monitor, networkResponse });
|
||||
|
||||
|
||||
// No change in monitor status, return early
|
||||
if (monitor.status === status)
|
||||
return {
|
||||
@@ -134,7 +134,7 @@ class StatusService {
|
||||
code,
|
||||
timestamp: new Date().getTime(),
|
||||
};
|
||||
|
||||
|
||||
// Monitor status changed, save prev status and update monitor
|
||||
this.logger.info({
|
||||
service: this.SERVICE_NAME,
|
||||
@@ -144,11 +144,11 @@ class StatusService {
|
||||
prevStatus: monitor.status,
|
||||
newStatus: status,
|
||||
});
|
||||
|
||||
|
||||
const prevStatus = monitor.status;
|
||||
monitor.status = status;
|
||||
await monitor.save();
|
||||
|
||||
|
||||
return {
|
||||
monitor,
|
||||
statusChanged: true,
|
||||
@@ -157,12 +157,8 @@ class StatusService {
|
||||
timestamp: new Date().getTime(),
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
service: this.SERVICE_NAME,
|
||||
message: error.message,
|
||||
method: "updateStatus",
|
||||
stack: error.stack,
|
||||
});
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "updateStatus";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -427,11 +427,13 @@ const updateAppSettingsBodyValidation = joi.object({
|
||||
checkTTL: joi.number().allow(""),
|
||||
pagespeedApiKey: joi.string().allow(""),
|
||||
language: joi.string().allow(""),
|
||||
showURL: joi.bool().required(),
|
||||
systemEmailHost: joi.string().allow(""),
|
||||
systemEmailPort: joi.number().allow(""),
|
||||
systemEmailAddress: joi.string().allow(""),
|
||||
systemEmailPassword: joi.string().allow(""),
|
||||
systemEmailUser: joi.string().allow(""),
|
||||
systemEmailConnectionHost: joi.string().allow(""),
|
||||
});
|
||||
|
||||
//****************************************
|
||||
@@ -590,6 +592,16 @@ const createAnnouncementValidation = joi.object({
|
||||
userId: joi.string().required(),
|
||||
});
|
||||
|
||||
const sendTestEmailBodyValidation = joi.object({
|
||||
to: joi.string().required(),
|
||||
systemEmailHost: joi.string(),
|
||||
systemEmailPort: joi.number(),
|
||||
systemEmailAddress: joi.string(),
|
||||
systemEmailPassword: joi.string(),
|
||||
systemEmailUser: joi.string(),
|
||||
systemEmailConnectionHost: joi.string(),
|
||||
});
|
||||
|
||||
export {
|
||||
roleValidatior,
|
||||
loginValidation,
|
||||
@@ -653,4 +665,5 @@ export {
|
||||
triggerNotificationBodyValidation,
|
||||
webhookConfigValidation,
|
||||
createAnnouncementValidation,
|
||||
sendTestEmailBodyValidation,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user