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