diff --git a/Client/src/Components/Alert/index.jsx b/Client/src/Components/Alert/index.jsx
index eb5fbd13b..cafd58451 100644
--- a/Client/src/Components/Alert/index.jsx
+++ b/Client/src/Components/Alert/index.jsx
@@ -31,7 +31,7 @@ const icons = {
const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
const theme = useTheme();
- const { text, light, border } = theme.palette[variant];
+ const { text, bg, border } = theme.palette[variant];
const icon = icons[variant];
return (
@@ -45,7 +45,7 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
padding: hasIcon
? theme.spacing(8)
: `${theme.spacing(4)} ${theme.spacing(8)}`,
- backgroundColor: light,
+ backgroundColor: bg,
border: `solid 1px ${border}`,
borderRadius: theme.shape.borderRadius,
}}
diff --git a/Client/src/Components/Charts/ResponseTimeChart/index.css b/Client/src/Components/Charts/ResponseTimeChart/index.css
deleted file mode 100644
index e69de29bb..000000000
diff --git a/Client/src/Components/Charts/ResponseTimeChart/index.jsx b/Client/src/Components/Charts/ResponseTimeChart/index.jsx
deleted file mode 100644
index fd6c0f41e..000000000
--- a/Client/src/Components/Charts/ResponseTimeChart/index.jsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import PropTypes from "prop-types";
-import { Stack } from "@mui/material";
-import { BarChart, Bar, ResponsiveContainer, Cell } from "recharts";
-import "./index.css";
-
-const ResponseTimeChart = ({ checks = [] }) => {
- return (
-
-
-
-
- {checks.map((check, index) => (
- |
- ))}
-
-
-
-
- );
-};
-
-ResponseTimeChart.propTypes = {
- checks: PropTypes.array,
-};
-export default ResponseTimeChart;
diff --git a/Client/src/Components/Inputs/Autocomplete/index.jsx b/Client/src/Components/Inputs/Autocomplete/index.jsx
new file mode 100644
index 000000000..5256efccc
--- /dev/null
+++ b/Client/src/Components/Inputs/Autocomplete/index.jsx
@@ -0,0 +1,116 @@
+import { Autocomplete, TextField } from "@mui/material";
+import React from "react";
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+
+/**
+ * @example
+ *
+ * const options = [
+ * { _id: "66d6119ef959cbc681e034f0", name: "Googler" },
+ * { _id: "66d6119ef959cbc681e034f0", name: "CNN" },
+ * { _id: "66d61a1bf959cbc681e0353f", name: "X Corp." },
+ * ];
+ *
+ *
+ *
+ */
+
+/**
+ * AutoCompleteField component.
+ *
+ * @component
+ * @param {Object} props - The component props.
+ * @param {string} props.id - The ID of the autocomplete field.
+ * @param {string} props.type - The type of the input field (text or number).
+ * @param {Array} props.options - The options for the autocomplete field.
+ * @param {string} props.placeholder - The placeholder text for the input field.
+ * @param {boolean} props.disabled - Indicates if the field is disabled.
+ * @returns {JSX.Element} The AutoCompleteField component.
+ */
+
+const AutoCompleteField = ({
+ id,
+ type,
+ options,
+ placeholder = "Type to search",
+ disabled,
+}) => {
+ const [value, setValue] = React.useState();
+ const [inputValue, setInputValue] = React.useState("");
+ const theme = useTheme();
+
+ return (
+ {
+ setValue(newValue);
+ }}
+ inputValue={inputValue}
+ onInputChange={(event, newInputValue) => {
+ setInputValue(newInputValue);
+ }}
+ options={options}
+ getOptionLabel={(option) => option.name}
+ renderInput={(params) => (
+
+ )}
+ renderOption={(props, option) => {
+ const { key, ...optionProps } = props;
+ return (
+
+ {{option.name}}
+
+ );
+ }}
+ slotProps={{
+ popper: {
+ sx: {
+ "& ul": { p: 0 },
+ "& li": { borderRadius: theme.shape.borderRadius },
+ },
+ },
+ paper: {
+ sx: {
+ p: 2,
+ fontSize: 13,
+ },
+ },
+ }}
+ />
+ );
+};
+
+AutoCompleteField.displayName = "AutoCompleteField";
+
+AutoCompleteField.propTypes = {
+ id: PropTypes.string,
+ type: PropTypes.oneOf(["text", "number"]),
+ options: PropTypes.array,
+ placeholder: PropTypes.string,
+ disabled: PropTypes.bool,
+ setWidth: PropTypes.string,
+};
+
+export default AutoCompleteField;
diff --git a/Client/src/Pages/Monitors/Home/host.jsx b/Client/src/Pages/Monitors/Home/host.jsx
index d450a53f4..9e6a71a05 100644
--- a/Client/src/Pages/Monitors/Home/host.jsx
+++ b/Client/src/Pages/Monitors/Home/host.jsx
@@ -53,7 +53,8 @@ Host.propTypes = {
params: PropTypes.shape({
title: PropTypes.string,
percentageColor: PropTypes.string,
- percentage: PropTypes.number,
+ percentage: PropTypes.string,
+ url: PropTypes.string,
}).isRequired,
};
diff --git a/Client/src/Pages/Monitors/Home/monitorData.jsx b/Client/src/Pages/Monitors/Home/monitorData.jsx
index d3fd66958..2351a59e9 100644
--- a/Client/src/Pages/Monitors/Home/monitorData.jsx
+++ b/Client/src/Pages/Monitors/Home/monitorData.jsx
@@ -27,30 +27,52 @@ const data = {
rows: [],
};
+/**
+ * Builds table data for a list of monitors.
+ *
+ * @param {Array} monitors - An array of monitor objects containing information about each monitor.
+ * @param {boolean} isAdmin - Flag indicating if the current user is an admin.
+ * @param {Function} navigate - A function to navigate to the monitor detail page.
+ * @returns {Object} The data structure containing columns and rows for the table.
+ */
export const buildData = (monitors, isAdmin, navigate) => {
const theme = useTheme();
data.rows = monitors.map((monitor, idx) => {
let uptimePercentage = "";
+ let percentageColor = theme.palette.percentage.uptimeExcellent;
+
+ // Determine uptime percentage and color based on the monitor's uptimePercentage value
if (monitor.uptimePercentage !== undefined) {
uptimePercentage =
monitor.uptimePercentage === 0
- ? "0"
- : (monitor.uptimePercentage * 100).toFixed(2);
+ ? "0"
+ : (monitor.uptimePercentage * 100).toFixed(2);
+
+ percentageColor =
+ monitor.uptimePercentage < 0.25
+ ? theme.palette.percentage.uptimePoor
+ : monitor.uptimePercentage < 0.5
+ ? theme.palette.percentage.uptimeFair
+ : monitor.uptimePercentage < 0.75
+ ? theme.palette.percentage.uptimeGood
+ : theme.palette.percentage.uptimeExcellent;
}
const params = {
url: monitor.url,
title: monitor.name,
percentage: uptimePercentage,
- percentageColor:
- monitor.status === true
- ? theme.palette.success.main
- : theme.palette.error.text,
- status: monitor.status === true ? "up" : "down",
+ percentageColor,
+ status:
+ monitor.status === undefined
+ ? "unknown"
+ : monitor.status === true
+ ? "up"
+ : "down",
};
- // Reverse checks so latest check is on the right
+ // Reverse checks so the latest check is on the right
const reversedChecks = monitor.checks.slice().reverse();
return {
@@ -86,6 +108,3 @@ export const buildData = (monitors, isAdmin, navigate) => {
});
return data;
};
-
-
-
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/Client/src/Utils/Theme/darkTheme.js b/Client/src/Utils/Theme/darkTheme.js
index eca445976..1f2b7c85b 100644
--- a/Client/src/Utils/Theme/darkTheme.js
+++ b/Client/src/Utils/Theme/darkTheme.js
@@ -57,6 +57,12 @@ const darkTheme = createTheme({
bg: "#624711",
border: "#e88c30",
},
+ percentage: {
+ uptimePoor: "#d32f2f",
+ uptimeFair: "#e88c30",
+ uptimeGood: "#ffd600",
+ uptimeExcellent: "#079455",
+ },
unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" },
divider: border.light,
other: {
diff --git a/Client/src/Utils/Theme/lightTheme.js b/Client/src/Utils/Theme/lightTheme.js
index ef38a2abc..4d554c54c 100644
--- a/Client/src/Utils/Theme/lightTheme.js
+++ b/Client/src/Utils/Theme/lightTheme.js
@@ -54,6 +54,12 @@ const lightTheme = createTheme({
bg: "#ffecbc",
border: "#fec84b",
},
+ percentage: {
+ uptimePoor: "#d32f2f",
+ uptimeFair: "#ec8013",
+ uptimeGood: "#ffb800",
+ uptimeExcellent: "#079455",
+ },
unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" },
divider: border.light,
other: {
diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js
index f33ddcf10..0d831f5b6 100644
--- a/Server/controllers/monitorController.js
+++ b/Server/controllers/monitorController.js
@@ -6,12 +6,13 @@ const {
editMonitorBodyValidation,
getMonitorsByTeamIdQueryValidation,
pauseMonitorParamValidation,
+ getMonitorAggregateStatsParamValidation,
+ getMonitorAggregateStatsQueryValidation,
} = require("../validation/joi");
const sslChecker = require("ssl-checker");
const SERVICE_NAME = "monitorController";
const { errorMessages, successMessages } = require("../utils/messages");
-const { runInNewContext } = require("vm");
/**
* Returns all monitors
@@ -35,6 +36,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
@@ -376,6 +415,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/openapi.json b/Server/openapi.json
new file mode 100644
index 000000000..23c16521d
--- /dev/null
+++ b/Server/openapi.json
@@ -0,0 +1,835 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "BlueWave Uptime",
+ "summary": "BlueWave Uptime OpenAPI Specifications",
+ "description": "BlueWave Uptime is an open source server monitoring application used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.",
+ "contact": {
+ "name": "API Support",
+ "url": "mailto:support@bluewavelabs.ca",
+ "email": "support@bluewavelabs.ca"
+ },
+ "license": {
+ "name": "AGPLv3",
+ "url": "https://github.com/bluewave-labs/bluewave-uptime/tree/HEAD/LICENSE"
+ },
+ "version": "1.0"
+ },
+ "servers": [
+ {
+ "url": "http://localhost:{PORT}/{API_PATH}",
+ "description": "Local Development Server",
+ "variables": {
+ "PORT": {
+ "description": "API Port",
+ "enum": ["5000", "3000", "8080"],
+ "default": "5000"
+ },
+ "API_PATH": {
+ "description": "API Base Path",
+ "enum": ["api/v1", "api/v1.1", "api/v2"],
+ "default": "api/v1"
+ }
+ }
+ }
+ ],
+ "tags": [
+ {
+ "name": "auth",
+ "description": "Authentication"
+ },
+ {
+ "name": "invite",
+ "description": "Invite"
+ },
+ {
+ "name": "monitors",
+ "description": "Monitors"
+ },
+ {
+ "name": "checks",
+ "description": "Checks"
+ },
+ {
+ "name": "alerts",
+ "description": "Alerts"
+ },
+ {
+ "name": "pagespeed",
+ "description": "Page Speed"
+ },
+ {
+ "name": "maintenance-window",
+ "description": "Maintenance window"
+ },
+ {
+ "name": "healthy",
+ "description": "Health check"
+ },
+ {
+ "name": "mail",
+ "description": "Mail service"
+ }
+ ],
+ "paths": {
+ "/auth/register": {
+ "post": {
+ "tags": ["auth"],
+ "description": "Register a new user",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["firstName", "lastName", "email", "password", "role", "teamId"],
+ "properties": {
+ "firstName": {
+ "type": "string"
+ },
+ "lastName": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string",
+ "format": "email"
+ },
+ "password": {
+ "type": "string",
+ "format": "password"
+ },
+ "profileImage": {
+ "type": "file",
+ "format": "file"
+ },
+ "role": {
+ "type": "array",
+ "enum": [["user"], ["admin"], ["superadmin"]],
+ "default": ["superadmin"]
+ },
+ "teamId": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/login": {
+ "post": {
+ "tags": ["auth"],
+ "description": "Login with credentials",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["email", "password"],
+ "properties": {
+ "email": {
+ "type": "string",
+ "format": "email"
+ },
+ "password": {
+ "type": "string",
+ "format": "password"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/user/:userId": {
+ "put": {
+ "tags": ["auth"],
+ "description": "Change user informations",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": ["auth"],
+ "description": "Delete user",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/users/admin": {
+ "get": {
+ "tags": ["auth"],
+ "description": "Checks to see if an admin account exists",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/users": {
+ "get": {
+ "tags": ["auth"],
+ "description": "Get all users",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/invite": {
+ "post": {
+ "tags": ["auth"],
+ "description": "Invite people to application",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/invite/verify": {
+ "post": {
+ "tags": ["auth"],
+ "description": "Verify the invite",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/recovery/request": {
+ "post": {
+ "tags": ["auth"],
+ "description": "Request a recovery token",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/recovery/validate": {
+ "post": {
+ "tags": ["auth"],
+ "description": "Validate recovery token",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/recovery/reset": {
+ "post": {
+ "tags": ["auth"],
+ "description": "Password reset",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UserSuccessResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Content",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/healthy": {
+ "get": {
+ "tags": ["healthy"],
+ "description": "Health check",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "ErrorResponse": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ }
+ }
+ },
+ "SuccessResponse": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object"
+ }
+ }
+ },
+ "UserSuccessResponse": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "$ref": "#/components/schemas/UserDataType"
+ }
+ }
+ },
+ "MonitorSuccessResponse": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "$ref": "#/components/schemas/MonitorDataType"
+ }
+ }
+ },
+ "CheckDataSuccessResponse": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "$ref": "#/components/schemas/CheckDataType"
+ }
+ }
+ },
+ "AlertDataSuccessResponse": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean"
+ },
+ "msg": {
+ "type": "string"
+ },
+ "data": {
+ "type": "object",
+ "$ref": "#/components/schemas/AlertDataType"
+ }
+ }
+ },
+ "UserDataType": {
+ "type": "object",
+ "required": [
+ "firstname",
+ "lastname",
+ "email",
+ "profilePicUrl",
+ "isActive",
+ "isVerified",
+ "updatedAt",
+ "createdAt"
+ ],
+ "properties": {
+ "firstname": {
+ "type": "string"
+ },
+ "lastname": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string",
+ "format": "email"
+ },
+ "profilePicUrl": {
+ "type": "string"
+ },
+ "isActive": {
+ "type": "boolean"
+ },
+ "isVerified": {
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string"
+ }
+ }
+ },
+ "MonitorDataType": {
+ "type": "object",
+ "required": [
+ "userId",
+ "name",
+ "description",
+ "url",
+ "isActive",
+ "interval",
+ "updatedAt",
+ "createdAt"
+ ],
+ "properties": {
+ "userId": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "isActive": {
+ "type": "boolean"
+ },
+ "interval": {
+ "type": "integer"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string"
+ }
+ }
+ },
+ "CheckDataType": {
+ "type": "object",
+ "required": [
+ "monitorId",
+ "status",
+ "responseTime",
+ "statusCode",
+ "message",
+ "updatedAt",
+ "createdAt"
+ ],
+ "properties": {
+ "monitorId": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ },
+ "responseTime": {
+ "type": "integer"
+ },
+ "statusCode": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string"
+ }
+ }
+ },
+ "AlertDataType": {
+ "type": "object",
+ "required": [
+ "checkId",
+ "monitorId",
+ "userId",
+ "status",
+ "message",
+ "notifiedStatus",
+ "acknowledgedStatus",
+ "updatedAt",
+ "createdAt"
+ ],
+ "properties": {
+ "checkId": {
+ "type": "string"
+ },
+ "monitorId": {
+ "type": "string"
+ },
+ "userId": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "notifiedStatus": {
+ "type": "boolean"
+ },
+ "acknowledgedStatus": {
+ "type": "boolean"
+ },
+ "updatedAt": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+}
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/validation/joi.js b/Server/validation/joi.js
index ccc8da9f6..629deddf5 100644
--- a/Server/validation/joi.js
+++ b/Server/validation/joi.js
@@ -201,6 +201,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
//****************************************
@@ -357,6 +364,8 @@ module.exports = {
getMonitorByIdQueryValidation,
getMonitorsByTeamIdValidation,
getMonitorsByTeamIdQueryValidation,
+ getMonitorAggregateStatsParamValidation,
+ getMonitorAggregateStatsQueryValidation,
editMonitorBodyValidation,
pauseMonitorParamValidation,
editUserParamValidation,