diff --git a/Client/src/Components/Label/index.jsx b/Client/src/Components/Label/index.jsx
index 0799a87d7..96468ffd6 100644
--- a/Client/src/Components/Label/index.jsx
+++ b/Client/src/Components/Label/index.jsx
@@ -126,6 +126,7 @@ ColoredLabel.propTypes = {
*/
const StatusLabel = ({ status, text, customStyles }) => {
+ console.log(status);
const theme = useTheme();
const colors = {
up: {
@@ -138,6 +139,11 @@ const StatusLabel = ({ status, text, customStyles }) => {
bgColor: theme.palette.error.bg,
borderColor: theme.palette.error.light,
},
+ unknown: {
+ dotColor: theme.palette.unresolved.main,
+ bgColor: theme.palette.unresolved.bg,
+ borderColor: theme.palette.unresolved.light,
+ },
"cannot resolve": {
dotColor: theme.palette.unresolved.main,
bgColor: theme.palette.unresolved.bg,
@@ -170,7 +176,7 @@ const StatusLabel = ({ status, text, customStyles }) => {
};
StatusLabel.propTypes = {
- status: PropTypes.oneOf(["up", "down", "cannot resolve"]),
+ status: PropTypes.oneOf(["up", "down", "unknown", "cannot resolve"]),
text: PropTypes.string,
customStyles: PropTypes.object,
};
diff --git a/Client/src/Components/TabPanels/Account/TeamPanel.jsx b/Client/src/Components/TabPanels/Account/TeamPanel.jsx
index 8c4630b19..9809ac710 100644
--- a/Client/src/Components/TabPanels/Account/TeamPanel.jsx
+++ b/Client/src/Components/TabPanels/Account/TeamPanel.jsx
@@ -1,25 +1,13 @@
import { useTheme } from "@emotion/react";
import TabPanel from "@mui/lab/TabPanel";
-import {
- Box,
- Button,
- ButtonGroup,
- Divider,
- IconButton,
- Modal,
- Stack,
- TextField,
- Typography,
-} from "@mui/material";
+import { Button, ButtonGroup, Modal, Stack, Typography } from "@mui/material";
import { useEffect, useState } from "react";
-import EditSvg from "../../../assets/icons/edit.svg?react";
import Field from "../../Inputs/Field";
import { credentials } from "../../../Validation/validation";
import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import { useSelector } from "react-redux";
import BasicTable from "../../BasicTable";
-import Remove from "../../../assets/icons/trash-bin.svg?react";
import Select from "../../Inputs/Select";
import LoadingButton from "@mui/lab/LoadingButton";
@@ -54,6 +42,7 @@ const TeamPanel = () => {
const [members, setMembers] = useState([]);
const [filter, setFilter] = useState("all");
const [errors, setErrors] = useState({});
+ const [isSendingInvite, setIsSendingInvite] = useState(false);
useEffect(() => {
const fetchTeam = async () => {
@@ -173,6 +162,7 @@ const TeamPanel = () => {
};
const handleInviteMember = async () => {
+ setIsSendingInvite(true);
if (!toInvite.role.includes("user") || !toInvite.role.includes("admin"))
setToInvite((prev) => ({ ...prev, role: ["user"] }));
@@ -185,24 +175,28 @@ const TeamPanel = () => {
if (error) {
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
- } else
- try {
- await networkService.requestInvitationToken(
- authToken,
- toInvite.email,
- toInvite.role
- );
+ return;
+ }
- closeInviteModal();
- createToast({
- body: "Member invited. They will receive an email with details on how to create their account.",
- });
- } catch (error) {
- createToast({
- body: error.message || "Unknown error.",
- });
- }
+ try {
+ await networkService.requestInvitationToken(
+ authToken,
+ toInvite.email,
+ toInvite.role
+ );
+ closeInviteModal();
+ createToast({
+ body: "Member invited. They will receive an email with details on how to create their account.",
+ });
+ } catch (error) {
+ createToast({
+ body: error.message || "Unknown error.",
+ });
+ } finally {
+ setIsSendingInvite(false);
+ }
};
+
const closeInviteModal = () => {
setIsOpen(false);
setToInvite({ email: "", role: ["0"] });
@@ -307,13 +301,14 @@ const TeamPanel = () => {
-
+
{
mt={theme.spacing(8)}
justifyContent="flex-end"
>
-
+
Send invite
diff --git a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
index e32d673cc..317ab1320 100644
--- a/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
+++ b/Client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
@@ -28,6 +28,26 @@ export const createPageSpeed = createAsyncThunk(
}
);
+export const getPagespeedMonitorById = createAsyncThunk(
+ "monitors/getMonitorById",
+ async (data, thunkApi) => {
+ try {
+ const { authToken, monitorId } = data;
+ const res = await networkService.getMonitorByid(authToken, monitorId);
+ return res.data;
+ } catch (error) {
+ if (error.response && error.response.data) {
+ return thunkApi.rejectWithValue(error.response.data);
+ }
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
+ }
+ }
+);
+
export const getPageSpeedByTeamId = createAsyncThunk(
"pageSpeedMonitors/getPageSpeedByTeamId",
async (token, thunkApi) => {
@@ -109,6 +129,25 @@ export const deletePageSpeed = createAsyncThunk(
}
}
);
+export const pausePageSpeed = createAsyncThunk(
+ "pageSpeedMonitors/pausePageSpeed",
+ async (data, thunkApi) => {
+ try {
+ const { authToken, monitorId } = data;
+ const res = await networkService.pauseMonitorById(authToken, monitorId);
+ return res.data;
+ } catch (error) {
+ if (error.response && error.response.data) {
+ return thunkApi.rejectWithValue(error.response.data);
+ }
+ const payload = {
+ status: false,
+ msg: error.message ? error.message : "Unknown error",
+ };
+ return thunkApi.rejectWithValue(payload);
+ }
+ }
+);
const pageSpeedMonitorSlice = createSlice({
name: "pageSpeedMonitor",
@@ -124,7 +163,7 @@ const pageSpeedMonitorSlice = createSlice({
extraReducers: (builder) => {
builder
// *****************************************************
- // Monitors by userId
+ // Monitors by teamId
// *****************************************************
.addCase(getPageSpeedByTeamId.pending, (state) => {
@@ -143,6 +182,23 @@ const pageSpeedMonitorSlice = createSlice({
: "Getting page speed monitors failed";
})
+ // *****************************************************
+ .addCase(getPagespeedMonitorById.pending, (state) => {
+ state.isLoading = true;
+ })
+ .addCase(getPagespeedMonitorById.fulfilled, (state, action) => {
+ state.isLoading = false;
+ state.success = action.payload.success;
+ state.msg = action.payload.msg;
+ })
+ .addCase(getPagespeedMonitorById.rejected, (state, action) => {
+ state.isLoading = false;
+ state.success = false;
+ state.msg = action.payload
+ ? action.payload.msg
+ : "Failed to get pagespeed monitor";
+ })
+
// *****************************************************
// Create Monitor
// *****************************************************
@@ -163,7 +219,7 @@ const pageSpeedMonitorSlice = createSlice({
})
// *****************************************************
- // Create Monitor
+ // Update Monitor
// *****************************************************
.addCase(updatePageSpeed.pending, (state) => {
state.isLoading = true;
@@ -198,6 +254,24 @@ const pageSpeedMonitorSlice = createSlice({
state.msg = action.payload
? action.payload.msg
: "Failed to delete page speed monitor";
+ })
+ // *****************************************************
+ // Pause Monitor
+ // *****************************************************
+ .addCase(pausePageSpeed.pending, (state) => {
+ state.isLoading = true;
+ })
+ .addCase(pausePageSpeed.fulfilled, (state, action) => {
+ state.isLoading = false;
+ state.success = action.payload.success;
+ state.msg = action.payload.msg;
+ })
+ .addCase(pausePageSpeed.rejected, (state, action) => {
+ state.isLoading = false;
+ state.success = false;
+ state.msg = action.payload
+ ? action.payload.msg
+ : "Failed to pause page speed monitor";
});
},
});
diff --git a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
index 2c381812c..895bd1493 100644
--- a/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
+++ b/Client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
@@ -236,7 +236,7 @@ const uptimeMonitorsSlice = createSlice({
state.success = false;
state.msg = action.payload
? action.payload.msg
- : "Failed to pause uptime monitor";
+ : "Failed to get uptime monitor";
})
// *****************************************************
// update Monitor
diff --git a/Client/src/Pages/Monitors/Configure/index.jsx b/Client/src/Pages/Monitors/Configure/index.jsx
index 483a2fd79..6e3caeff7 100644
--- a/Client/src/Pages/Monitors/Configure/index.jsx
+++ b/Client/src/Pages/Monitors/Configure/index.jsx
@@ -275,14 +275,15 @@ const Configure = () => {
>
)}
-
+
diff --git a/Client/src/Pages/PageSpeed/Configure/index.jsx b/Client/src/Pages/PageSpeed/Configure/index.jsx
index 92dd5bb36..005583f2c 100644
--- a/Client/src/Pages/PageSpeed/Configure/index.jsx
+++ b/Client/src/Pages/PageSpeed/Configure/index.jsx
@@ -1,12 +1,14 @@
import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
-import { Box, Button, Modal, Skeleton, Stack, Typography } from "@mui/material";
+import { Box, Button, Modal, Stack, Typography } from "@mui/material";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router";
import {
deletePageSpeed,
+ getPagespeedMonitorById,
getPageSpeedByTeamId,
updatePageSpeed,
+ pausePageSpeed,
} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
@@ -17,63 +19,18 @@ import Checkbox from "../../../Components/Inputs/Checkbox";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
-
+import LoadingButton from "@mui/lab/LoadingButton";
+import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
+import SkeletonLayout from "./skeleton";
import "./index.css";
-/**
- * Renders a skeleton layout.
- *
- * @returns {JSX.Element}
- */
-const SkeletonLayout = () => {
- const theme = useTheme();
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
const PageSpeedConfigure = () => {
const theme = useTheme();
const navigate = useNavigate();
const dispatch = useDispatch();
const MS_PER_MINUTE = 60000;
const { authToken } = useSelector((state) => state.auth);
- const { monitors } = useSelector((state) => state.pageSpeedMonitors);
+ const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
const { monitorId } = useParams();
const [monitor, setMonitor] = useState({});
const [errors, setErrors] = useState({});
@@ -89,15 +46,25 @@ const PageSpeedConfigure = () => {
];
useEffect(() => {
- const data = monitors.find((monitor) => monitor._id === monitorId);
- if (!data) {
- logger.error("Error fetching pagespeed monitor of id: " + monitorId);
- navigate("/not-found", { replace: true });
- }
- setMonitor({
- ...data,
- });
- }, [monitorId, monitors, navigate]);
+ const fetchMonitor = async () => {
+ try {
+ const action = await dispatch(
+ getPagespeedMonitorById({ authToken, monitorId })
+ );
+
+ if (getPagespeedMonitorById.fulfilled.match(action)) {
+ const monitor = action.payload.data;
+ setMonitor(monitor);
+ } else if (getPagespeedMonitorById.rejected.match(action)) {
+ throw new Error(action.error.message);
+ }
+ } catch (error) {
+ logger.error("Error fetching monitor of id: " + monitorId);
+ navigate("/not-found", { replace: true });
+ }
+ };
+ fetchMonitor();
+ }, [dispatch, authToken, monitorId, navigate]);
const handleChange = (event, id) => {
let { value } = event.target;
@@ -119,6 +86,21 @@ const PageSpeedConfigure = () => {
});
};
+ const handlePause = async () => {
+ try {
+ const action = await dispatch(pausePageSpeed({ authToken, monitorId }));
+ if (pausePageSpeed.fulfilled.match(action)) {
+ const monitor = action.payload.data;
+ setMonitor(monitor);
+ } else if (pausePageSpeed.rejected.match(action)) {
+ throw new Error(action.error.message);
+ }
+ } catch (error) {
+ logger.error("Error pausing monitor: " + monitorId);
+ createToast({ body: "Failed to pause monitor" });
+ }
+ };
+
const handleSave = async (event) => {
event.preventDefault();
const action = await dispatch(
@@ -143,11 +125,9 @@ const PageSpeedConfigure = () => {
}
};
- let loading = Object.keys(monitor).length === 0;
-
return (
- {loading ? (
+ {Object.keys(monitor).length === 0 ? (
) : (
<>
@@ -195,7 +175,9 @@ const PageSpeedConfigure = () => {
-
-
+
{
-
+
>
diff --git a/Client/src/Pages/PageSpeed/Configure/skeleton.jsx b/Client/src/Pages/PageSpeed/Configure/skeleton.jsx
new file mode 100644
index 000000000..301a00e52
--- /dev/null
+++ b/Client/src/Pages/PageSpeed/Configure/skeleton.jsx
@@ -0,0 +1,51 @@
+import { Box, Skeleton, Stack } from "@mui/material";
+import { useTheme } from "@emotion/react";
+
+/**
+ * Renders a skeleton layout.
+ *
+ * @returns {JSX.Element}
+ */
+const SkeletonLayout = () => {
+ const theme = useTheme();
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default SkeletonLayout;
diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
index 00c02a362..4c8204153 100644
--- a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
+++ b/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx
@@ -1,4 +1,6 @@
-import { Box, Button, Stack, Typography } from "@mui/material";
+import { Box, Stack, Typography } from "@mui/material";
+import LoadingButton from "@mui/lab/LoadingButton";
+
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useDispatch, useSelector } from "react-redux";
@@ -12,11 +14,11 @@ import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMon
import Breadcrumbs from "../../../Components/Breadcrumbs";
import "./index.css";
import { logger } from "../../../Utils/Logger";
-
const CreatePageSpeed = () => {
const theme = useTheme();
const navigate = useNavigate();
const dispatch = useDispatch();
+ const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
const MS_PER_MINUTE = 60000;
const { user, authToken } = useSelector((state) => state.auth);
@@ -226,7 +228,8 @@ const CreatePageSpeed = () => {
-
+
diff --git a/Client/src/Pages/Settings/index.jsx b/Client/src/Pages/Settings/index.jsx
index fc615d848..d1f15ea8c 100644
--- a/Client/src/Pages/Settings/index.jsx
+++ b/Client/src/Pages/Settings/index.jsx
@@ -16,6 +16,9 @@ const Settings = ({ isAdmin }) => {
const { isLoading } = useSelector((state) => state.uptimeMonitors);
const dispatch = useDispatch();
+
+ // TODO Handle saving
+
const handleClearStats = async () => {
try {
const action = await dispatch(
@@ -156,13 +159,14 @@ const Settings = ({ isAdmin }) => {
-
+
diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js
index 31a23b5b1..6dcde82bb 100644
--- a/Client/src/Utils/NetworkService.js
+++ b/Client/src/Utils/NetworkService.js
@@ -116,7 +116,7 @@ class NetworkService {
* @param {string} [sortOrder] - The order in which to sort the retrieved statistics.
* @param {number} [limit] - The maximum number of statistics to retrieve.
* @param {string} [dateRange] - The date range for which to retrieve statistics.
- * @param {number} [numToDisplay] - The number of statistics to display.
+ * @param {number} [numToDisplay] - The number of checks to display.
* @param {boolean} [normalize] - Whether to normalize the retrieved statistics.
* @returns {Promise} The response from the axios GET request.
*/
diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js
index 9576f3064..091d41e2a 100644
--- a/Server/controllers/monitorController.js
+++ b/Server/controllers/monitorController.js
@@ -9,6 +9,8 @@ const {
getMonitorStatsByIdParamValidation,
getMonitorStatsByIdQueryValidation,
getCertificateParamValidation,
+ getMonitorAggregateStatsParamValidation,
+ getMonitorAggregateStatsQueryValidation,
} = require("../validation/joi");
const sslChecker = require("ssl-checker");
@@ -37,6 +39,44 @@ const getAllMonitors = async (req, res, next) => {
}
};
+/**
+ * Returns agregate stats for a monitor
+ * @async
+ * @param {Express.Request} req
+ * @param {Express.Response} res
+ * @returns {Promise}
+ * @throws {Error}
+ */
+
+const getMonitorAggregateStats = async (req, res, next) => {
+ try {
+ await getMonitorAggregateStatsParamValidation.validateAsync(req.params);
+ await getMonitorAggregateStatsQueryValidation.validateAsync(req.query);
+ } catch (error) {
+ error.status = 422;
+ error.message =
+ error.details?.[0]?.message || error.message || "Validation Error";
+ next(error);
+ return;
+ }
+
+ try {
+ const { monitorId } = req.params;
+ const dateRange = req.query.dateRange;
+ const aggregateStats = await req.db.getMonitorAggregateStats(
+ monitorId,
+ dateRange
+ );
+ return res.json({
+ success: true,
+ msg: successMessages.MONTIOR_STATS_BY_ID,
+ data: aggregateStats,
+ });
+ } catch (error) {
+ next(error);
+ }
+};
+
/**
* Returns monitor stats for monitor with matching ID
* @async
@@ -362,6 +402,7 @@ const pauseMonitor = async (req, res, next) => {
await req.jobQueue.addJob(monitor._id, monitor);
}
monitor.isActive = !monitor.isActive;
+ monitor.status = undefined;
monitor.save();
return res.status(200).json({
success: true,
@@ -379,6 +420,7 @@ const pauseMonitor = async (req, res, next) => {
module.exports = {
getAllMonitors,
+ getMonitorAggregateStats,
getMonitorStatsById,
getMonitorCertificate,
getMonitorById,
diff --git a/Server/db/mongo/MongoDB.js b/Server/db/mongo/MongoDB.js
index 912bfb438..6f959f758 100644
--- a/Server/db/mongo/MongoDB.js
+++ b/Server/db/mongo/MongoDB.js
@@ -64,6 +64,7 @@ const {
const {
getAllMonitors,
+ getMonitorAggregateStats,
getMonitorStatsById,
getMonitorById,
getMonitorsByTeamId,
@@ -144,6 +145,7 @@ module.exports = {
resetPassword,
checkSuperadmin,
getAllMonitors,
+ getMonitorAggregateStats,
getMonitorStatsById,
getMonitorById,
getMonitorsByTeamId,
diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js
index 79e8917ea..b913519b6 100644
--- a/Server/db/mongo/modules/monitorModule.js
+++ b/Server/db/mongo/modules/monitorModule.js
@@ -143,6 +143,87 @@ const getStatusBarValues = (monitor, checks) => {
}
return statusBarValues.reverse();
};
+
+/**
+ * Get aggregate monitor stats for charts
+ * @async
+ * @param {Express.Request} req
+ * @param {Express.Response} res
+ * @returns {Promise}
+ * @throws {Error}
+ */
+const getMonitorAggregateStats = async (monitorId, dateRange) => {
+ const startDates = {
+ day: new Date(new Date().setDate(new Date().getDate() - 1)),
+ week: new Date(new Date().setDate(new Date().getDate() - 7)),
+ month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
+ };
+
+ try {
+ const startDate = startDates[dateRange];
+ if (!startDate) {
+ throw new Error("Invalid date range specified");
+ }
+ const endDate = new Date();
+
+ const checks = await Check.find({
+ monitorId: monitorId,
+ createdAt: {
+ $gte: startDate,
+ $lte: endDate,
+ },
+ });
+
+ let groupedChecks;
+ // Group checks by hour if range is day
+ if (dateRange === "day") {
+ groupedChecks = checks.reduce((acc, check) => {
+ const hour = new Date(check.createdAt).getHours();
+ if (!acc[hour]) {
+ acc[hour] = { hour, checks: [] };
+ }
+ acc[hour].checks.push(check);
+ return acc;
+ }, {});
+ }
+
+ // Group checks by day if range is week or month
+ else {
+ groupedChecks = checks.reduce((acc, check) => {
+ const day = new Date(check.createdAt).toISOString().split("T")[0]; // Extract the date part
+ if (!acc[day]) {
+ acc[day] = { day, checks: [] };
+ }
+ acc[day].checks.push(check);
+ return acc;
+ }, {});
+ }
+
+ // Map grouped checks to stats
+ stats = Object.values(groupedChecks).map((group) => {
+ const totalChecks = group.checks.length;
+ const totalIncidents = group.checks.filter(
+ (check) => check.status === false
+ ).length;
+ const avgResponseTime =
+ group.checks.reduce((sum, check) => sum + check.responseTime, 0) /
+ totalChecks;
+
+ return {
+ hour: group.hour,
+ day: group.day,
+ totalChecks,
+ totalIncidents,
+ avgResponseTime,
+ };
+ });
+
+ return stats;
+ } catch (error) {
+ throw error;
+ }
+};
+
/**
* Get stats by monitor ID
* @async
@@ -200,6 +281,10 @@ const getMonitorStatsById = async (req) => {
...checksQuery,
createdAt: { $gte: filterLookup.day },
};
+ const checksQuery7Days = {
+ ...checksQuery,
+ createdAt: { $gte: filterLookup.week },
+ };
const checksQuery30Days = {
...checksQuery,
createdAt: { $gte: filterLookup.month },
@@ -209,15 +294,18 @@ const getMonitorStatsById = async (req) => {
createdAt: { $gte: filterLookup.hour },
};
- const [checks24Hours, checks30Days, checks60Mins] = await Promise.all([
- model.find(checksQuery24Hours).sort({ createdAt: sortOrder }),
- model.find(checksQuery30Days).sort({ createdAt: sortOrder }),
- model.find(checksQuery60Mins).sort({ createdAt: sortOrder }),
- ]);
+ const [checks24Hours, checks7Days, checks30Days, checks60Mins] =
+ await Promise.all([
+ model.find(checksQuery24Hours).sort({ createdAt: sortOrder }),
+ model.find(checksQuery7Days).sort({ createdAt: sortOrder }),
+ model.find(checksQuery30Days).sort({ createdAt: sortOrder }),
+ model.find(checksQuery60Mins).sort({ createdAt: sortOrder }),
+ ]);
// HTTP/PING Specific stats
monitorStats.avgResponseTime24hours =
getAverageResponseTime24Hours(checks24Hours);
+ monitorStats.uptime7Days = getUptimePercentage(checks7Days);
monitorStats.uptime24Hours = getUptimePercentage(checks24Hours);
monitorStats.uptime30Days = getUptimePercentage(checks30Days);
monitorStats.statusBar = getStatusBarValues(monitor, checks60Mins);
@@ -452,6 +540,7 @@ const editMonitor = async (candidateId, candidateMonitor) => {
module.exports = {
getAllMonitors,
+ getMonitorAggregateStats,
getMonitorStatsById,
getMonitorById,
getMonitorsByTeamId,
diff --git a/Server/models/Monitor.js b/Server/models/Monitor.js
index fdc147d1b..d91275e23 100644
--- a/Server/models/Monitor.js
+++ b/Server/models/Monitor.js
@@ -24,7 +24,7 @@ const MonitorSchema = mongoose.Schema(
},
status: {
type: Boolean,
- default: true,
+ default: undefined,
},
type: {
type: String,
@@ -46,7 +46,7 @@ const MonitorSchema = mongoose.Schema(
},
uptimePercentage: {
type: Number,
- default: undefined,
+ default: undefined,
},
notifications: [
{
diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js
index ee1c08488..7831f42f7 100644
--- a/Server/routes/monitorRoute.js
+++ b/Server/routes/monitorRoute.js
@@ -3,6 +3,7 @@ const monitorController = require("../controllers/monitorController");
const { isAllowed } = require("../middleware/isAllowed");
router.get("/", monitorController.getAllMonitors);
+router.get("/aggregate/:monitorId", monitorController.getMonitorAggregateStats);
router.get("/stats/:monitorId", monitorController.getMonitorStatsById);
router.get("/certificate/:monitorId", monitorController.getMonitorCertificate);
router.get("/:monitorId", monitorController.getMonitorById);
diff --git a/Server/service/jobQueue.js b/Server/service/jobQueue.js
index ed15e0f94..5248c88fa 100644
--- a/Server/service/jobQueue.js
+++ b/Server/service/jobQueue.js
@@ -238,7 +238,6 @@ class JobQueue {
console.log("Adding job", payload.url);
// Execute job immediately
await this.queue.add(jobName, payload);
-
await this.queue.add(jobName, payload, {
repeat: {
every: payload.interval,
diff --git a/Server/service/networkService.js b/Server/service/networkService.js
index f5c333a60..863349dbf 100644
--- a/Server/service/networkService.js
+++ b/Server/service/networkService.js
@@ -15,44 +15,60 @@ class NetworkService {
this.NETWORK_ERROR = 5000;
}
- async handleNotification(job, isAlive) {
+ async handleNotification(monitor, isAlive) {
try {
- const { _id } = job.data;
- const monitor = await this.db.getMonitorById(_id);
- if (monitor === null || monitor === undefined) {
- logger.error(`Null Monitor: ${_id}`, {
- method: "handleNotification",
- service: this.SERVICE_NAME,
- jobId: job.id,
- });
- return;
- }
+ let template =
+ isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate";
+ let status = isAlive === true ? "up" : "down";
- // If monitor status changes, update monitor status and send notification
- if (monitor.status !== isAlive) {
- monitor.status = !monitor.status;
- await monitor.save();
-
- let template =
- isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate";
- let status = isAlive === true ? "up" : "down";
-
- const notifications = await this.db.getNotificationsByMonitorId(_id);
- for (const notification of notifications) {
- if (notification.type === "email") {
- await this.emailService.buildAndSendEmail(
- template,
- { monitorName: monitor.name, monitorUrl: monitor.url },
- notification.address,
- `Monitor ${monitor.name} is ${status}`
- );
- }
+ const notifications = await this.db.getNotificationsByMonitorId(
+ monitor._id
+ );
+ for (const notification of notifications) {
+ if (notification.type === "email") {
+ await this.emailService.buildAndSendEmail(
+ template,
+ { monitorName: monitor.name, monitorUrl: monitor.url },
+ notification.address,
+ `Monitor ${monitor.name} is ${status}`
+ );
}
}
} catch (error) {
logger.error(error.message, {
method: "handleNotification",
service: this.SERVICE_NAME,
+ monitorId: monitor._id,
+ });
+ }
+ }
+
+ async handleStatusUpdate(job, isAlive) {
+ try {
+ const { _id } = job.data;
+ const monitor = await this.db.getMonitorById(_id);
+
+ if (monitor === null || monitor === undefined) {
+ logger.error(`Null Monitor: ${_id}`, {
+ method: "handleStatusUpdate",
+ service: this.SERVICE_NAME,
+ jobId: job.id,
+ });
+ return;
+ }
+ if (monitor.status === undefined || monitor.status !== isAlive) {
+ const oldStatus = monitor.status;
+ monitor.status = isAlive;
+ await monitor.save();
+
+ if (oldStatus !== undefined && oldStatus !== isAlive) {
+ this.handleNotification(monitor, isAlive);
+ }
+ }
+ } catch (error) {
+ logger.error(error.message, {
+ method: "handleStatusUpdate",
+ service: this.SERVICE_NAME,
jobId: job.id,
});
}
@@ -113,7 +129,7 @@ class NetworkService {
};
return await this.logAndStoreCheck(checkData, this.db.createCheck);
} finally {
- this.handleNotification(job, isAlive);
+ this.handleStatusUpdate(job, isAlive);
}
}
@@ -161,7 +177,7 @@ class NetworkService {
return await this.logAndStoreCheck(checkData, this.db.createCheck);
} finally {
- this.handleNotification(job, isAlive);
+ this.handleStatusUpdate(job, isAlive);
}
}
@@ -241,7 +257,7 @@ class NetworkService {
};
this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck);
} finally {
- this.handleNotification(job, isAlive);
+ this.handleStatusUpdate(job, isAlive);
}
}
diff --git a/Server/validation/joi.js b/Server/validation/joi.js
index f6a3aba68..d0ef24751 100644
--- a/Server/validation/joi.js
+++ b/Server/validation/joi.js
@@ -218,6 +218,13 @@ const pauseMonitorParamValidation = joi.object({
monitorId: joi.string().required(),
});
+const getMonitorAggregateStatsParamValidation = joi.object({
+ monitorId: joi.string().required(),
+});
+const getMonitorAggregateStatsQueryValidation = joi.object({
+ dateRange: joi.string().valid("day", "week", "month"),
+});
+
//****************************************
// Alerts
//****************************************
@@ -377,6 +384,8 @@ module.exports = {
getMonitorStatsByIdParamValidation,
getMonitorStatsByIdQueryValidation,
getCertificateParamValidation,
+ getMonitorAggregateStatsParamValidation,
+ getMonitorAggregateStatsQueryValidation,
editMonitorBodyValidation,
pauseMonitorParamValidation,
editUserParamValidation,