From 6bc9194e8f69145bf5df8ef55db70e0f2faf8886 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 17 Feb 2026 19:03:17 +0000 Subject: [PATCH] initial commit --- server/src/config/services.ts | 2 + .../incidents/IIncidentsRepository.ts | 1 + .../monitor-stats/IMonitorStatsRepository.ts | 2 + .../MongoMonitorStatsRepository.ts | 12 ++++ .../monitors/IMonitorsRepository.ts | 2 + .../monitors/MongoMonitorsRepository.ts | 11 ++++ .../repositories/teams/ITeamsRepository.ts | 1 + .../teams/MongoTeamsRepository.ts | 5 ++ .../SuperSimpleQueueHelper.ts | 59 ++++++++++++++++++- 9 files changed, 92 insertions(+), 3 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 6843fe64b..aa563fe85 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -202,6 +202,8 @@ export const initializeServices = async ({ incidentService, maintenanceWindowsRepository, monitorsRepository, + teamsRepository, + monitorStatsRepository, }); const superSimpleQueue = await SuperSimpleQueue.create({ diff --git a/server/src/repositories/incidents/IIncidentsRepository.ts b/server/src/repositories/incidents/IIncidentsRepository.ts index 42948a9ee..c4fcef2ae 100644 --- a/server/src/repositories/incidents/IIncidentsRepository.ts +++ b/server/src/repositories/incidents/IIncidentsRepository.ts @@ -24,5 +24,6 @@ export interface IIncidentsRepository { updateById(incidentId: string, teamId: string, updateData: Partial): Promise; // delete deleteByMonitorId(monitorId: string, teamId: string): Promise; + deleteByMonitorIdsNotIn(monitorIds: string[]): Promise; // other } diff --git a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts index c718e7eb3..1ad05596e 100644 --- a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts +++ b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts @@ -7,5 +7,7 @@ export interface IMonitorStatsRepository { // update // delete deleteByMonitorId(monitorId: string): Promise; + deleteByMonitorIds(monitorIds: string[]): Promise; + deleteByMonitorIdsNotIn(monitorIds: string[]): Promise; // other } diff --git a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts index 7211c7d89..0a4905fc1 100644 --- a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts +++ b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts @@ -52,6 +52,18 @@ class MongoMonitorStatsRepository implements IMonitorStatsRepository { } return this.toEntity(deleted); }; + + deleteByMonitorIds = async (monitorIds: string[]): Promise => { + const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + const result = await MonitorStatsModel.deleteMany({ monitorId: { $in: objectIds } }); + return result.deletedCount ?? 0; + }; + + deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise => { + const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + const result = await MonitorStatsModel.deleteMany({ monitorId: { $nin: objectIds } }); + return result.deletedCount ?? 0; + }; } export default MongoMonitorStatsRepository; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 2fbce3406..63940f591 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -41,4 +41,6 @@ export interface IMonitorsRepository { findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; findGroupsByTeamId(teamId: string): Promise; removeNotificationFromMonitors(notificationId: string): Promise; + deleteByTeamIdsNotIn(teamIds: string[]): Promise; + findAllMonitorIds(): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index c6f3266a6..728d2081f 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -431,6 +431,17 @@ class MongoMonitorsRepository implements IMonitorsRepository { createdAt: toDateString(doc.createdAt), }; }; + + deleteByTeamIdsNotIn = async (teamIds: string[]): Promise => { + const objectIds = teamIds.map((id) => new mongoose.Types.ObjectId(id)); + const result = await MonitorModel.deleteMany({ teamId: { $nin: objectIds } }); + return result.deletedCount ?? 0; + }; + + findAllMonitorIds = async (): Promise => { + const monitors = await MonitorModel.find({}, { _id: 1 }).lean(); + return monitors.map((doc) => doc._id.toString()); + }; } export default MongoMonitorsRepository; diff --git a/server/src/repositories/teams/ITeamsRepository.ts b/server/src/repositories/teams/ITeamsRepository.ts index 9ccdd97fb..ba1b353cf 100644 --- a/server/src/repositories/teams/ITeamsRepository.ts +++ b/server/src/repositories/teams/ITeamsRepository.ts @@ -6,4 +6,5 @@ export interface ITeamsRepository { // update // delete // other + findAllTeamIds(): Promise; } diff --git a/server/src/repositories/teams/MongoTeamsRepository.ts b/server/src/repositories/teams/MongoTeamsRepository.ts index 93a306787..04e2be6f4 100644 --- a/server/src/repositories/teams/MongoTeamsRepository.ts +++ b/server/src/repositories/teams/MongoTeamsRepository.ts @@ -31,6 +31,11 @@ class MongoTeamsRepository implements ITeamsRepository { const team = await TeamModel.create({ email }); return this.toEntity(team); }; + + findAllTeamIds = async (): Promise => { + const teams = await TeamModel.find({}, { _id: 1 }).lean(); + return teams.map((team) => this.toStringId(team._id)); + }; } export default MongoTeamsRepository; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 08bc5e257..4d91088a1 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -4,7 +4,7 @@ import { AppError } from "@/utils/AppError.js"; import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js"; import type { StatusChangeResult, MonitorStatusResponse, HardwareStatusPayload, MonitorStatus } from "@/types/index.js"; import IncidentService from "@/service/business/incidentService.js"; -import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js"; +import { IMaintenanceWindowsRepository, IMonitorsRepository, ITeamsRepository, IMonitorStatsRepository } from "@/repositories/index.js"; export interface MonitorActionDecision { shouldCreateIncident: boolean; @@ -32,6 +32,8 @@ class SuperSimpleQueueHelper { private incidentService: IncidentService; private maintenanceWindowsRepository: IMaintenanceWindowsRepository; private monitorsRepository: IMonitorsRepository; + private teamsRepository: ITeamsRepository; + private monitorStatsRepository: IMonitorStatsRepository; constructor({ logger, @@ -43,6 +45,8 @@ class SuperSimpleQueueHelper { incidentService, maintenanceWindowsRepository, monitorsRepository, + teamsRepository, + monitorStatsRepository, }: { logger: any; networkService: INetworkService; @@ -53,6 +57,8 @@ class SuperSimpleQueueHelper { incidentService: IncidentService; maintenanceWindowsRepository: IMaintenanceWindowsRepository; monitorsRepository: IMonitorsRepository; + teamsRepository: ITeamsRepository; + monitorStatsRepository: IMonitorStatsRepository; }) { this.logger = logger; this.networkService = networkService; @@ -63,6 +69,8 @@ class SuperSimpleQueueHelper { this.incidentService = incidentService; this.maintenanceWindowsRepository = maintenanceWindowsRepository; this.monitorsRepository = monitorsRepository; + this.teamsRepository = teamsRepository; + this.monitorStatsRepository = monitorStatsRepository; } get serviceName() { @@ -156,10 +164,55 @@ class SuperSimpleQueueHelper { getCleanupOrphanedJob = () => { return async () => { try { - // Remove orphaned monitors - // remove orphaned monitorStats + this.logger.info({ + message: "Starting cleanup of orphaned data", + service: SERVICE_NAME, + method: "getCleanupOrphanedJob", + }); + + // Get all valid team IDs + const validTeamIds = await this.teamsRepository.findAllTeamIds(); + this.logger.debug({ + message: `Found ${validTeamIds.length} valid teams`, + service: SERVICE_NAME, + method: "getCleanupOrphanedJob", + }); + + // Remove orphaned monitors (monitors without a valid team) + const deletedMonitorCount = await this.monitorsRepository.deleteByTeamIdsNotIn(validTeamIds); + if (deletedMonitorCount > 0) { + this.logger.info({ + message: `Deleted ${deletedMonitorCount} orphaned monitors`, + service: SERVICE_NAME, + method: "getCleanupOrphanedJob", + }); + } + + // Remove orphaned monitorStats (stats without a valid monitor) + const allMonitorIds = await this.monitorsRepository.findAllMonitorIds(); + this.logger.debug({ + message: `Found ${allMonitorIds.length} valid monitors`, + service: SERVICE_NAME, + method: "getCleanupOrphanedJob", + }); + + const deletedStatsCount = await this.monitorStatsRepository.deleteByMonitorIdsNotIn(allMonitorIds); + if (deletedStatsCount > 0) { + this.logger.info({ + message: `Deleted ${deletedStatsCount} orphaned monitor stats`, + service: SERVICE_NAME, + method: "getCleanupOrphanedJob", + }); + } + // Remove orphaned checks // Remove orphaned incidents + + this.logger.info({ + message: "Cleanup of orphaned data completed", + service: SERVICE_NAME, + method: "getCleanupOrphanedJob", + }); } catch (error: any) { this.logger.warn({ message: error.message,