jobqueue and stauts service

This commit is contained in:
Alex Holliday committed 2026-01-02 23:28:36 +00:00
1 parent 3bdfe5567d
commit 602597abe2
8 files changed
+270 -124

No files matched your search

+4 -3
View File
@@ -36,6 +36,8 @@ import { MeService } from "@/services/index.js";
import { MongoMonitorRepository } from "@/repositories/index.js";
export const initServices = async () => {
const monitorRepository = new MongoMonitorRepository();
const checkService = new CheckService();
const inviteService = new InviteService();
const maintenanceService = new MaintenanceService();
@@ -46,7 +48,7 @@ export const initServices = async () => {
const slackService = new SlackService();
const webhookService = new WebhookService();
const networkService = new NetworkService(got);
const statusService = new StatusService();
const statusService = new StatusService(monitorRepository);
const settingsService = new SettingsService();
const stripeService = new StripeService();
const billingService = new BillingService();
@@ -68,11 +70,10 @@ export const initServices = async () => {
maintenanceService,
statsAggregationService
);
const jobQueue = await JobQueue.create(jobGenerator);
const jobQueue = await JobQueue.create(jobGenerator, monitorRepository);
const entitlementsProvider = EntitlementsFactory.create();
const authService = new AuthService(jobQueue, entitlementsProvider);
const meService = new MeService(entitlementsProvider);
const monitorRepository = new MongoMonitorRepository();
const monitorService = new MonitorService(jobQueue, monitorRepository);
const queueService = new QueueService(jobQueue);
const teamService = new TeamService(jobQueue);
@@ -4,6 +4,20 @@ import type {
MonitorStatus,
} from "@/types/domain/monitor.js";
export type UpdateMonitorPatch = Partial<
Pick<
Monitor,
| "name"
| "secret"
| "interval"
| "rejectUnauthorized"
| "n"
| "thresholds"
| "notificationChannels"
| "status"
>
>;
export interface TeamQueryConfig {
search: string;
sortField: string;
@@ -15,7 +29,16 @@ export interface TeamQueryConfig {
}
export interface IMonitorRepository {
// Create
create(
orgId: string,
userId: string,
currentTeamId: string,
monitorData: Monitor
): Promise<Monitor>;
// Single fetch
findAll(): Promise<Monitor[]>;
findById(monitorId: string, teamId: string): Promise<Monitor | null>;
// Collection fetches
@@ -34,8 +57,23 @@ export interface IMonitorRepository {
findByOrgId(orgId: string): Promise<Monitor[]>;
// Deletions
deleteById(id: string): Promise<boolean>;
deleteByOrgId(orgId: string): Promise<number>;
}
deleteById(monitorId: string, teamId: string): Promise<boolean>;
deleteByOrgId(orgId: string): Promise<boolean>;
export default IMonitorRepository;
// Updates
togglePauseById(
monitorId: string,
teamId: string,
userId: string
): Promise<Monitor | null>;
updateById(
monitorId: string,
teamId: string,
userId: string,
patch: UpdateMonitorPatch
): Promise<Monitor | null>;
// Counts
countByOrgId(orgId: string): Promise<number>;
}
@@ -30,6 +30,27 @@ class MongoMonitorRepository implements IMonitorRepository {
};
};
// Create
create = async (
orgId: string,
userId: string,
currentTeamId: string,
monitorData: MonitorEntity
) => {
const monitorLiteral: Partial<IMonitor> = {
...monitorData,
orgId: new mongoose.Types.ObjectId(orgId),
teamId: new mongoose.Types.ObjectId(currentTeamId),
createdBy: new mongoose.Types.ObjectId(userId),
updatedBy: new mongoose.Types.ObjectId(userId),
notificationChannels: monitorData.notificationChannels?.map(
(id) => new mongoose.Types.ObjectId(id)
),
};
const monitor = await Monitor.create(monitorLiteral);
return this.toEntity(monitor);
};
// Single fetch
findById = async (monitorId: string, teamId: string) => {
const monitor = await Monitor.findOne({ _id: monitorId, teamId });
@@ -38,6 +59,11 @@ class MongoMonitorRepository implements IMonitorRepository {
};
// Collection fetches
findAll = async () => {
const monitors = await Monitor.find();
return monitors.map(this.toEntity);
};
findByTeamId = async (teamId: string) => {
const monitors = await Monitor.find({ teamId });
return monitors.map(this.toEntity);
@@ -81,8 +107,10 @@ class MongoMonitorRepository implements IMonitorRepository {
return monitors.map(this.toEntity);
};
findByOrgId = async (orgId: string) => {
return [];
const monitors = await Monitor.find({ orgId });
return monitors.map(this.toEntity);
};
findMonitorCountsByTeamId = async (teamId: string) => {
@@ -115,12 +143,69 @@ class MongoMonitorRepository implements IMonitorRepository {
};
// Deletions
deleteById = async (id: string) => {
return false;
deleteById = async (monitorId: string, teamId: string) => {
await Monitor.deleteOne({ _id: monitorId, teamId });
return true;
};
deleteByOrgId = async (orgId: string) => {
return 0;
const deleted = await Monitor.deleteMany({ orgId });
return deleted.acknowledged;
};
togglePauseById = async (
monitorId: string,
teamId: string,
userId: string
) => {
const monitor = await Monitor.findOneAndUpdate(
{ _id: monitorId, teamId },
[
{
$set: {
status: {
$cond: {
if: { $eq: ["$status", "paused"] },
then: "resuming",
else: "paused",
},
},
updatedBy: userId,
updatedAt: new Date(),
},
},
],
{ new: true }
);
if (!monitor) return null;
return this.toEntity(monitor);
};
// Update
updateById = async (
monitorId: string,
teamId: string,
userId: string,
patch: Partial<MonitorEntity>
) => {
const updatedMonitor = await Monitor.findOneAndUpdate(
{ _id: monitorId, teamId },
{
$set: {
...patch,
updatedAt: new Date(),
updatedBy: userId,
},
},
{ new: true, runValidators: true }
);
if (!updatedMonitor) return null;
return this.toEntity(updatedMonitor);
};
// Counts
countByOrgId(orgId: string): Promise<number> {
return Monitor.countDocuments({ orgId });
}
}
export default MongoMonitorRepository;
+40 -56
View File
@@ -44,8 +44,8 @@ export interface IMonitorService {
orgId: string,
userId: string,
currentTeamId: string,
monitorData: IMonitor
) => Promise<IMonitor>;
monitorData: MonitorEntity
) => Promise<MonitorEntity>;
getAll: (teamId: string) => Promise<MonitorEntity[]>;
getAllEmbedChecks: (
teamId: string,
@@ -73,13 +73,13 @@ export interface IMonitorService {
userId: string,
teamId: string,
monitorId: string
) => Promise<IMonitor>;
) => Promise<MonitorEntity>;
update: (
userId: string,
teamId: string,
monitorId: string,
updateData: Partial<IMonitor>
) => Promise<IMonitor>;
updateData: Partial<MonitorEntity>
) => Promise<MonitorEntity>;
delete: (teamId: string, monitorId: string) => Promise<boolean>;
deleteAllInOrg: (orgId: string) => Promise<boolean>;
export: (teamId: string) => Promise<Array<Record<string, unknown>>>;
@@ -106,19 +106,16 @@ class MonitorService implements IMonitorService {
orgId: string,
userId: string,
currentTeamId: string,
monitorData: IMonitor
monitorData: MonitorEntity
) => {
const monitorLiteral: Partial<IMonitor> = {
...monitorData,
orgId: new mongoose.Types.ObjectId(orgId),
teamId: new mongoose.Types.ObjectId(currentTeamId),
createdBy: new mongoose.Types.ObjectId(userId),
updatedBy: new mongoose.Types.ObjectId(userId),
};
const monitor = await Monitor.create(monitorLiteral);
const monitor = await this.monitorRepository.create(
orgId,
userId,
currentTeamId,
monitorData
);
await MonitorStats.create({
monitorId: monitor._id,
monitorId: monitor.id,
currentStreakStartedAt: Date.now(),
});
await this.jobQueue.addJob(monitor);
@@ -736,24 +733,10 @@ class MonitorService implements IMonitorService {
};
async togglePause(userId: string, teamId: string, id: string) {
const updatedMonitor = await Monitor.findOneAndUpdate(
{ _id: id, teamId },
[
{
$set: {
status: {
$cond: {
if: { $eq: ["$status", "paused"] },
then: "resuming",
else: "paused",
},
},
updatedBy: userId,
updatedAt: new Date(),
},
},
],
{ new: true }
const updatedMonitor = await this.monitorRepository.togglePauseById(
id,
teamId,
userId
);
if (!updatedMonitor) {
@@ -774,9 +757,9 @@ class MonitorService implements IMonitorService {
userId: string,
teamId: string,
monitorId: string,
updateData: Partial<IMonitor>
updateData: Partial<MonitorEntity>
) {
const allowedFields: (keyof IMonitor)[] = [
const allowedFields: (keyof MonitorEntity)[] = [
"name",
"secret",
"interval",
@@ -785,7 +768,7 @@ class MonitorService implements IMonitorService {
"notificationChannels",
"thresholds",
];
const safeUpdate: Partial<IMonitor> = {};
const safeUpdate: Partial<MonitorEntity> = {};
for (const field of allowedFields) {
if (updateData[field] !== undefined) {
@@ -793,16 +776,11 @@ class MonitorService implements IMonitorService {
}
}
const updatedMonitor = await Monitor.findOneAndUpdate(
{ _id: monitorId, teamId },
{
$set: {
...safeUpdate,
updatedAt: new Date(),
updatedBy: userId,
},
},
{ new: true, runValidators: true }
const updatedMonitor = await this.monitorRepository.updateById(
monitorId,
teamId,
userId,
safeUpdate
);
if (!updatedMonitor) {
@@ -813,7 +791,7 @@ class MonitorService implements IMonitorService {
}
async delete(teamId: string, monitorId: string) {
const monitor = await Monitor.findOne({ _id: monitorId, teamId });
const monitor = await this.monitorRepository.findById(monitorId, teamId);
if (!monitor) {
throw new ApiError("Monitor not found", 404);
}
@@ -821,23 +799,29 @@ class MonitorService implements IMonitorService {
if (!deleted) {
throw new ApiError("Failed to delete monitor job from queue", 500);
}
await monitor.deleteOne();
await this.monitorRepository.deleteById(monitorId, teamId);
return true;
}
async deleteAllInOrg(orgId: string) {
const montiors = await Monitor.find({ orgId });
const montiors = await this.monitorRepository.findByOrgId(orgId);
for (const monitor of montiors) {
await this.jobQueue.deleteJob(monitor);
}
const deleted = await Monitor.deleteMany({ orgId });
return deleted.acknowledged;
return await this.monitorRepository.deleteByOrgId(orgId);
}
export = async (teamId: string) => {
return Monitor.find({ teamId })
.select("name url port type interval n secret -_id")
.lean();
const monitor = await this.monitorRepository.findByTeamId(teamId);
return monitor.map((m) => ({
name: m.name,
url: m.url,
port: m.port,
type: m.type,
interval: m.interval,
n: m.n,
secret: m.secret,
}));
};
import = async (
@@ -852,7 +836,7 @@ class MonitorService implements IMonitorService {
let canImport = 0;
try {
// Get current count
const monitorCount = await Monitor.countDocuments({ orgId });
const monitorCount = await this.monitorRepository.countByOrgId(orgId);
// Check limits, return NO_CAPACITY if at limit
if (monitorCount >= entitlements.monitorsMax) {
@@ -1,4 +1,4 @@
import { IMonitor, ICheck } from "@/db/models/index.js";
import { IMonitor } from "@/db/models/index.js";
import { INetworkService } from "./NetworkService.js";
import { ICheckService } from "../business/CheckService.js";
import { IMonitorStatsService } from "../business/MonitorStatsService.js";
@@ -6,17 +6,15 @@ import { IStatusService } from "./StatusService.js";
import { INotificationService } from "./NotificationService.js";
import { IMaintenanceService } from "../business/MaintenanceService.js";
import { IIncidentService } from "../business/IncidentService.js";
import StatsAggregationService, {
IStatsAggregationService,
} from "@/services/business/StatsAggregationService.js";
import { StatsHourly } from "@/db/models/index.js";
import { IStatsAggregationService } from "@/services/business/StatsAggregationService.js";
import ApiError from "@/utils/ApiError.js";
import { getChildLogger } from "@/logger/Logger.js";
import type { Monitor as MonitorEntity } from "@/types/domain/index.js";
const SERVICE_NAME = "JobGenerator";
const logger = getChildLogger(SERVICE_NAME);
export interface IJobGenerator {
generateJob: () => (Monitor: IMonitor) => Promise<void>;
generateJob: () => (monitor: MonitorEntity) => Promise<void>;
generateCleanupJob: () => () => Promise<void>;
}
@@ -53,9 +51,9 @@ class JobGenerator implements IJobGenerator {
}
generateJob = () => {
return async (monitor: IMonitor) => {
return async (monitor: MonitorEntity) => {
try {
const monitorId = monitor._id.toString();
const monitorId = monitor.id.toString();
if (!monitorId) {
throw new ApiError("No monitorID for creating job", 400);
}
+27 -21
View File
@@ -1,9 +1,10 @@
import { IJob } from "super-simple-scheduler/dist/job/job.js";
import { Monitor, IMonitor } from "@/db/models/index.js";
import type { Monitor as MonitorEntity } from "@/types/domain/index.js";
import Scheduler from "super-simple-scheduler";
import { IJobGenerator } from "./JobGenerator.js";
import { getChildLogger } from "@/logger/Logger.js";
import { config } from "@/config/index.js";
import { IMonitorRepository } from "@/repositories/index.js";
const SERVICE_NAME = "JobQueue";
const logger = getChildLogger(SERVICE_NAME);
@@ -29,11 +30,11 @@ export interface IJobData extends IJob {
export interface IJobQueue {
init: () => Promise<boolean>;
addJob: (monitor: IMonitor) => Promise<boolean>;
pauseJob: (monitor: IMonitor) => Promise<boolean>;
resumeJob: (monitor: IMonitor) => Promise<boolean>;
updateJob: (monitor: IMonitor) => Promise<boolean>;
deleteJob: (monitor: IMonitor) => Promise<boolean>;
addJob: (monitor: MonitorEntity) => Promise<boolean>;
pauseJob: (monitor: MonitorEntity) => Promise<boolean>;
resumeJob: (monitor: MonitorEntity) => Promise<boolean>;
updateJob: (monitor: MonitorEntity) => Promise<boolean>;
deleteJob: (monitor: MonitorEntity) => Promise<boolean>;
getMetrics: () => Promise<IJobMetrics | null>;
getJobs: () => Promise<IJobData[] | null>;
flush: () => Promise<boolean>;
@@ -45,16 +46,21 @@ export default class JobQueue implements IJobQueue {
private scheduler: Scheduler;
private static instance: JobQueue | null = null;
private jobGenerator: any;
constructor() {
private monitorRepository: IMonitorRepository;
constructor(monitorRepository: IMonitorRepository) {
this.SERVICE_NAME = SERVICE_NAME;
this.scheduler = new Scheduler({
logLevel: "debug",
});
this.monitorRepository = monitorRepository;
}
static async create(jobGenerator: IJobGenerator) {
static async create(
jobGenerator: IJobGenerator,
monitorRepository: IMonitorRepository
) {
if (!JobQueue.instance) {
const instance = new JobQueue();
const instance = new JobQueue(monitorRepository);
instance.jobGenerator = jobGenerator;
await instance.init();
JobQueue.instance = instance;
@@ -107,12 +113,12 @@ export default class JobQueue implements IJobQueue {
logger.warn("Initial stats aggregation run failed", e as any);
}
const monitors = await Monitor.find();
const monitors = await this.monitorRepository.findAll();
for (const monitor of monitors) {
const randomOffset = 1000 + Math.random() * 1000;
setTimeout(async () => {
const exists = await Monitor.exists({ _id: monitor._id });
const exists = await Monitor.exists({ _id: monitor.id });
if (exists) {
this.addJob(monitor);
}
@@ -126,10 +132,10 @@ export default class JobQueue implements IJobQueue {
}
};
addJob = async (monitor: IMonitor) => {
addJob = async (monitor: MonitorEntity) => {
try {
return await this.scheduler?.addJob({
id: monitor._id.toString(),
id: monitor.id,
template: "monitor-job",
repeat: monitor.interval,
active: monitor.status !== "paused",
@@ -141,27 +147,27 @@ export default class JobQueue implements IJobQueue {
}
};
pauseJob = async (monitor: IMonitor) => {
pauseJob = async (monitor: MonitorEntity) => {
try {
return await this.scheduler?.pauseJob(monitor._id.toString());
return await this.scheduler?.pauseJob(monitor.id);
} catch (error) {
logger.error(error);
return false;
}
};
resumeJob = async (monitor: IMonitor) => {
resumeJob = async (monitor: MonitorEntity) => {
try {
return await this.scheduler.resumeJob(monitor._id.toString());
return await this.scheduler.resumeJob(monitor.id);
} catch (error) {
logger.error(error);
return false;
}
};
updateJob = async (monitor: IMonitor) => {
updateJob = async (monitor: MonitorEntity) => {
try {
return await this.scheduler.updateJob(monitor._id.toString(), {
return await this.scheduler.updateJob(monitor.id, {
repeat: monitor.interval,
data: monitor,
});
@@ -171,9 +177,9 @@ export default class JobQueue implements IJobQueue {
}
};
deleteJob = async (monitor: IMonitor) => {
deleteJob = async (monitor: MonitorEntity) => {
try {
this.scheduler?.removeJob(monitor._id.toString());
this.scheduler?.removeJob(monitor.id);
return true;
} catch (error) {
logger.error(error);
@@ -16,16 +16,17 @@ import { config } from "@/config/index.js";
import CacheableLookup from "cacheable-lookup";
import { getChildLogger } from "@/logger/Logger.js";
import type { IDockerContainerSummary } from "@/db/models/index.js";
import type { Monitor as MonitorEntity } from "@/types/domain/monitor.js";
const SERVICE_NAME = "NetworkService";
const logger = getChildLogger(SERVICE_NAME);
export interface INetworkService {
requestHttp: (monitor: IMonitor) => Promise<StatusResponse>;
requestInfrastructure: (monitor: IMonitor) => Promise<StatusResponse>;
requestStatus: (monitor: IMonitor) => Promise<StatusResponse>;
requestPagespeed: (monitor: IMonitor) => Promise<StatusResponse>;
requestPing: (monitor: IMonitor) => Promise<StatusResponse>;
requestPort: (monitor: IMonitor) => Promise<StatusResponse>;
requestHttp: (monitor: MonitorEntity) => Promise<StatusResponse>;
requestInfrastructure: (monitor: MonitorEntity) => Promise<StatusResponse>;
requestStatus: (monitor: MonitorEntity) => Promise<StatusResponse>;
requestPagespeed: (monitor: MonitorEntity) => Promise<StatusResponse>;
requestPing: (monitor: MonitorEntity) => Promise<StatusResponse>;
requestPort: (monitor: MonitorEntity) => Promise<StatusResponse>;
}
export interface ICapturePayload {
@@ -92,7 +93,7 @@ class NetworkService implements INetworkService {
}
private buildStatusResponse = <T>(
monitor: IMonitor,
monitor: MonitorEntity,
response: Response<T> | null,
certificateExpiryOrError?: Date | null | any,
maybeError?: any | null
@@ -110,8 +111,8 @@ class NetworkService implements INetworkService {
}
if (error) {
const statusResponse: StatusResponse<T> = {
monitorId: monitor._id.toString(),
teamId: monitor.teamId.toString(),
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: "down" as MonitorStatus,
code: this.NETWORK_ERROR,
@@ -130,8 +131,8 @@ class NetworkService implements INetworkService {
}
const statusResponse: StatusResponse<T> = {
monitorId: monitor._id.toString(),
teamId: monitor.teamId.toString(),
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
code: response?.statusCode || this.NETWORK_ERROR,
status: response?.ok === true ? "up" : "down",
@@ -144,7 +145,7 @@ class NetworkService implements INetworkService {
return statusResponse;
};
requestHttp = async (monitor: IMonitor) => {
requestHttp = async (monitor: MonitorEntity) => {
try {
const url = monitor.url;
if (!url) {
@@ -190,7 +191,7 @@ class NetworkService implements INetworkService {
}
};
requestInfrastructure = async (monitor: IMonitor) => {
requestInfrastructure = async (monitor: MonitorEntity) => {
const url = monitor.url;
if (!url) {
throw new Error("No URL provided");
@@ -225,7 +226,7 @@ class NetworkService implements INetworkService {
return statusResponse;
};
requestPagespeed = async (monitor: IMonitor) => {
requestPagespeed = async (monitor: MonitorEntity) => {
const apiKey = config.PAGESPEED_API_KEY;
if (!apiKey) {
throw new Error("No API key provided for pagespeed monitor");
@@ -265,7 +266,7 @@ class NetworkService implements INetworkService {
}
};
requestPing = async (monitor: IMonitor) => {
requestPing = async (monitor: MonitorEntity) => {
const response = await ping.promise.probe(monitor.url);
const status = response?.alive === true ? "up" : "down";
@@ -276,8 +277,8 @@ class NetworkService implements INetworkService {
const responseTime = Number.isFinite(rawTime) ? rawTime : 0;
return {
monitorId: monitor._id.toString(),
teamId: monitor.teamId.toString(),
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: status as MonitorStatus,
message: "Ping successful",
@@ -286,10 +287,10 @@ class NetworkService implements INetworkService {
};
};
requestPort = async (monitor: IMonitor) => {
requestPort = async (monitor: MonitorEntity) => {
const response = {
monitorId: monitor._id.toString(),
teamId: monitor.teamId.toString(),
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: "down" as MonitorStatus,
message: "Port check failed",
@@ -358,7 +359,7 @@ class NetworkService implements INetworkService {
}
};
requestStatus = async (monitor: IMonitor) => {
requestStatus = async (monitor: MonitorEntity) => {
switch (monitor?.type) {
case "http":
return await this.requestHttp(monitor); // uses GOT
@@ -5,6 +5,9 @@ import {
ICheck,
} from "@/db/models/index.js";
import { StatusResponse } from "./NetworkService.js";
import { IMonitorRepository } from "@/repositories/index.js";
import type { Monitor as MonitorEntity } from "@/types/domain/index.js";
import ApiError from "@/utils/ApiError.js";
// Using compact latestStatuses on Monitor for transition checks
const emptyMetrics = (): ThresholdEvaluationResult["metrics"] => ({
@@ -33,9 +36,10 @@ export interface ThresholdEvaluationResult {
const SERVICE_NAME = "StatusService";
const MAX_LATEST_CHECKS = 25;
export interface IStatusService {
updateMonitorStatus: (
monitor: IMonitor,
monitor: MonitorEntity,
status: StatusResponse
) => Promise<StatusChangeResult>;
@@ -56,18 +60,20 @@ export interface IStatusService {
}
export type StatusChangeResult = [
updatedMonitor: IMonitor,
updatedMonitor: MonitorEntity,
statusChanged: boolean
];
class StatusService implements IStatusService {
public SERVICE_NAME: string;
constructor() {
private monitorRepository: IMonitorRepository;
constructor(monitorRepository: IMonitorRepository) {
this.SERVICE_NAME = SERVICE_NAME;
this.monitorRepository = monitorRepository;
}
updateMonitorStatus = async (
monitor: IMonitor,
monitor: MonitorEntity,
statusResponse: StatusResponse
): Promise<StatusChangeResult> => {
const newStatus = statusResponse.status;
@@ -84,20 +90,47 @@ class StatusService implements IStatusService {
monitor.status = newStatus;
const lenAfter = monitor.latestStatuses.length;
const thresholdReachedNow = lenAfter >= monitor.n && prevLen < monitor.n;
return [await monitor.save(), thresholdReachedNow];
const updated = await this.monitorRepository.updateById(
monitor.id,
monitor.teamId,
monitor.updatedBy,
monitor
);
if (!updated) {
throw new ApiError("Failed to update monitor status", 500);
}
return [updated, thresholdReachedNow];
}
const { n } = monitor;
const latestN = monitor.latestStatuses.slice(-n);
if (latestN.length < n) {
return [await monitor.save(), false];
const updated = await this.monitorRepository.updateById(
monitor.id,
monitor.teamId,
monitor.updatedBy,
monitor
);
if (!updated) {
throw new ApiError("Failed to update monitor status", 500);
}
return [updated, false];
}
const allDifferent = latestN.every((s) => s !== monitor.status);
if (allDifferent && monitor.status !== newStatus) {
monitor.status = newStatus;
}
return [await monitor.save(), allDifferent];
const updated = await this.monitorRepository.updateById(
monitor.id,
monitor.teamId,
monitor.updatedBy,
monitor
);
if (!updated) {
throw new ApiError("Failed to update monitor status", 500);
}
return [updated, allDifferent];
};
calculateAvgResponseTime = (