diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 6e2e9dea1..90fcc5a94 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -1,4 +1,4 @@ -import { type MonitorType, type Monitor, type MonitorsSummary, CheckSnapshot } from "@/types/index.js"; +import { type MonitorType, type Monitor, type MonitorStatus, type MonitorsSummary, CheckSnapshot } from "@/types/index.js"; export interface TeamQueryConfig { limit?: number; @@ -31,7 +31,7 @@ export interface IMonitorsRepository { // update updateById(monitorId: string, teamId: string, updates: Partial): Promise; - updateByIds(monitorIds: string[], teamId: string, updates: Partial): Promise; + updateByIds(monitorIds: string[], teamId: string, updates: Partial, excludeStatuses?: MonitorStatus[]): Promise; updateStatusWindowAndChecks( monitorId: string, teamId: string, diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 5f6bbac73..3d1f9c3a5 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -1,6 +1,6 @@ import { MonitorModel } from "@/db/models/index.js"; import type { MonitorDocument, CheckSnapshotDocument } from "@/db/models/index.js"; -import type { Monitor, MonitorsSummary, CheckSnapshot } from "@/types/index.js"; +import type { Monitor, MonitorStatus, MonitorsSummary, CheckSnapshot } from "@/types/index.js"; import mongoose, { type FilterQuery, type PipelineStage } from "mongoose"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import { MongoBulkWriteError } from "mongodb"; @@ -190,9 +190,13 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.toEntity(updatedMonitor); }; - updateByIds = async (monitorIds: string[], teamId: string, updates: Partial) => { + updateByIds = async (monitorIds: string[], teamId: string, updates: Partial, excludeStatuses?: MonitorStatus[]) => { const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); - const updated = await MonitorModel.updateMany({ _id: { $in: objectIds }, teamId }, { $set: updates }, { runValidators: true }); + const filter: Record = { _id: { $in: objectIds }, teamId }; + if (excludeStatuses && excludeStatuses.length > 0) { + filter.status = { $nin: excludeStatuses }; + } + const updated = await MonitorModel.updateMany(filter, { $set: updates }, { runValidators: true }); return updated.modifiedCount; }; diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index d198d1e9d..2ea4c4eae 100644 --- a/server/src/service/business/maintenanceWindowService.ts +++ b/server/src/service/business/maintenanceWindowService.ts @@ -69,7 +69,7 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { const toInitializing = monitorIds.filter((monitorId) => !stillCovered.has(monitorId)); if (toInitializing.length > 0) { - await this.monitorsRepository.updateByIds(toInitializing, teamId, { status: "initializing" }); + await this.monitorsRepository.updateByIds(toInitializing, teamId, { status: "initializing" }, ["paused"]); } }; @@ -120,7 +120,7 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { }); if (isWindowActive(created)) { - await this.monitorsRepository.updateByIds(created.monitorIds, teamId, { status: "maintenance" }); + await this.monitorsRepository.updateByIds(created.monitorIds, teamId, { status: "maintenance" }, ["paused"]); } }; @@ -205,7 +205,7 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { const leavingCandidates = wasActive ? existing.monitorIds.filter((monitorId) => !isActive || !updated.monitorIds.includes(monitorId)) : []; if (enteringMaintenance.length > 0) { - await this.monitorsRepository.updateByIds(enteringMaintenance, teamId, { status: "maintenance" }); + await this.monitorsRepository.updateByIds(enteringMaintenance, teamId, { status: "maintenance" }, ["paused"]); } if (leavingCandidates.length > 0) { diff --git a/server/test/unit/services/maintenanceWindowService.test.ts b/server/test/unit/services/maintenanceWindowService.test.ts index bee60d4ef..12b46de95 100644 --- a/server/test/unit/services/maintenanceWindowService.test.ts +++ b/server/test/unit/services/maintenanceWindowService.test.ts @@ -158,7 +158,7 @@ describe("MaintenanceWindowService", () => { await service.createMaintenanceWindow(defaultCreateParams); expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1); - expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], "team-1", { status: "maintenance" }); + expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], "team-1", { status: "maintenance" }, ["paused"]); }); it("does not touch monitor status when the created window is inactive", async () => { @@ -272,7 +272,7 @@ describe("MaintenanceWindowService", () => { await service.deleteMaintenanceWindow({ id: "mw-1", teamId: "team-1" }); expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1); - expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], "team-1", { status: "initializing" }); + expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], "team-1", { status: "initializing" }, ["paused"]); }); it("excludes the deleted window from the overlap check", async () => { @@ -294,7 +294,7 @@ describe("MaintenanceWindowService", () => { await service.deleteMaintenanceWindow({ id: "mw-1", teamId: "team-1" }); expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1); - expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1"], "team-1", { status: "initializing" }); + expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1"], "team-1", { status: "initializing" }, ["paused"]); }); }); @@ -406,7 +406,7 @@ describe("MaintenanceWindowService", () => { }); expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1); - expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], "team-1", { status: "maintenance" }); + expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], "team-1", { status: "maintenance" }, ["paused"]); }); it("sets all monitors to initializing when window transitions active → inactive", async () => { @@ -422,7 +422,7 @@ describe("MaintenanceWindowService", () => { }); expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1); - expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], "team-1", { status: "initializing" }); + expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], "team-1", { status: "initializing" }, ["paused"]); }); it("flips only newly added monitors to maintenance when window stays active", async () => { @@ -438,7 +438,7 @@ describe("MaintenanceWindowService", () => { }); expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1); - expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-2"], "team-1", { status: "maintenance" }); + expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-2"], "team-1", { status: "maintenance" }, ["paused"]); }); it("flips only removed monitors to initializing when window stays active", async () => { @@ -454,7 +454,7 @@ describe("MaintenanceWindowService", () => { }); expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1); - expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-2"], "team-1", { status: "initializing" }); + expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-2"], "team-1", { status: "initializing" }, ["paused"]); }); it("makes no monitor updates when window stays active and monitor set is unchanged", async () => { @@ -486,7 +486,7 @@ describe("MaintenanceWindowService", () => { }); expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1); - expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1"], "team-1", { status: "initializing" }); + expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1"], "team-1", { status: "initializing" }, ["paused"]); }); it("excludes the current window from the overlap check", async () => { @@ -519,7 +519,7 @@ describe("MaintenanceWindowService", () => { body: { active: false }, }); - expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1"], "team-1", { status: "initializing" }); + expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1"], "team-1", { status: "initializing" }, ["paused"]); }); }); });