diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js
index 197779b53..bc391ebf6 100644
--- a/client/src/Hooks/monitorHooks.js
+++ b/client/src/Hooks/monitorHooks.js
@@ -6,7 +6,7 @@ import { useMonitorUtils } from "./useMonitorUtils.js";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
-const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => {
+export const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => {
const [isLoading, setIsLoading] = useState(false);
const [monitors, setMonitors] = useState(undefined);
const [monitorsSummary, setMonitorsSummary] = useState(undefined);
@@ -37,7 +37,7 @@ const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => {
return [monitors, monitorsSummary, isLoading, networkError];
};
-const useFetchMonitorsWithChecks = ({
+export const useFetchMonitorsWithChecks = ({
types,
limit,
page,
@@ -100,7 +100,7 @@ const useFetchMonitorsWithChecks = ({
return [monitors, count, isLoading, networkError];
};
-const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => {
+export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(false);
const [monitors, setMonitors] = useState(undefined);
const [networkError, setNetworkError] = useState(false);
@@ -126,15 +126,11 @@ const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => {
}
};
fetchMonitors();
- }, [
- types,
- filter,
- updateTrigger,
- ]);
+ }, [types, filter, updateTrigger]);
return [monitors, isLoading, networkError];
};
-const useFetchStatsByMonitorId = ({
+export const useFetchStatsByMonitorId = ({
monitorId,
sortOrder,
limit,
@@ -173,7 +169,7 @@ const useFetchStatsByMonitorId = ({
return [monitor, audits, isLoading, networkError];
};
-const useFetchMonitorGames = ({ setGames, updateTrigger }) => {
+export const useFetchMonitorGames = ({ setGames, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchGames = async () => {
@@ -192,7 +188,7 @@ const useFetchMonitorGames = ({ setGames, updateTrigger }) => {
return [isLoading];
};
-const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
+export const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
if (typeof monitorId === "undefined") {
@@ -215,7 +211,7 @@ const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
return [isLoading];
};
-const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => {
+export const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [monitor, setMonitor] = useState(undefined);
@@ -241,8 +237,34 @@ const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) =>
}, [monitorId, dateRange, updateTrigger]);
return [monitor, isLoading, networkError];
};
+export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigger }) => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [networkError, setNetworkError] = useState(false);
+ const [monitor, setMonitor] = useState(undefined);
-const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
+ useEffect(() => {
+ const fetchMonitor = async () => {
+ try {
+ if (!monitorId) {
+ return { monitor: undefined, isLoading: false, networkError: undefined };
+ }
+ const response = await networkService.getPageSpeedDetailsByMonitorId({
+ monitorId: monitorId,
+ dateRange: dateRange,
+ });
+ setMonitor(response.data.data);
+ } catch (error) {
+ setNetworkError(true);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchMonitor();
+ }, [monitorId, dateRange, updateTrigger]);
+ return [monitor, isLoading, networkError];
+};
+
+export const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
const [networkError, setNetworkError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [monitor, setMonitor] = useState(undefined);
@@ -270,7 +292,7 @@ const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
return [monitor, monitorStats, isLoading, networkError];
};
-const useCreateMonitor = () => {
+export const useCreateMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const createMonitor = async ({ monitor, redirect }) => {
@@ -290,7 +312,7 @@ const useCreateMonitor = () => {
return [createMonitor, isLoading];
};
-const useFetchGlobalSettings = () => {
+export const useFetchGlobalSettings = () => {
const [isLoading, setIsLoading] = useState(true);
const [globalSettings, setGlobalSettings] = useState(undefined);
useEffect(() => {
@@ -312,7 +334,7 @@ const useFetchGlobalSettings = () => {
return [globalSettings, isLoading];
};
-const useDeleteMonitor = () => {
+export const useDeleteMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const deleteMonitor = async ({ monitor, redirect }) => {
@@ -333,7 +355,7 @@ const useDeleteMonitor = () => {
return [deleteMonitor, isLoading];
};
-const useUpdateMonitor = () => {
+export const useUpdateMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const updateMonitor = async ({ monitor, redirect }) => {
@@ -380,7 +402,7 @@ const useUpdateMonitor = () => {
return [updateMonitor, isLoading];
};
-const usePauseMonitor = () => {
+export const usePauseMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const pauseMonitor = async ({ monitorId, triggerUpdate }) => {
@@ -403,7 +425,7 @@ const usePauseMonitor = () => {
return [pauseMonitor, isLoading, error];
};
-const useAddDemoMonitors = () => {
+export const useAddDemoMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const addDemoMonitors = async () => {
@@ -420,7 +442,7 @@ const useAddDemoMonitors = () => {
return [addDemoMonitors, isLoading];
};
-const useDeleteAllMonitors = () => {
+export const useDeleteAllMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const deleteAllMonitors = async () => {
@@ -437,7 +459,7 @@ const useDeleteAllMonitors = () => {
return [deleteAllMonitors, isLoading];
};
-const useDeleteMonitorStats = () => {
+export const useDeleteMonitorStats = () => {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const deleteMonitorStats = async () => {
@@ -455,7 +477,7 @@ const useDeleteMonitorStats = () => {
return [deleteMonitorStats, isLoading];
};
-const useCreateBulkMonitors = () => {
+export const useCreateBulkMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const createBulkMonitors = async (file, user) => {
@@ -478,7 +500,7 @@ const useCreateBulkMonitors = () => {
return [createBulkMonitors, isLoading];
};
-const useExportMonitors = () => {
+export const useExportMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
@@ -511,7 +533,7 @@ const useExportMonitors = () => {
return [exportMonitors, isLoading];
};
-const useFetchJson = () => {
+export const useFetchJson = () => {
const [isLoading, setIsLoading] = useState(false);
const fetchJson = async () => {
try {
@@ -527,25 +549,3 @@ const useFetchJson = () => {
};
return [fetchJson, isLoading];
};
-
-export {
- useFetchMonitorsWithSummary,
- useFetchMonitorsWithChecks,
- useFetchMonitorsByTeamId,
- useFetchStatsByMonitorId,
- useFetchMonitorById,
- useFetchUptimeMonitorById,
- useFetchHardwareMonitorById,
- useCreateMonitor,
- useFetchGlobalSettings,
- useDeleteMonitor,
- useUpdateMonitor,
- usePauseMonitor,
- useAddDemoMonitors,
- useDeleteAllMonitors,
- useDeleteMonitorStats,
- useCreateBulkMonitors,
- useExportMonitors,
- useFetchMonitorGames,
- useFetchJson,
-};
diff --git a/client/src/Pages/PageSpeed/Details/index.jsx b/client/src/Pages/PageSpeed/Details/index.jsx
index 8a87133df..df411048a 100644
--- a/client/src/Pages/PageSpeed/Details/index.jsx
+++ b/client/src/Pages/PageSpeed/Details/index.jsx
@@ -11,14 +11,13 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
import { useTheme } from "@emotion/react";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import { useParams } from "react-router-dom";
-import { useFetchStatsByMonitorId } from "../../../Hooks/monitorHooks.js";
+import { useFetchPageSpeedMonitorById } from "../../../Hooks/monitorHooks.js";
import { useState } from "react";
import { useTranslation } from "react-i18next";
// Constants
const BREADCRUMBS = [
{ name: "pagespeed", path: "/pagespeed" },
{ name: "details", path: `` },
- // { name: "details", path: `/pagespeed/${monitorId}` }, // Not needed?
];
const PageSpeedDetails = () => {
@@ -36,13 +35,9 @@ const PageSpeedDetails = () => {
});
const [trigger, setTrigger] = useState(false);
// Network
- const [monitor, audits, isLoading, networkError] = useFetchStatsByMonitorId({
+ const [monitor, isLoading, networkError] = useFetchPageSpeedMonitorById({
monitorId,
- sortOrder: "desc",
- limit: 50,
dateRange: "day",
- numToDisplay: null,
- normalize: null,
updateTrigger: trigger,
});
@@ -116,7 +111,7 @@ const PageSpeedDetails = () => {
/>
);
diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js
index a736a1c3b..bd0e37b30 100644
--- a/client/src/Utils/NetworkService.js
+++ b/client/src/Utils/NetworkService.js
@@ -152,7 +152,8 @@ class NetworkService {
params.append("type", type);
});
}
- if (filter !== undefined && filter !== null && filter !== "") params.append("filter", filter);
+ if (filter !== undefined && filter !== null && filter !== "")
+ params.append("filter", filter);
return this.axiosInstance.get(`/monitors/team?${params.toString()}`, {
headers: {
@@ -197,6 +198,14 @@ class NetworkService {
`/monitors/hardware/details/${config.monitorId}?${params.toString()}`
);
}
+ async getPageSpeedDetailsByMonitorId(config) {
+ const params = new URLSearchParams();
+ if (config.dateRange) params.append("dateRange", config.dateRange);
+
+ return this.axiosInstance.get(
+ `/monitors/pagespeed/details/${config.monitorId}?${params.toString()}`
+ );
+ }
async getUptimeDetailsById(config) {
const params = new URLSearchParams();
if (config.dateRange) params.append("dateRange", config.dateRange);
diff --git a/server/src/controllers/controllerUtils.ts b/server/src/controllers/controllerUtils.ts
index 4d9f9a279..af3eddd44 100755
--- a/server/src/controllers/controllerUtils.ts
+++ b/server/src/controllers/controllerUtils.ts
@@ -1,3 +1,6 @@
+import { AppError } from "@/utils/AppError.js";
+import { type MonitorType, MonitorTypes } from "@/types/index.js";
+
const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise => {
const monitorUrl = new URL(monitor.url);
const hostname = monitorUrl.hostname;
@@ -8,5 +11,101 @@ const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise {
+ if (typeof value === "string" && value.trim().length > 0) {
+ return value;
+ }
+ throw new AppError({ message: `${fieldName} is required`, status: 400 });
+};
-export { fetchMonitorCertificate };
+const optionalString = (value: unknown, fieldName: string): string | undefined => {
+ if (value === undefined) {
+ return undefined;
+ }
+ if (typeof value === "string") {
+ return value;
+ }
+ throw new AppError({ message: `${fieldName} must be a string`, status: 400 });
+};
+
+const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
+ if (value === undefined) {
+ return undefined;
+ }
+ if (typeof value === "number" && Number.isFinite(value)) {
+ return value;
+ }
+ if (typeof value === "string" && value.trim() !== "") {
+ const parsed = Number(value);
+ if (!Number.isNaN(parsed)) {
+ return parsed;
+ }
+ }
+ throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
+};
+
+const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
+ if (value === undefined) {
+ return undefined;
+ }
+ if (typeof value === "boolean") {
+ return value;
+ }
+ if (typeof value === "string") {
+ if (value === "true") {
+ return true;
+ }
+ if (value === "false") {
+ return false;
+ }
+ }
+ throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
+};
+
+const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
+ const parseSingle = (input: unknown): MonitorType => {
+ if (typeof input !== "string") {
+ throw new AppError({ message: "Monitor type must be a string", status: 400 });
+ }
+ if (!MonitorTypes.includes(input as MonitorType)) {
+ throw new AppError({ message: `Invalid monitor type: ${input}`, status: 400 });
+ }
+ return input as MonitorType;
+ };
+
+ if (value === undefined) {
+ return undefined;
+ }
+ if (Array.isArray(value)) {
+ return value.map((entry) => parseSingle(entry));
+ }
+ return parseSingle(value);
+};
+
+const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
+ if (value === undefined) {
+ return undefined;
+ }
+ if (value === "asc" || value === "desc") {
+ return value;
+ }
+ throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 });
+};
+
+const requireTeamId = (teamId?: string): string => {
+ if (!teamId) {
+ throw new AppError({ message: "Team ID is required", status: 400 });
+ }
+ return teamId;
+};
+
+export {
+ fetchMonitorCertificate,
+ requireString,
+ optionalString,
+ optionalNumber,
+ optionalBoolean,
+ parseMonitorTypeFilter,
+ parseSortOrder,
+ requireTeamId,
+};
diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts
index 9124abd06..90ab5e7a5 100644
--- a/server/src/controllers/monitorController.ts
+++ b/server/src/controllers/monitorController.ts
@@ -14,16 +14,26 @@ import {
getHardwareDetailsByIdQueryValidation,
} from "@/validation/joi.js";
import sslChecker from "ssl-checker";
-import { fetchMonitorCertificate } from "./controllerUtils.js";
+import {
+ fetchMonitorCertificate,
+ requireString,
+ optionalString,
+ optionalNumber,
+ optionalBoolean,
+ parseMonitorTypeFilter,
+ parseSortOrder,
+ requireTeamId,
+} from "./controllerUtils.js";
import { AppError } from "@/utils/AppError.js";
+import { IMonitorService } from "@/service/index.js";
const SERVICE_NAME = "monitorController";
class MonitorController {
static SERVICE_NAME = SERVICE_NAME;
- private monitorService: any;
+ private monitorService: IMonitorService;
- constructor(monitorService: any) {
+ constructor(monitorService: IMonitorService) {
this.monitorService = monitorService;
}
@@ -32,8 +42,8 @@ class MonitorController {
}
async verifyTeamAccess(teamId: string, monitorId: string) {
- const monitor = await this.monitorService.getMonitorById(monitorId);
- if (!monitor.teamId.equals(teamId)) {
+ const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
+ if (monitor.teamId !== teamId) {
throw new AppError({ message: "Access denied", status: 403 });
}
}
@@ -41,11 +51,8 @@ class MonitorController {
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
try {
await getCertificateParamValidation.validateAsync(req.params);
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new AppError({ message: "Team ID is required", status: 400 });
- }
- const { monitorId } = req.params;
+ const teamId = requireTeamId(req?.user?.teamId);
+ const monitorId = requireString(req.params?.monitorId, "Monitor ID");
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
const certificate = await fetchMonitorCertificate(sslChecker, monitor);
@@ -63,15 +70,10 @@ class MonitorController {
getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => {
try {
- const monitorId = req?.params?.monitorId;
- const dateRange = req?.query?.dateRange;
- const normalize = req?.query?.normalize;
-
- const teamId = req?.user?.teamId;
-
- if (!teamId) {
- throw new AppError({ message: "Team ID is required", status: 400 });
- }
+ const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
+ const dateRange = requireString(req?.query?.dateRange, "dateRange");
+ const normalize = optionalBoolean(req?.query?.normalize, "normalize");
+ const teamId = requireTeamId(req?.user?.teamId);
const data = await this.monitorService.getUptimeDetailsById({
teamId,
@@ -94,12 +96,9 @@ class MonitorController {
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
- const monitorId = req?.params?.monitorId;
- const dateRange = req?.query?.dateRange;
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new AppError({ message: "Team ID is required", status: 400 });
- }
+ const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
+ const dateRange = requireString(req?.query?.dateRange, "dateRange");
+ const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.getHardwareDetailsById({
teamId,
@@ -116,18 +115,40 @@ class MonitorController {
next(error);
}
};
+ getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
+ await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
+
+ const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
+ const dateRange = requireString(req?.query?.dateRange, "dateRange");
+ const teamId = requireTeamId(req?.user?.teamId);
+
+ const monitor = await this.monitorService.getPageSpeedDetailsById({
+ teamId,
+ monitorId,
+ dateRange,
+ });
+
+ return res.status(200).json({
+ success: true,
+ msg: "Page speed details retrieved successfully",
+ data: monitor,
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
getMonitorById = async (req: Request, res: Response, next: NextFunction) => {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
await getMonitorByIdQueryValidation.validateAsync(req.query);
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new AppError({ message: "Team ID is required", status: 400 });
- }
+ const teamId = requireTeamId(req?.user?.teamId);
+ const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
- const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: req?.params?.monitorId });
+ const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
return res.status(200).json({
success: true,
@@ -143,8 +164,8 @@ class MonitorController {
try {
await createMonitorBodyValidation.validateAsync(req.body);
- const userId = req?.user?._id;
- const teamId = req?.user?.teamId;
+ const userId = requireString(req?.user?._id, "User ID");
+ const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.createMonitor(teamId, userId, req.body);
@@ -172,12 +193,8 @@ class MonitorController {
throw new AppError({ message: "File is empty", status: 400 });
}
- const userId = req?.user?._id;
- const teamId = req?.user?.teamId;
-
- if (!userId || !teamId) {
- throw new AppError({ message: "Missing userId or teamId", status: 400 });
- }
+ const userId = requireString(req?.user?._id, "User ID");
+ const teamId = requireTeamId(req?.user?.teamId);
const fileData = req?.file?.buffer?.toString("utf-8");
if (!fileData) {
@@ -199,11 +216,8 @@ class MonitorController {
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
- const monitorId = req.params.monitorId;
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new AppError({ message: "Team ID is required", status: 400 });
- }
+ const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
+ const teamId = requireTeamId(req?.user?.teamId);
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
@@ -219,10 +233,7 @@ class MonitorController {
deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => {
try {
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new AppError({ message: "Team ID is required", status: 400 });
- }
+ const teamId = requireTeamId(req?.user?.teamId);
const deletedCount = await this.monitorService.deleteAllMonitors({ teamId });
@@ -239,12 +250,8 @@ class MonitorController {
try {
await getMonitorByIdParamValidation.validateAsync(req.params);
await editMonitorBodyValidation.validateAsync(req.body);
- const monitorId = req?.params?.monitorId;
-
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new AppError({ message: "Team ID is required", status: 400 });
- }
+ const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
+ const teamId = requireTeamId(req?.user?.teamId);
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body });
@@ -262,11 +269,8 @@ class MonitorController {
try {
await pauseMonitorParamValidation.validateAsync(req.params);
- const monitorId = req.params.monitorId;
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new AppError({ message: "Team ID is required", status: 400 });
- }
+ const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
+ const teamId = requireTeamId(req?.user?.teamId);
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
@@ -282,7 +286,8 @@ class MonitorController {
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
try {
- const { _id, teamId } = req.user;
+ const _id = requireString(req?.user?._id, "User ID");
+ const teamId = requireTeamId(req?.user?.teamId);
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId });
return res.status(200).json({
@@ -318,8 +323,9 @@ class MonitorController {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
- const { type, filter } = req.query;
- const teamId = req?.user?.teamId;
+ const teamId = requireTeamId(req?.user?.teamId);
+ const type = parseMonitorTypeFilter(req.query?.type);
+ const filter = optionalString(req.query?.filter, "filter");
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter });
@@ -338,12 +344,9 @@ class MonitorController {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
- const explain = req?.query?.explain;
- const type = req?.query?.type;
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new AppError({ message: "Team ID is required", status: 400 });
- }
+ const explain = optionalBoolean(req?.query?.explain, "explain");
+ const type = parseMonitorTypeFilter(req?.query?.type);
+ const teamId = requireTeamId(req?.user?.teamId);
const result = await this.monitorService.getMonitorsAndSummaryByTeamId({ teamId, type, explain });
@@ -361,12 +364,15 @@ class MonitorController {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsWithChecksQueryValidation.validateAsync(req.query);
- const explain = req?.query?.explain;
- let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
- const teamId = req?.user?.teamId;
- if (!teamId) {
- throw new AppError({ message: "Team ID is required", status: 400 });
- }
+ const explain = optionalBoolean(req?.query?.explain, "explain");
+ const limit = optionalNumber(req?.query?.limit, "limit");
+ const page = optionalNumber(req?.query?.page, "page");
+ const rowsPerPage = optionalNumber(req?.query?.rowsPerPage, "rowsPerPage");
+ const filter = optionalString(req?.query?.filter, "filter");
+ const field = optionalString(req?.query?.field, "field");
+ const order = parseSortOrder(req?.query?.order);
+ const type = parseMonitorTypeFilter(req?.query?.type);
+ const teamId = requireTeamId(req?.user?.teamId);
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
teamId,
diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts
index 507389e6a..cedea8d86 100644
--- a/server/src/repositories/checks/IChecksRepository.ts
+++ b/server/src/repositories/checks/IChecksRepository.ts
@@ -1,6 +1,58 @@
-import type { Check, MonitorType } from "@/types/index.js";
+import type { Check, CheckAudits, MonitorType } from "@/types/index.js";
import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js";
+export interface PageSpeedChecksResult {
+ monitorType: "pagespeed";
+ checks: Check[];
+}
+
+export interface HardwareChecksResult {
+ monitorType: "hardware";
+ aggregateData: {
+ latestCheck: Check | null;
+ totalChecks: number;
+ };
+ upChecks: {
+ totalChecks: number;
+ };
+ checks: Array<{
+ _id: string;
+ avgCpuUsage: number;
+ avgMemoryUsage: number;
+ avgTemperature: number[];
+ disks: Array<{
+ name: string;
+ readSpeed: number;
+ writeSpeed: number;
+ totalBytes: number;
+ freeBytes: number;
+ usagePercent: number;
+ }>;
+ net: Array<{
+ name: string;
+ bytesSentPerSecond: number;
+ deltaBytesRecv: number;
+ deltaPacketsSent: number;
+ deltaPacketsRecv: number;
+ deltaErrIn: number;
+ deltaErrOut: number;
+ deltaDropIn: number;
+ deltaDropOut: number;
+ deltaFifoIn: number;
+ deltaFifoOut: number;
+ }>;
+ }>;
+}
+
+export interface UptimeChecksResult {
+ monitorType: Exclude;
+ groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>;
+ groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
+ groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
+ uptimePercentage: number;
+ avgResponseTime: number;
+}
+
export interface IChecksRepository {
findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise;
findDateRangeChecksByMonitor(
@@ -9,51 +61,5 @@ export interface IChecksRepository {
endDate: Date,
dateString: string,
options?: { type?: MonitorType }
- ): Promise<
- | {
- monitorType: "uptime";
- groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>;
- groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
- groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
- uptimePercentage: number;
- avgResponseTime: number;
- }
- | {
- monitorType: "hardware";
- aggregateData: {
- latestCheck: Check | null;
- totalChecks: number;
- };
- upChecks: {
- totalChecks: number;
- };
- checks: Array<{
- _id: string;
- avgCpuUsage: number;
- avgMemoryUsage: number;
- avgTemperature: number[];
- disks: Array<{
- name: string;
- readSpeed: number;
- writeSpeed: number;
- totalBytes: number;
- freeBytes: number;
- usagePercent: number;
- }>;
- net: Array<{
- name: string;
- bytesSentPerSecond: number;
- deltaBytesRecv: number;
- deltaPacketsSent: number;
- deltaPacketsRecv: number;
- deltaErrIn: number;
- deltaErrOut: number;
- deltaDropIn: number;
- deltaDropOut: number;
- deltaFifoIn: number;
- deltaFifoOut: number;
- }>;
- }>;
- }
- >;
+ ): Promise;
}
diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts
index 2aeb32b2d..d9466b4d8 100644
--- a/server/src/repositories/checks/MongoChecksRepistory.ts
+++ b/server/src/repositories/checks/MongoChecksRepistory.ts
@@ -11,6 +11,7 @@ import type {
CheckMetadata,
CheckNetworkInterfaceInfo,
CheckTimings,
+ MonitorType,
} from "@/types/index.js";
import { CheckModel, type CheckDocument } from "@/db/models/index.js";
import mongoose from "mongoose";
@@ -177,10 +178,7 @@ class MongoChecksRepistory implements IChecksRepository {
};
};
- findLatestChecksByMonitorIds = async (
- monitorIds: string[],
- options?: { limitPerMonitor?: number }
- ): Promise => {
+ findLatestChecksByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise => {
if (monitorIds.length === 0) {
return {};
}
@@ -218,15 +216,24 @@ class MongoChecksRepistory implements IChecksRepository {
}, {});
};
- findDateRangeChecksByMonitor = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: string }) => {
+ findDateRangeChecksByMonitor = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: MonitorType }) => {
const monitorObjectId = new mongoose.Types.ObjectId(monitorId);
if (options?.type === "hardware") {
return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
}
- return this.findUptimeDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
+ if (options?.type === "pagespeed") {
+ return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate);
+ }
+ return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString);
};
- private findUptimeDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date, dateString: string) => {
+ private findUptimeDateRangeChecks = async (
+ monitorType: Exclude,
+ monitorObjectId: mongoose.Types.ObjectId,
+ startDate: Date,
+ endDate: Date,
+ dateString: string
+ ) => {
const matchStage = {
"metadata.monitorId": monitorObjectId,
updatedAt: { $gte: startDate, $lte: endDate },
@@ -307,7 +314,7 @@ class MongoChecksRepistory implements IChecksRepository {
const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0;
return {
- monitorType: "uptime" as const,
+ monitorType,
groupedChecks: result?.groupedChecks ?? [],
groupedUpChecks: result?.groupedUpChecks ?? [],
groupedDownChecks: result?.groupedDownChecks ?? [],
@@ -369,6 +376,19 @@ class MongoChecksRepistory implements IChecksRepository {
checks,
};
};
+
+ private findPageSpeedDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date) => {
+ const matchStage = {
+ "metadata.monitorId": monitorObjectId,
+ createdAt: { $gte: startDate, $lte: endDate },
+ };
+
+ const checks = await CheckModel.find(matchStage).sort({ createdAt: -1 }).limit(25).lean();
+ return {
+ monitorType: "pagespeed" as const,
+ checks: checks.map((doc) => this.toEntity(doc)),
+ };
+ };
}
export default MongoChecksRepistory;
diff --git a/server/src/routes/v1/monitorRoute.ts b/server/src/routes/v1/monitorRoute.ts
index bcbf5a1c6..24bc64ec8 100755
--- a/server/src/routes/v1/monitorRoute.ts
+++ b/server/src/routes/v1/monitorRoute.ts
@@ -27,6 +27,8 @@ class MonitorRoutes {
// Hardware routes
this.router.get("/hardware/details/:monitorId", this.monitorController.getHardwareDetailsById);
+ // PageSpeed routes
+ this.router.get("/pagespeed/details/:monitorId", this.monitorController.getPageSpeedDetailsById);
// General monitor routes
this.router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.pauseMonitor);
diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts
index 46731446f..3793ac35e 100644
--- a/server/src/service/business/monitorService.ts
+++ b/server/src/service/business/monitorService.ts
@@ -24,6 +24,7 @@ export interface IMonitorService {
// read
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise;
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise;
+ getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise;
getMonitorById(args: { teamId: string; monitorId: string }): Promise;
getMonitorsByTeamId(args: {
teamId: string;
@@ -268,7 +269,13 @@ export class MonitorService implements IMonitorService {
});
const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id);
- if (checksData.monitorType !== "uptime") {
+ if (
+ checksData.monitorType !== "http" &&
+ checksData.monitorType !== "ping" &&
+ checksData.monitorType !== "docker" &&
+ checksData.monitorType !== "port" &&
+ checksData.monitorType !== "game"
+ ) {
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
}
@@ -317,6 +324,30 @@ export class MonitorService implements IMonitorService {
};
};
+ getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => {
+ await this.verifyTeamAccess({ teamId, monitorId });
+ const monitor = await this.monitorsRepository.findById(monitorId, teamId);
+ if (!monitor) {
+ throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
+ }
+ if (monitor.type !== "pagespeed") {
+ throw new AppError({ message: `${monitor.type} monitors are not supported for pagespeed details`, status: 400 });
+ }
+
+ const rangeKey = (dateRange as DateRangeKey) ?? "recent";
+ const { start, end } = this.getDateRange(rangeKey);
+ const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), {
+ type: monitor.type,
+ });
+
+ if (checksData.monitorType !== "pagespeed") {
+ throw new AppError({ message: "Unable to load pagespeed stats for this monitor", status: 500 });
+ }
+ return {
+ ...monitor,
+ checks: checksData.checks,
+ };
+ };
getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.monitorsRepository.findById(monitorId, teamId);