diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx index bb4399b85..a86565f49 100644 --- a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx +++ b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx @@ -66,7 +66,7 @@ const MonitorDetailsControlHeader = ({ gap={theme.spacing(2)} > @@ -78,7 +78,7 @@ const MonitorDetailsControlHeader = ({ startIcon={} disabled={isTestNotificationsDisabled} onClick={() => { - testAllNotifications({ monitorId: monitor?._id }); + testAllNotifications({ monitorId: monitor?.id }); }} sx={{ whiteSpace: "nowrap", @@ -92,7 +92,7 @@ const MonitorDetailsControlHeader = ({ variant="contained" color="secondary" onClick={(e) => { - navigate(`/incidents/${monitor?._id}`); + navigate(`/incidents/${monitor?.id}`); }} > {t("menu.incidents")} @@ -107,7 +107,7 @@ const MonitorDetailsControlHeader = ({ } onClick={() => { pauseMonitor({ - monitorId: monitor?._id, + monitorId: monitor?.id, triggerUpdate, }); }} @@ -120,7 +120,7 @@ const MonitorDetailsControlHeader = ({ variant="contained" color="secondary" startIcon={} - onClick={() => navigate(`/${path}/configure/${monitor._id}`)} + onClick={() => navigate(`/${path}/configure/${monitor.id}`)} > Configure diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index bc391ebf6..d865ffab8 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -50,6 +50,7 @@ export const useFetchMonitorsWithChecks = ({ const [isLoading, setIsLoading] = useState(false); const [count, setCount] = useState(undefined); const [monitors, setMonitors] = useState(undefined); + const [summary, setSummary] = useState(undefined); const [networkError, setNetworkError] = useState(false); const theme = useTheme(); @@ -68,10 +69,11 @@ export const useFetchMonitorsWithChecks = ({ order, }); - const { count, monitors } = res?.data?.data ?? {}; + const { count, monitors, summary } = res?.data?.data ?? {}; const mappedMonitors = monitors.map((monitor) => getMonitorWithPercentage(monitor, theme) ); + setSummary(summary); setMonitors(mappedMonitors); setCount(count || 0); } catch (error) { @@ -97,7 +99,7 @@ export const useFetchMonitorsWithChecks = ({ types, monitorUpdateTrigger, ]); - return [monitors, count, isLoading, networkError]; + return [summary, monitors, count, isLoading, networkError]; }; export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => { @@ -385,7 +387,7 @@ export const useUpdateMonitor = () => { }), }; await networkService.updateMonitor({ - monitorId: monitor._id, + monitorId: monitor.id, updatedFields, }); diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx index b25636fa9..e4add415c 100644 --- a/client/src/Pages/Infrastructure/Monitors/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/index.jsx @@ -71,7 +71,7 @@ const InfrastructureMonitors = () => { const field = toFilterStatus !== undefined ? "status" : undefined; - const [monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({ + const [summary, monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({ types: TYPES, limit: 1, page: page, diff --git a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx index 6b0a800fb..8708502cd 100644 --- a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx +++ b/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx @@ -2,6 +2,7 @@ import { Grid, Grid2 } from "@mui/material"; import Card from "../Card/index.jsx"; const MonitorGrid = ({ shouldRender, monitors }) => { + console.log(monitors); return ( { const isAdmin = useIsAdmin(); const [ + summary, monitorsWithChecks, monitorsWithChecksCount, monitorsWithChecksIsLoading, diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 4f847db4e..2c7821571 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -199,7 +199,7 @@ const UptimeCreate = ({ isClone = false }) => { }; } else { form = { - _id: monitor._id, + id: monitor.id, url: monitor.url, name: monitor.name || monitor.url.substring(0, 50), statusWindowSize: monitor.statusWindowSize, @@ -292,7 +292,7 @@ const UptimeCreate = ({ isClone = false }) => { const handleRemove = async (event) => { event.preventDefault(); - const TEMP_MONITOR = { id: monitor._id }; + const TEMP_MONITOR = { id: monitor.id }; await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/uptime" }); }; diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index fb8d6246b..101020eea 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -27,8 +27,8 @@ import { useSelector, useDispatch } from "react-redux"; import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; import PropTypes from "prop-types"; import { - useFetchMonitorsWithSummary, useFetchMonitorsWithChecks, + useFetchMonitorsByTeamId, } from "@/Hooks/monitorHooks.js"; import { useTranslation } from "react-i18next"; @@ -103,12 +103,6 @@ const UptimeMonitors = () => { setMonitorUpdateTrigger((prev) => !prev); }, []); - const [monitors, monitorsSummary, monitorsWithSummaryIsLoading, networkError] = - useFetchMonitorsWithSummary({ - types: TYPES, - monitorUpdateTrigger, - }); - const handleReset = () => { setSelectedState(undefined); setSelectedTypes(undefined); @@ -123,16 +117,17 @@ const UptimeMonitors = () => { ]); const activeFilter = [...filterLookup].find(([key]) => key !== undefined); - const field = activeFilter?.[1] || sort?.field; + const field = activeFilter?.[1] || (search ? "name" : sort?.field); const filter = activeFilter?.[0] || search; const effectiveTypes = selectedTypes?.length ? selectedTypes : TYPES; const [ + summary, monitorsWithChecks, monitorsWithChecksCount, monitorsWithChecksIsLoading, - monitorsWithChecksNetworkError, + networkError, ] = useFetchMonitorsWithChecks({ types: effectiveTypes, limit: 25, @@ -144,13 +139,18 @@ const UptimeMonitors = () => { monitorUpdateTrigger, }); + const [monitors, listIsLoading, listNetworkError] = useFetchMonitorsByTeamId({ + type: ["http", "ping", "docker", "port", "game"], + }); + + console.log(monitors); useEffect(() => { if (isSearching) { setPage(undefined); } }, [isSearching]); - const isLoading = monitorsWithSummaryIsLoading || monitorsWithChecksIsLoading; + const isLoading = monitorsWithChecksIsLoading; return ( <> @@ -174,14 +174,14 @@ const UptimeMonitors = () => { /> { const serviceRegistry = new ServiceRegistry({ logger }); @@ -124,6 +124,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService const monitorsRepository = new MongoMonitorsRepository(); const checksRepository = new MongoChecksRepository(); const monitorStatsRepository = new MongoMonitorStatsRepository(); + const statusPagesRepository = new MongoStatusPagesRepository(); const networkService = new NetworkService({ axios, @@ -217,7 +218,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService errorService, }); const monitorService = new MonitorService({ - db, jobQueue: superSimpleQueue, stringService, emailService, @@ -228,6 +228,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService monitorsRepository, checksRepository, monitorStatsRepository, + statusPagesRepository, }); const services = { diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 90ab5e7a5..d46845026 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -339,26 +339,6 @@ class MonitorController { } }; - getMonitorsAndSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => { - try { - await getMonitorsByTeamIdParamValidation.validateAsync(req.params); - await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - - 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 }); - - return res.status(200).json({ - msg: "Monitors and summary retrieved successfully", - data: result, - }); - } catch (error) { - next(error); - } - }; - getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); @@ -394,21 +374,6 @@ class MonitorController { next(error); } }; - exportMonitorsToCSV = 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 csv = await this.monitorService.exportMonitorsToCSV({ teamId }); - res.setHeader("Content-Type", "text/csv"); - res.setHeader("Content-Disposition", "attachment; filename=monitors.csv"); - return res.send(csv); - } catch (error) { - next(error); - } - }; exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => { try { diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts index f6f9d4371..17565a2e4 100644 --- a/server/src/db/models/Monitor.ts +++ b/server/src/db/models/Monitor.ts @@ -171,35 +171,6 @@ const MonitorSchema = new Schema( } ); -MonitorSchema.pre("findOneAndDelete", async function (next) { - try { - const doc = await this.model.findOne(this.getFilter()); - - if (!doc) { - throw new Error("Monitor not found"); - } - - await Check.deleteMany({ monitorId: doc._id }); - await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } }); - await MonitorStats.deleteMany({ monitorId: doc?._id.toString() }); - next(); - } catch (error) { - next(error as Error); - } -}); - -MonitorSchema.pre("deleteMany", async function (next) { - const filter = this.getFilter(); - const monitors = (await this.model.find(filter).select(["_id", "type"]).lean()) as { _id: Types.ObjectId }[]; - - for (const monitor of monitors) { - await Check.deleteMany({ monitorId: monitor._id }); - await StatusPage.updateMany({ monitors: monitor._id }, { $pull: { monitors: monitor._id } }); - await MonitorStats.deleteMany({ monitorId: monitor._id.toString() }); - } - next(); -}); - MonitorSchema.pre("save", function (next) { if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) { this.cpuAlertThreshold = this.alertThreshold; diff --git a/server/src/db/models/StatusPage.js b/server/src/db/models/StatusPage.js deleted file mode 100755 index 621749fb8..000000000 --- a/server/src/db/models/StatusPage.js +++ /dev/null @@ -1,85 +0,0 @@ -import mongoose from "mongoose"; - -const StatusPageSchema = mongoose.Schema( - { - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - immutable: true, - required: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - required: true, - }, - type: { - type: String, - required: true, - default: "uptime", - enum: ["uptime"], - }, - companyName: { - type: String, - required: true, - default: "", - }, - url: { - type: String, - unique: true, - required: true, - default: "", - }, - timezone: { - type: String, - required: false, - }, - color: { - type: String, - required: false, - default: "#4169E1", - }, - monitors: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - required: true, - }, - ], - subMonitors: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - required: true, - }, - ], - logo: { - data: Buffer, - contentType: String, - }, - isPublished: { - type: Boolean, - default: false, - }, - showCharts: { - type: Boolean, - default: true, - }, - showUptimePercentage: { - type: Boolean, - default: true, - }, - showAdminLoginLink: { - type: Boolean, - default: false, - }, - customCSS: { - type: String, - default: "", - }, - }, - { timestamps: true } -); - -export default mongoose.model("StatusPage", StatusPageSchema); diff --git a/server/src/db/models/StatusPage.ts b/server/src/db/models/StatusPage.ts new file mode 100644 index 000000000..42d11bc46 --- /dev/null +++ b/server/src/db/models/StatusPage.ts @@ -0,0 +1,115 @@ +import { Schema, model, type Types } from "mongoose"; +import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js"; +import { StatusPageTypes } from "@/types/statusPage.js"; + +type StatusPageDocumentBase = Omit< + StatusPage, + "id" | "userId" | "teamId" | "monitors" | "subMonitors" | "originalMonitors" | "createdAt" | "updatedAt" +> & { + monitors: Types.ObjectId[]; + subMonitors: Types.ObjectId[]; + originalMonitors?: Types.ObjectId[]; + logo?: StatusPageLogo | null; +}; + +interface StatusPageDocument extends StatusPageDocumentBase { + _id: Types.ObjectId; + userId: Types.ObjectId; + teamId: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const logoSchema = new Schema( + { + data: { type: Buffer }, + contentType: { type: String }, + }, + { _id: false } +); + +const StatusPageSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + ref: "User", + immutable: true, + required: true, + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, + }, + type: { + type: String, + required: true, + default: "uptime", + enum: StatusPageTypes, + }, + companyName: { + type: String, + required: true, + default: "", + }, + url: { + type: String, + unique: true, + required: true, + default: "", + }, + timezone: { + type: String, + }, + color: { + type: String, + default: "#4169E1", + }, + monitors: [ + { + type: Schema.Types.ObjectId, + ref: "Monitor", + required: true, + }, + ], + subMonitors: [ + { + type: Schema.Types.ObjectId, + ref: "Monitor", + required: true, + }, + ], + logo: { + type: logoSchema, + default: null, + }, + isPublished: { + type: Boolean, + default: false, + }, + showCharts: { + type: Boolean, + default: true, + }, + showUptimePercentage: { + type: Boolean, + default: true, + }, + showAdminLoginLink: { + type: Boolean, + default: false, + }, + customCSS: { + type: String, + default: "", + }, + }, + { timestamps: true } +); + +const StatusPageModel = model("StatusPage", StatusPageSchema); + +export type { StatusPageDocument }; +export { StatusPageModel }; +export default StatusPageModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 3ac99a390..b92ae18b7 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -6,3 +6,6 @@ export { default as CheckModel } from "@/db/models/Check.js"; export * from "@/db/models/MonitorStats.js"; export { default as MonitorStatsModel } from "@/db/models/MonitorStats.js"; + +export * from "@/db/models/StatusPage.js"; +export { default as StatusPageModel } from "@/db/models/StatusPage.js"; diff --git a/server/src/db/modules/monitorModule.js b/server/src/db/modules/monitorModule.js index 84ff86996..e13da4b59 100755 --- a/server/src/db/modules/monitorModule.js +++ b/server/src/db/modules/monitorModule.js @@ -318,23 +318,22 @@ class MonitorModule { } }; - getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => { + findMonitorsSummaryByTeamId = async ({ type, teamId, explain }) => { try { const matchStage = { teamId: new this.ObjectId(teamId) }; if (type !== undefined) { matchStage.type = Array.isArray(type) ? { $in: type } : type; } + const pipeline = buildMonitorsSummaryByTeamIdPipeline({ matchStage }); if (explain === true) { - return this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })).explain("executionStats"); + return this.Monitor.aggregate(pipeline).explain("executionStats"); } - - const queryResult = await this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })); - const { monitors, summary } = queryResult?.[0] ?? {}; - return { monitors, summary }; + const [summary] = await this.Monitor.aggregate(pipeline); + return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 }; } catch (error) { error.service = SERVICE_NAME; - error.method = "getMonitorsAndSummaryByTeamId"; + error.method = "findMonitorsSummaryByTeamId"; throw error; } }; diff --git a/server/src/db/modules/statusPageModule.js b/server/src/db/modules/statusPageModule.js index 999e0bdc9..081d3bea5 100755 --- a/server/src/db/modules/statusPageModule.js +++ b/server/src/db/modules/statusPageModule.js @@ -287,6 +287,7 @@ class StatusPageModule { deleteStatusPagesByMonitorId = async (monitorId) => { try { await this.StatusPage.deleteMany({ monitors: { $in: [monitorId] } }); + await this.StatusPage.deleteMany({ subMonitors: { $in: [monitorId] } }); } catch (error) { error.service = SERVICE_NAME; error.method = "deleteStatusPageByMonitorId"; diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index cedea8d86..19faa0159 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -54,6 +54,9 @@ export interface UptimeChecksResult { } export interface IChecksRepository { + // create + // single fetch + // collection fetch findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; findDateRangeChecksByMonitor( monitorId: string, @@ -62,4 +65,7 @@ export interface IChecksRepository { dateString: string, options?: { type?: MonitorType } ): Promise; + // update + //delete + deleteByMonitorId(monitorId: string): Promise; } diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 7ede5587e..615754933 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -23,7 +23,7 @@ import { export type LatestChecksMap = Record; -class MongoChecksRepistory implements IChecksRepository { +class MongoChecksRepository implements IChecksRepository { private toEntity = (doc: CheckDocument): Check => { const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => { if (!value) { @@ -184,18 +184,13 @@ class MongoChecksRepistory implements IChecksRepository { } const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); const limitPerMonitor = options?.limitPerMonitor ?? 25; - const maxIntervalMs = Number(10 * 60 * 1000); - const bufferMs = Number(maxIntervalMs); - const lookbackMs = limitPerMonitor * maxIntervalMs + bufferMs; - - const cutoffDate = new Date(Date.now() - lookbackMs); const checkGroups = await CheckModel.aggregate([ { $match: { "metadata.monitorId": { $in: mongoIds }, - createdAt: { $gte: cutoffDate }, }, }, + { $sort: { "metadata.monitorId": 1, createdAt: -1 } }, { $group: { _id: "$metadata.monitorId", @@ -390,6 +385,11 @@ class MongoChecksRepistory implements IChecksRepository { checks: checks.map((doc) => this.toEntity(doc)), }; }; + + deleteByMonitorId = async (monitorId: string): Promise => { + const result = await CheckModel.deleteMany({ "metadata.monitorId": monitorId }); + return result.deletedCount; + }; } -export default MongoChecksRepistory; +export default MongoChecksRepository; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 44d64f5bb..2c27591f1 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -6,3 +6,6 @@ export { default as MongoChecksRepository } from "@/repositories/checks/MongoChe export * from "@/repositories/monitor-stats/IMonitorStatsRepository.js"; export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-stats/MongoMonitorStatsRepository.js"; + +export * from "@/repositories/status-pages/IStatusPagesRepository.js"; +export { default as MongoStatusPagesRepository } from "@/repositories/status-pages/MongoStatusPagesRepository.js"; diff --git a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts index a0720619f..08309b4ee 100644 --- a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts +++ b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts @@ -5,5 +5,6 @@ export interface IMonitorStatsRepository { findByMonitorId(monitorId: string): Promise; // update // delete + deleteByMonitorId(monitorId: string): Promise; // other } diff --git a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts index d1d6a4825..52dbe8aa5 100644 --- a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts +++ b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts @@ -39,6 +39,14 @@ class MongoMonitorStatsRepository implements IMonitorStatsRepository { } return this.toEntity(monitorStats); }; + + deleteByMonitorId = async (monitorId: string) => { + const deleted = await MonitorStatsModel.findOneAndDelete({ monitorId: new mongoose.Types.ObjectId(monitorId) }); + if (!deleted) { + throw new AppError({ message: "Monitor stats not found", status: 404 }); + } + return this.toEntity(deleted); + }; } export default MongoMonitorStatsRepository; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 0da9c7279..06c57ddb9 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -10,6 +10,10 @@ export interface TeamQueryConfig { order?: "asc" | "desc"; } +export interface SummaryConfig { + type?: MonitorType | MonitorType[]; +} + export interface IMonitorsRepository { // create create(monitor: Monitor, teamId: string, userId: string): Promise; @@ -20,11 +24,18 @@ export interface IMonitorsRepository { // collection fetch findAll(): Promise; findByTeamId(teamId: string, config: TeamQueryConfig): Promise; + // update - update(monitorId: string, updates: Partial): Promise; + updateById(monitorId: string, teamId: string, updates: Partial): Promise; + togglePauseById(monitorId: string, teamId: string): Promise; // delete + deleteById(monitorId: string, teamId: string): Promise; deleteByTeamId(teamId: string): Promise<{ monitors: Monitor[]; deletedCount: number }>; // counts findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise; + + // other + findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; + findGroupsByTeamId(teamId: string): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index c9af6668d..52f423154 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -1,8 +1,8 @@ import { MonitorModel } from "@/db/models/index.js"; -import type { MonitorDocument } from "@/db/models/Monitor.js"; -import type { Monitor, MonitorType } from "@/types/monitor.js"; +import type { MonitorDocument } from "@/db/models/index.js"; +import type { Monitor } from "@/types/index.js"; import mongoose, { type FilterQuery } from "mongoose"; -import type { IMonitorsRepository, TeamQueryConfig } from "./IMonitorsRepository.js"; +import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import { AppError } from "@/utils/AppError.js"; class MongoMonitorsRepository implements IMonitorsRepository { @@ -44,7 +44,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { - const { page = 0, rowsPerPage = 25, filter, field = "createdAt", order = "desc", type, limit } = config ?? {}; + const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type, limit } = config ?? {}; const query: Record = { teamId: new mongoose.Types.ObjectId(teamId), @@ -75,9 +75,8 @@ class MongoMonitorsRepository implements IMonitorsRepository { const sort = { [field]: order === "asc" ? 1 : -1 } as const; const skip = Math.max(page, 0) * rowsPerPage; - const limitValue = limit ?? rowsPerPage; - const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(limitValue).exec(); + const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(rowsPerPage); return this.mapDocuments(documents); }; @@ -97,9 +96,9 @@ class MongoMonitorsRepository implements IMonitorsRepository { return count; }; - update = async (monitorId: string, patch: Partial) => { + updateById = async (monitorId: string, teamId: string, patch: Partial) => { const updatedMonitor = await MonitorModel.findOneAndUpdate( - { _id: monitorId }, + { _id: monitorId, teamId }, { $set: { ...patch, @@ -113,6 +112,35 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.toEntity(updatedMonitor); }; + togglePauseById = async (monitorId: string, teamId: string) => { + const monitor = await MonitorModel.findOneAndUpdate( + { _id: monitorId, teamId }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + status: "$$REMOVE", + }, + }, + ], + { new: true } + ); + if (!monitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found for the given team.`, status: 404 }); + } + return this.toEntity(monitor); + }; + + deleteById = async (monitorId: string, teamId: string) => { + const deletedMonitor = await MonitorModel.findOneAndDelete({ _id: monitorId, teamId }); + + if (!deletedMonitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found for the given team.`, status: 404 }); + } + + return this.toEntity(deletedMonitor); + }; + deleteByTeamId = async (teamId: string) => { const monitors = await MonitorModel.find({ teamId }); const { deletedCount } = await MonitorModel.deleteMany({ teamId }); @@ -120,6 +148,52 @@ class MongoMonitorsRepository implements IMonitorsRepository { return { monitors: this.mapDocuments(monitors), deletedCount }; }; + findMonitorsSummaryByTeamId = async ( + teamId: string, + config?: SummaryConfig + ): Promise<{ totalMonitors: number; upMonitors: number; downMonitors: number; pausedMonitors: number }> => { + const match: FilterQuery = { teamId: new mongoose.Types.ObjectId(teamId) }; + if (config?.type !== undefined) { + match.type = Array.isArray(config.type) ? { $in: config.type } : config.type; + } + const pipeline = [ + { $match: match }, + { + $group: { + _id: null, + totalMonitors: { $sum: 1 }, + upMonitors: { + $sum: { + $cond: [{ $eq: ["$status", true] }, 1, 0], + }, + }, + downMonitors: { + $sum: { + $cond: [{ $eq: ["$status", false] }, 1, 0], + }, + }, + pausedMonitors: { + $sum: { + $cond: [{ $eq: ["$isActive", false] }, 1, 0], + }, + }, + }, + }, + { $project: { _id: 0 } }, + ]; + + const [summary] = await MonitorModel.aggregate(pipeline); + return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 }; + }; + + findGroupsByTeamId = async (teamId: string): Promise => { + const groups = await MonitorModel.distinct("group", { + teamId: new mongoose.Types.ObjectId(teamId), + group: { $nin: [null, ""] }, + }); + return groups.sort(); + }; + private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { if (!documents?.length) { return []; diff --git a/server/src/repositories/status-pages/IStatusPagesRepository.ts b/server/src/repositories/status-pages/IStatusPagesRepository.ts new file mode 100644 index 000000000..1b9f9303b --- /dev/null +++ b/server/src/repositories/status-pages/IStatusPagesRepository.ts @@ -0,0 +1,11 @@ +import type { StatusPage } from "@/types/statusPage.js"; + +export interface IStatusPagesRepository { + // create + // single fetch + // collection fetch + // update + // delete + // other + removeMonitorFromStatusPages(monitorId: string): Promise; +} diff --git a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts new file mode 100644 index 000000000..6beb23f0e --- /dev/null +++ b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts @@ -0,0 +1,52 @@ +import { IStatusPagesRepository } from "@/repositories/index.js"; +import { type StatusPageDocument, StatusPageModel } from "@/db/models/StatusPage.js"; +import type { StatusPage } from "@/types/statusPage.js"; +import mongoose from "mongoose"; + +class MongoStatusPagesRepository implements IStatusPagesRepository { + private toEntity(doc: StatusPageDocument): StatusPage { + const toStringId = (value: unknown): string => { + if (value instanceof mongoose.Types.ObjectId) { + return value.toString(); + } + return value?.toString() ?? ""; + }; + + const toDateString = (value: Date | string): string => { + return value instanceof Date ? value.toISOString() : value; + }; + + const mapIdArray = (values?: mongoose.Types.ObjectId[]): string[] => { + return values?.map((value) => toStringId(value)) ?? []; + }; + + return { + id: toStringId(doc._id), + userId: toStringId(doc.userId), + teamId: toStringId(doc.teamId), + type: doc.type, + companyName: doc.companyName, + url: doc.url, + timezone: doc.timezone ?? undefined, + color: doc.color, + monitors: mapIdArray(doc.monitors), + subMonitors: mapIdArray(doc.subMonitors), + originalMonitors: doc.originalMonitors?.map((value) => toStringId(value)), + logo: doc.logo ?? undefined, + isPublished: doc.isPublished, + showCharts: doc.showCharts, + showUptimePercentage: doc.showUptimePercentage, + showAdminLoginLink: doc.showAdminLoginLink, + customCSS: doc.customCSS, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + }; + } + + removeMonitorFromStatusPages = async (monitorId: string): Promise => { + const res = await StatusPageModel.updateMany({ monitors: monitorId }, { $pull: { monitors: monitorId } }); + return res.modifiedCount; + }; +} + +export default MongoStatusPagesRepository; diff --git a/server/src/routes/v1/monitorRoute.ts b/server/src/routes/v1/monitorRoute.ts index 24bc64ec8..2e6db630d 100755 --- a/server/src/routes/v1/monitorRoute.ts +++ b/server/src/routes/v1/monitorRoute.ts @@ -19,7 +19,6 @@ class MonitorRoutes { // Team routes this.router.get("/team", this.monitorController.getMonitorsByTeamId); this.router.get("/team/with-checks", this.monitorController.getMonitorsWithChecksByTeamId); - this.router.get("/team/summary", this.monitorController.getMonitorsAndSummaryByTeamId); this.router.get("/team/groups", this.monitorController.getGroupsByTeamId); // Uptime routes @@ -44,7 +43,6 @@ class MonitorRoutes { // Other static routes this.router.post("/demo", isAllowed(["admin", "superadmin"]), this.monitorController.addDemoMonitors); - this.router.get("/export", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToCSV); this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON); this.router.post("/bulk", isAllowed(["admin", "superadmin"]), upload.single("csvFile"), this.monitorController.createBulkMonitors); this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail); diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 3793ac35e..ecdf8ca02 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -2,7 +2,7 @@ import { createMonitorsBodyValidation } from "@/validation/joi.js"; import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js"; import { type Monitor } from "@/types/index.js"; import type { MonitorType } from "@/types/monitor.js"; -import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository } from "@/repositories/index.js"; +import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository, IStatusPagesRepository } from "@/repositories/index.js"; import fs from "fs"; import { fileURLToPath } from "url"; import path from "path"; @@ -14,7 +14,6 @@ type DateRangeKey = "recent" | "day" | "week" | "month" | "all"; export interface IMonitorService { readonly serviceName: string; - verifyTeamAccess(args: { teamId: string; monitorId: string }): Promise; // create createMonitor(teamId: string, userId: string, body: Monitor): Promise; @@ -36,7 +35,6 @@ export interface IMonitorService { field?: string; order?: "asc" | "desc"; }): Promise; - getMonitorsAndSummaryByTeamId(args: { teamId: string; type?: string | string[]; explain?: boolean }): Promise; getMonitorsWithChecksByTeamId(args: { teamId: string; limit?: number; @@ -47,21 +45,20 @@ export interface IMonitorService { field?: string; order?: "asc" | "desc"; explain?: boolean; - }): Promise<{ count: number; monitors: any[] }>; + }): Promise<{ summary: any; count: number; monitors: any[] }>; getAllGames(): any; - getGroupsByTeamId(args: { teamId: string }): Promise; + getGroupsByTeamId(args: { teamId: string }): Promise; // update - editMonitor(args: { teamId: string; monitorId: string; body: any }): Promise; - pauseMonitor(args: { teamId: string; monitorId: string }): Promise; + editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise; + pauseMonitor(args: { teamId: string; monitorId: string }): Promise; // delete - deleteMonitor(args: { teamId: string; monitorId: string }): Promise; + deleteMonitor(args: { teamId: string; monitorId: string }): Promise; deleteAllMonitors(args: { teamId: string }): Promise; // other sendTestEmail(args: { to: string }): Promise; - exportMonitorsToCSV(args: { teamId: string }): Promise; exportMonitorsToJSON(args: { teamId: string }): Promise; } @@ -79,9 +76,9 @@ export class MonitorService implements IMonitorService { private monitorsRepository: IMonitorsRepository; private checksRepository: IChecksRepository; private monitorStatsRepository: IMonitorStatsRepository; + private statusPagesRepository: IStatusPagesRepository; constructor({ - db, jobQueue, stringService, emailService, @@ -92,8 +89,8 @@ export class MonitorService implements IMonitorService { monitorsRepository, checksRepository, monitorStatsRepository, + statusPagesRepository, }: { - db: any; jobQueue: any; stringService: any; emailService: any; @@ -104,8 +101,8 @@ export class MonitorService implements IMonitorService { monitorsRepository: IMonitorsRepository; checksRepository: IChecksRepository; monitorStatsRepository: IMonitorStatsRepository; + statusPagesRepository: IStatusPagesRepository; }) { - this.db = db; this.jobQueue = jobQueue; this.stringService = stringService; this.emailService = emailService; @@ -116,6 +113,7 @@ export class MonitorService implements IMonitorService { this.monitorsRepository = monitorsRepository; this.checksRepository = checksRepository; this.monitorStatsRepository = monitorStatsRepository; + this.statusPagesRepository = statusPagesRepository; } get serviceName(): string { @@ -147,13 +145,6 @@ export class MonitorService implements IMonitorService { return formatLookup[dateRange]; }; - verifyTeamAccess = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - if (!monitor?.teamId?.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - }; - createMonitor = async (teamId: string, userId: string, body: Monitor): Promise => { const monitor = await this.monitorsRepository.create(body, teamId, userId); if (!monitor) { @@ -256,8 +247,6 @@ export class MonitorService implements IMonitorService { dateRange: string; normalize?: boolean; }): 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 }); @@ -293,7 +282,6 @@ export class MonitorService implements IMonitorService { }; getHardwareDetailsById = 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 }); @@ -325,7 +313,6 @@ 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 }); @@ -349,7 +336,6 @@ export class MonitorService implements IMonitorService { }; }; getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); const monitor = await this.monitorsRepository.findById(monitorId, teamId); return monitor; }; @@ -362,23 +348,6 @@ export class MonitorService implements IMonitorService { return monitors; }; - getMonitorsAndSummaryByTeamId = async ({ - teamId, - type, - explain, - }: { - teamId: string; - type?: string | string[]; - explain?: boolean; - }): Promise => { - const result = await this.db.monitorModule.getMonitorsAndSummaryByTeamId({ - type, - explain, - teamId, - }); - return result; - }; - getMonitorsWithChecksByTeamId = async ({ teamId, limit, @@ -399,7 +368,8 @@ export class MonitorService implements IMonitorService { field?: string; order?: "asc" | "desc"; explain?: boolean; - }): Promise<{ count: number; monitors: any[] }> => { + }): Promise<{ summary: any; count: number; monitors: any[] }> => { + const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId); const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter }); const monitors = await this.monitorsRepository.findByTeamId(teamId, { limit, @@ -433,36 +403,36 @@ export class MonitorService implements IMonitorService { }; }); - return { count, monitors: monitorsWithChecks }; + return { summary: summary ?? null, count, monitors: monitorsWithChecks }; }; getAllGames = (): any => { return this.games; }; - getGroupsByTeamId = async ({ teamId }: { teamId: string }): Promise => { - const groups = await this.db.monitorModule.getGroupsByTeamId({ teamId }); + getGroupsByTeamId = async ({ teamId }: { teamId: string }): Promise => { + const groups = await this.monitorsRepository.findGroupsByTeamId(teamId); return groups; }; - editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: any }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); - const editedMonitor = await this.db.monitorModule.editMonitor({ monitorId, body }); + editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Monitor }) => { + const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body); await this.jobQueue.updateJob(editedMonitor); + return editedMonitor; }; - pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.pauseMonitor({ monitorId }); - monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor); + pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + const monitor = await this.monitorsRepository.togglePauseById(monitorId, teamId); + monitor.isActive === true ? await this.jobQueue.resumeJob(monitor) : await this.jobQueue.pauseJob(monitor); return monitor; }; - deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.deleteMonitor({ teamId, monitorId }); + deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + const monitor = await this.monitorsRepository.deleteById(monitorId, teamId); + await this.monitorStatsRepository.deleteByMonitorId(monitor.id); + await this.checksRepository.deleteByMonitorId(monitor.id); + await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id); await this.jobQueue.deleteJob(monitor); - await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id); return monitor; }; @@ -472,9 +442,9 @@ export class MonitorService implements IMonitorService { monitors.map(async (monitor) => { try { await this.jobQueue.deleteJob(monitor); - await this.db.checkModule.deleteChecks(monitor.id); - await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor.id); - await this.db.notificationsModule.deleteNotificationsByMonitorId(monitor.id); + await this.checksRepository.deleteByMonitorId(monitor.id); + await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id); + await this.monitorStatsRepository.deleteByMonitorId(monitor.id); } catch (error: any) { this.logger.warn({ message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`, @@ -502,65 +472,13 @@ export class MonitorService implements IMonitorService { return messageId; }; - exportMonitorsToCSV = async ({ teamId }: { teamId: string }): Promise => { - const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId }); - - if (!monitors || monitors.length === 0) { - throw this.errorService.createNotFoundError("No monitors to export"); - } - - const csvData = monitors?.filteredMonitors?.map((monitor: any) => ({ - name: monitor.name, - description: monitor.description, - type: monitor.type, - url: monitor.url, - interval: monitor.interval, - port: monitor.port, - ignoreTlsErrors: monitor.ignoreTlsErrors, - isActive: monitor.isActive, - })); - - const csv = this.papaparse.unparse(csvData); - return csv; - }; exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise => { - const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId }); + const monitors = await this.monitorsRepository.findByTeamId(teamId, {}); if (!monitors || monitors.length === 0) { throw this.errorService.createNotFoundError("No monitors to export"); } - const json = monitors?.filteredMonitors - ?.map((monitor: any) => { - const initialType = monitor.type; - let parsedType; - - if (initialType === "hardware") { - parsedType = "infrastructure"; - } else if (initialType === "http") { - if (monitor.url.startsWith("https://")) { - parsedType = "https"; - } else { - parsedType = "http"; - } - } else if (initialType === "pagespeed") { - parsedType = initialType; - } else { - // Skip unsupported types - return; - } - - return { - name: monitor.name, - url: monitor.url, - type: parsedType, - interval: monitor.interval, - n: monitor.statusWindowSize, - secret: monitor.secret, - }; - }) - .filter(Boolean); - - return json; + return monitors; }; } diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index e849bbb83..0b2e9f44c 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -5,15 +5,6 @@ const SERVICE_NAME = "StatusService"; class StatusService { static SERVICE_NAME = SERVICE_NAME; - /** - * @param {{ - * db: any - * logger: any - * buffer: import("./bufferService.js").BufferService - * incidentService: import("../business/incidentService.js").IncidentService - * monitorsRepository: any - * }} - */ constructor({ db, logger, buffer, incidentService, monitorsRepository }) { this.db = db; this.logger = logger; @@ -204,7 +195,7 @@ class StatusService { // Return early if not enough data points if (monitor.statusWindow.length < monitor.statusWindowSize) { - const updated = await this.monitorsRepository.update(monitor.id, monitor); + const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor); return { monitor: updated, statusChanged: false, @@ -294,7 +285,7 @@ class StatusService { } monitor.status = newStatus; - const updated = await this.monitorsRepository.update(monitor.id, monitor); + const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor); return { monitor: updated, diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 2367f1c1c..81620da03 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -1,3 +1,4 @@ export * from "@/types/check.js"; export * from "@/types/monitor.js"; export * from "@/types/monitorStats.js"; +export * from "@/types/statusPage.js"; diff --git a/server/src/types/statusPage.ts b/server/src/types/statusPage.ts new file mode 100644 index 000000000..64a5ac50b --- /dev/null +++ b/server/src/types/statusPage.ts @@ -0,0 +1,29 @@ +export const StatusPageTypes = ["uptime"] as const; +export type StatusPageType = (typeof StatusPageTypes)[number]; + +export interface StatusPageLogo { + data: Buffer; + contentType: string; +} + +export interface StatusPage { + id: string; + userId: string; + teamId: string; + type: StatusPageType; + companyName: string; + url: string; + timezone?: string; + color: string; + monitors: string[]; + subMonitors: string[]; + originalMonitors?: string[]; + logo?: StatusPageLogo | null; + isPublished: boolean; + showCharts: boolean; + showUptimePercentage: boolean; + showAdminLoginLink: boolean; + customCSS: string; + createdAt: string; + updatedAt: string; +}