diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts
index 963409fc8a..7015c7da81 100644
--- a/client/src/Hooks/useMonitorForm.ts
+++ b/client/src/Hooks/useMonitorForm.ts
@@ -7,11 +7,30 @@ interface UseMonitorFormOptions {
defaultType?: MonitorType;
}
+const normalizeNotifications = (notifications?: unknown[]) => {
+ if (!Array.isArray(notifications)) {
+ return [];
+ }
+ return notifications.map((notification) => {
+ if (typeof notification === "string") {
+ return { channelId: notification, delayMinutes: 0 };
+ }
+ const item = notification as {
+ channelId?: unknown;
+ delayMinutes?: unknown;
+ };
+ return {
+ channelId: typeof item.channelId === "string" ? item.channelId : "",
+ delayMinutes: typeof item.delayMinutes === "number" ? item.delayMinutes : 0,
+ };
+ });
+};
+
const getBaseDefaults = (data?: Monitor | null) => ({
name: data?.name || "",
description: data?.description || "",
interval: data?.interval || 60000,
- notifications: data?.notifications || [],
+ notifications: normalizeNotifications(data?.notifications),
statusWindowSize: data?.statusWindowSize || 5,
statusWindowThreshold: data?.statusWindowThreshold || 60,
geoCheckEnabled: data?.geoCheckEnabled ?? false,
diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx
index 15b76eab36..10492e9535 100644
--- a/client/src/Pages/CreateMonitor/index.tsx
+++ b/client/src/Pages/CreateMonitor/index.tsx
@@ -705,71 +705,108 @@ const CreateMonitorPage = () => {
name="notifications"
control={control}
render={({ field }) => {
- // Map notifications to have 'name' property for Autocomplete
- const notificationOptions = (notifications ?? []).map((n) => ({
- ...n,
- name: n.notificationName,
- }));
- const selectedNotifications = notificationOptions.filter((n) =>
- (field.value ?? []).includes(n.id)
- );
- return (
-
- option.name}
- onChange={(_: unknown, newValue: typeof notificationOptions) => {
- field.onChange(newValue.map((n) => n.id));
- }}
- isOptionEqualToValue={(option, value) => option.id === value.id}
- />
- {selectedNotifications.length > 0 && (
-
- {selectedNotifications.map((notification, index) => (
-
-
- {notification.notificationName}
-
- {
- field.onChange(
- (field.value ?? []).filter(
- (id: string) => id !== notification.id
- )
- );
- }}
- aria-label="Remove notification"
- >
-
-
- {index < selectedNotifications.length - 1 && }
-
- ))}
-
- )}
-
- );
- }}
- />
- }
- />
-
- {(watchedType === "http" ||
- watchedType === "grpc" ||
- watchedType === "websocket") && (
- ({
+ ...n,
+ name: n.notificationName,
+ }));
+ const selectedNotifications = notificationOptions.filter((notification) =>
+ (field.value ?? []).some(
+ (item: { channelId: string; delayMinutes: number }) =>
+ item.channelId === notification.id
+ )
+ );
+ return (
+
+ option.name}
+ onChange={(_: unknown, newValue: typeof notificationOptions) => {
+ const nextValues = newValue.map((notification) => {
+ const existing = (field.value ?? []).find(
+ (item: { channelId: string; delayMinutes: number }) =>
+ item.channelId === notification.id
+ );
+ return {
+ channelId: notification.id,
+ delayMinutes: existing?.delayMinutes ?? 0,
+ };
+ });
+ field.onChange(nextValues);
+ }}
+ isOptionEqualToValue={(option, value) => option.id === value.id}
+ />
+ {selectedNotifications.length > 0 && (
+
+ {selectedNotifications.map((notification, index) => {
+ const selectedRule = (field.value ?? []).find(
+ (item: { channelId: string; delayMinutes: number }) =>
+ item.channelId === notification.id
+ );
+ const delayMinutes = selectedRule?.delayMinutes ?? 0;
+ return (
+
+
+ {notification.notificationName}
+
+ {
+ const value = Number(event.target.value) || 0;
+ field.onChange(
+ (field.value ?? []).map(
+ (item: { channelId: string; delayMinutes: number }) =>
+ item.channelId === notification.id
+ ? { ...item, delayMinutes: value }
+ : item
+ )
+ );
+ }}
+ />
+ {
+ field.onChange(
+ (field.value ?? []).filter(
+ (item: { channelId: string }) =>
+ item.channelId !== notification.id
+ )
+ );
+ }}
+ aria-label="Remove notification"
+ >
+
+
+ {index < selectedNotifications.length - 1 && }
+
+ );
+ })}
+
+ )}
+
+ );
+ }}
+ />
+ }
+ />
+ {(watchedType === "http" ||
+ watchedType === "grpc" ||
+ watchedType === "websocket") && (
+ notification.channelId);
if (notifications.length === 0) {
throw new AppError({ message: "No notifications", status: 400 });
diff --git a/server/src/db/models/Incident.ts b/server/src/db/models/Incident.ts
index 82e2b5eb2b..ca28e7568e 100644
--- a/server/src/db/models/Incident.ts
+++ b/server/src/db/models/Incident.ts
@@ -9,6 +9,11 @@ type IncidentDocumentBase = Omit(
type: String,
default: null,
},
+ escalatedNotificationIds: {
+ type: [String],
+ default: [],
+ },
+ acknowledged: {
+ type: Boolean,
+ default: false,
+ index: true,
+ },
+ acknowledgedBy: {
+ type: Schema.Types.ObjectId,
+ ref: "User",
+ default: null,
+ },
+ acknowledgedAt: {
+ type: Date,
+ default: null,
+ },
+ acknowledgedByEmail: {
+ type: String,
+ default: null,
+ },
},
{ timestamps: true }
);
@@ -83,6 +110,8 @@ IncidentSchema.index({ status: 1, startTime: -1 });
IncidentSchema.index({ resolutionType: 1, status: 1 });
IncidentSchema.index({ resolvedBy: 1, status: 1 });
IncidentSchema.index({ createdAt: -1 });
+IncidentSchema.index({ acknowledged: 1, status: 1 });
+IncidentSchema.index({ acknowledgedBy: 1, status: 1 });
const IncidentModel = model("Incident", IncidentSchema);
diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts
index 036aeadad6..3d383e4b2e 100644
--- a/server/src/db/models/Monitor.ts
+++ b/server/src/db/models/Monitor.ts
@@ -1,5 +1,5 @@
import { Schema, model, Types } from "mongoose";
-import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
+import type { Monitor, MonitorMatchMethod, CheckSnapshot, EscalationNotification } from "@/types/monitor.js";
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
import type {
CheckAudits,
@@ -22,7 +22,7 @@ type MonitorDocumentBase = Omit<
> & {
statusWindow: boolean[];
recentChecks: CheckSnapshotDocument[];
- notifications: Types.ObjectId[];
+ notifications: EscalationNotification[];
selectedDisks: string[];
matchMethod?: MonitorMatchMethod;
};
@@ -280,8 +280,16 @@ const MonitorSchema = new Schema(
},
notifications: [
{
- type: Schema.Types.ObjectId,
- ref: "Notification",
+ delayMinutes: {
+ type: Number,
+ required: true,
+ min: 0,
+ },
+ channelId: {
+ type: Schema.Types.ObjectId,
+ ref: "Notification",
+ required: true,
+ },
},
],
secret: {
diff --git a/server/src/repositories/incidents/MongoIncidentRepository.ts b/server/src/repositories/incidents/MongoIncidentRepository.ts
index 096ba3d37b..e56e5ca082 100644
--- a/server/src/repositories/incidents/MongoIncidentRepository.ts
+++ b/server/src/repositories/incidents/MongoIncidentRepository.ts
@@ -60,6 +60,11 @@ class MongoIncidentRepository implements IIncidentsRepository {
resolvedBy: doc.resolvedBy ? this.toStringId(doc.resolvedBy) : null,
resolvedByEmail: doc.resolvedByEmail ?? null,
comment: doc.comment ?? null,
+ acknowledged: doc.acknowledged,
+ acknowledgedBy: doc.acknowledgedBy ? this.toStringId(doc.acknowledgedBy) : null,
+ acknowledgedAt: doc.acknowledgedAt ? this.toDateString(doc.acknowledgedAt) : null,
+ acknowledgedByEmail: doc.acknowledgedByEmail ?? null,
+ escalatedNotificationIds: doc.escalatedNotificationIds ?? [],
createdAt: this.toDateString(doc.createdAt),
updatedAt: this.toDateString(doc.updatedAt),
};
diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts
index b2d7594483..213d3ffa6a 100644
--- a/server/src/repositories/monitors/MongoMonitorsRepository.ts
+++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts
@@ -17,7 +17,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
if (!monitors.length) {
return [];
}
- const payload = monitors.map((monitor) => ({ ...monitor, notifications: undefined }));
+ const payload = monitors.map((monitor) => ({ ...monitor }));
try {
const inserted = await MonitorModel.insertMany(payload, { ordered: false });
return this.mapDocuments(inserted);
@@ -293,7 +293,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
};
removeNotificationFromMonitors = async (notificationId: string): Promise => {
- await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } });
+ await MonitorModel.updateMany({ "notifications.channelId": notificationId }, { $pull: { notifications: { channelId: notificationId } } });
};
updateNotifications = async (
@@ -315,13 +315,19 @@ class MongoMonitorsRepository implements IMonitorsRepository {
let update;
switch (action) {
case "add":
- update = { $addToSet: { notifications: { $each: notificationObjectIds } } };
+ update = {
+ $addToSet: {
+ notifications: {
+ $each: notificationObjectIds.map((id) => ({ channelId: id, delayMinutes: 0 })),
+ },
+ },
+ };
break;
case "remove":
- update = { $pull: { notifications: { $in: notificationObjectIds } } };
+ update = { $pull: { notifications: { channelId: { $in: notificationObjectIds } } } };
break;
case "set":
- update = { $set: { notifications: notificationObjectIds } };
+ update = { $set: { notifications: notificationObjectIds.map((id) => ({ channelId: id, delayMinutes: 0 })) } };
break;
default:
throw new AppError({ message: `Invalid action: ${action}`, status: 400 });
@@ -350,7 +356,19 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return value instanceof Date ? value.toISOString() : value;
};
- const notificationIds = (doc.notifications ?? []).map((notification) => toStringId(notification));
+ const notificationObjects = (doc.notifications ?? []).map((notification: unknown) => {
+ if (notification && typeof notification === "object" && "channelId" in notification) {
+ const item = notification as { channelId?: unknown; delayMinutes?: unknown };
+ return {
+ channelId: toStringId(item.channelId),
+ delayMinutes: typeof item.delayMinutes === "number" ? item.delayMinutes : 0,
+ };
+ }
+ return {
+ channelId: toStringId(notification),
+ delayMinutes: 0,
+ };
+ });
return {
id: toStringId(doc._id),
@@ -373,7 +391,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
isActive: doc.isActive,
interval: doc.interval,
uptimePercentage: doc.uptimePercentage ?? undefined,
- notifications: notificationIds,
+ notifications: notificationObjects,
secret: doc.secret ?? undefined,
cpuAlertThreshold: doc.cpuAlertThreshold,
cpuAlertCounter: doc.cpuAlertCounter,
@@ -409,7 +427,19 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return value instanceof Date ? value.toISOString() : value;
};
- const notificationIds = (doc.notifications ?? []).map((notification: unknown) => toStringId(notification));
+ const notificationObjects = (doc.notifications ?? []).map((notification: unknown) => {
+ if (notification && typeof notification === "object" && "channelId" in notification) {
+ const item = notification as { channelId?: unknown; delayMinutes?: unknown };
+ return {
+ channelId: toStringId(item.channelId),
+ delayMinutes: typeof item.delayMinutes === "number" ? item.delayMinutes : 0,
+ };
+ }
+ return {
+ channelId: toStringId(notification),
+ delayMinutes: 0,
+ };
+ });
return {
id: toStringId(doc._id),
@@ -432,7 +462,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
isActive: doc.isActive,
interval: doc.interval,
uptimePercentage: doc.uptimePercentage ?? undefined,
- notifications: notificationIds,
+ notifications: notificationObjects,
secret: doc.secret ?? undefined,
cpuAlertThreshold: doc.cpuAlertThreshold,
cpuAlertCounter: doc.cpuAlertCounter,
diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts
index 4790f9aacc..61a77a09ad 100644
--- a/server/src/service/business/incidentService.ts
+++ b/server/src/service/business/incidentService.ts
@@ -105,7 +105,7 @@ export class IncidentService implements IIncidentService {
return await this.incidentsRepository.updateById(activeIncident.id, activeIncident.teamId, activeIncident);
}
- return null;
+ return activeIncident;
};
private buildThresholdBreachMessage(monitor: Monitor, monitorStatusResponse?: MonitorStatusResponse): string {
diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts
index b6908127b2..4f4b938d77 100644
--- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts
+++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts
@@ -169,14 +169,26 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
}
// Step 7. Handle incidents (best effort, don't wait)
- this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision, status).catch((error: unknown) => {
+ const activeIncident = await this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision, status).catch((error: unknown) => {
this.logger.warn({
message: `Error handling incident for job ${monitor.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
service: SERVICE_NAME,
method: "getMonitorJob",
stack: error instanceof Error ? error.stack : undefined,
});
+ return null;
});
+
+ if (activeIncident && (statusChangeResult.monitor.status === "down" || statusChangeResult.monitor.status === "breached")) {
+ this.notificationsService.handleEscalationNotifications(statusChangeResult.monitor, status, activeIncident).catch((error: unknown) => {
+ this.logger.error({
+ message: `Error sending escalation notifications for job ${statusChangeResult.monitor.id}: ${error instanceof Error ? error.message : "Unknown error"}`,
+ service: SERVICE_NAME,
+ method: "getMonitorJob",
+ stack: error instanceof Error ? error.stack : undefined,
+ });
+ });
+ }
} catch (error: unknown) {
this.logger.warn({
message: error instanceof Error ? error.message : "Unknown error",
diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts
index c75477c88c..9938443453 100644
--- a/server/src/service/infrastructure/notificationsService.ts
+++ b/server/src/service/infrastructure/notificationsService.ts
@@ -1,6 +1,6 @@
-import type { Monitor, MonitorStatusResponse, Notification } from "@/types/index.js";
+import type { Incident, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
-import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js";
+import { IIncidentsRepository, IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js";
import { INotificationProvider } from "./notificationProviders/INotificationProvider.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { ISettingsService } from "@/service/system/settingsService.js";
@@ -14,6 +14,7 @@ export interface INotificationsService {
updateById(id: string, teamId: string, updateData: Partial): Promise;
deleteById: (id: string, teamId: string) => Promise;
handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => Promise;
+ handleEscalationNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, incident: Incident) => Promise;
sendTestNotification: (notification: Partial) => Promise;
testAllNotifications: (notificationIds: string[]) => Promise;
@@ -25,6 +26,7 @@ export class NotificationsService implements INotificationsService {
static SERVICE_NAME = SERVICE_NAME;
private notificationsRepository: INotificationsRepository;
+ private incidentsRepository: IIncidentsRepository;
private monitorsRepository: IMonitorsRepository;
private webhookProvider: INotificationProvider;
private emailProvider: INotificationProvider;
@@ -40,6 +42,7 @@ export class NotificationsService implements INotificationsService {
constructor(
notificationsRepository: INotificationsRepository,
monitorsRepository: IMonitorsRepository,
+ incidentsRepository: IIncidentsRepository,
webhookProvider: INotificationProvider,
emailProvider: INotificationProvider,
slackProvider: INotificationProvider,
@@ -51,6 +54,7 @@ export class NotificationsService implements INotificationsService {
logger: ILogger,
notificationMessageBuilder: INotificationMessageBuilder
) {
+ this.incidentsRepository = incidentsRepository;
this.notificationsRepository = notificationsRepository;
this.monitorsRepository = monitorsRepository;
this.webhookProvider = webhookProvider;
@@ -107,8 +111,17 @@ export class NotificationsService implements INotificationsService {
}
};
- private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => {
- const notificationIds = monitor.notifications ?? [];
+ private sendNotificationObjects = async (
+ notificationObjects: Array<{ delayMinutes: number; channelId: string }>,
+ monitor: Monitor,
+ monitorStatusResponse: MonitorStatusResponse,
+ decision: MonitorActionDecision
+ ) => {
+ if (!notificationObjects.length) {
+ return [] as string[];
+ }
+
+ const notificationIds = notificationObjects.map((notification) => notification.channelId);
const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds);
// Build notification message once for all notifications
@@ -116,20 +129,31 @@ export class NotificationsService implements INotificationsService {
const clientHost = settings.clientHost || "Host not defined";
const notificationMessage = this.notificationMessageBuilder.buildMessage(monitor, monitorStatusResponse, decision, clientHost);
- const tasks = notifications.map((notification) => this.send(notification, monitor, monitorStatusResponse, decision, notificationMessage));
+ const outcomes = await Promise.all(
+ notifications.map((notification) => this.send(notification, monitor, monitorStatusResponse, decision, notificationMessage))
+ );
- const outcomes = await Promise.all(tasks);
- const succeeded = outcomes.filter(Boolean).length;
- const failed = outcomes.length - succeeded;
- if (failed > 0) {
+ const sentNotificationIds = notifications
+ .filter((_, index) => outcomes[index])
+ .map((notification) => notification.id);
+
+ const succeededCount = sentNotificationIds.length;
+ const failedCount = notifications.length - succeededCount;
+ if (failedCount > 0) {
this.logger.warn({
- message: `Notification send completed with ${succeeded} success, ${failed} failure(s)`,
+ message: `Notification send completed with ${succeededCount} success, ${failedCount} failure(s)`,
service: SERVICE_NAME,
- method: "sendNotifications",
+ method: "sendNotificationObjects",
});
}
- // Return true if all notifications succeeded
- return succeeded === notifications.length;
+
+ return sentNotificationIds;
+ };
+
+ private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => {
+ const immediateNotifications = (monitor.notifications ?? []).filter((notification) => notification.delayMinutes <= 0);
+ const sentChannelIds = await this.sendNotificationObjects(immediateNotifications, monitor, monitorStatusResponse, decision);
+ return sentChannelIds.length > 0;
};
handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => {
@@ -137,10 +161,54 @@ export class NotificationsService implements INotificationsService {
return false;
}
- // Send notifications based on decision
+ // Send only immediate notifications on status change.
return await this.sendNotifications(monitor, monitorStatusResponse, decision);
};
+ handleEscalationNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, incident: Incident) => {
+ if (!incident || !incident.status || incident.acknowledged) {
+ return false;
+ }
+
+ const incidentStart = new Date(incident.startTime).getTime();
+ if (Number.isNaN(incidentStart)) {
+ return false;
+ }
+
+ const dueNotifications = (monitor.notifications ?? []).filter((notification) => {
+ if (notification.delayMinutes <= 0) {
+ return false;
+ }
+ if (incident.escalatedNotificationIds?.includes(notification.channelId)) {
+ return false;
+ }
+ return Date.now() - incidentStart >= notification.delayMinutes * 60 * 1000;
+ });
+
+ if (!dueNotifications.length) {
+ return false;
+ }
+
+ const escalationDecision: MonitorActionDecision = {
+ shouldCreateIncident: false,
+ shouldResolveIncident: false,
+ shouldSendNotification: true,
+ incidentReason: monitor.status === "breached" ? "threshold_breach" : "status_down",
+ notificationReason: "status_change",
+ };
+
+ const sentChannelIds = await this.sendNotificationObjects(dueNotifications, monitor, monitorStatusResponse, escalationDecision);
+
+ if (!sentChannelIds.length) {
+ return false;
+ }
+
+ const updatedEscalatedChannelIds = Array.from(new Set([...(incident.escalatedNotificationIds ?? []), ...sentChannelIds]));
+ await this.incidentsRepository.updateById(incident.id, incident.teamId, { escalatedNotificationIds: updatedEscalatedChannelIds });
+
+ return true;
+ };
+
sendTestNotification = async (notification: Partial) => {
switch (notification.type) {
case "email":
diff --git a/server/src/types/incident.ts b/server/src/types/incident.ts
index 6b076ff835..c0c068591d 100644
--- a/server/src/types/incident.ts
+++ b/server/src/types/incident.ts
@@ -1,4 +1,5 @@
// export type IncidentResolutionType = "automatic" | "manual" | null;
+import type { Types } from "mongoose";
export const IncidentResolutionTypes = ["automatic", "manual", null] as const;
export type IncidentResolutionType = (typeof IncidentResolutionTypes)[number];
@@ -18,6 +19,11 @@ export interface Incident {
comment?: string | null;
createdAt: string;
updatedAt: string;
+ acknowledged: boolean;
+ acknowledgedBy?: string | null;
+ acknowledgedAt: string | null;
+ acknowledgedByEmail?: string | null;
+ escalatedNotificationIds: string[];
}
export interface IncidentSummaryTopMonitor {
diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts
index f29ce75d78..f5e81e9908 100644
--- a/server/src/types/monitor.ts
+++ b/server/src/types/monitor.ts
@@ -15,6 +15,11 @@ export type MonitorStatus = (typeof MonitorStatuses)[number];
export const MonitorMatchMethods = ["equal", "include", "regex"] as const;
export type MonitorMatchMethod = (typeof MonitorMatchMethods)[number] | "";
+export interface EscalationNotification {
+ delayMinutes: number;
+ channelId: string;
+}
+
export interface Monitor {
id: string;
userId: string;
@@ -36,7 +41,7 @@ export interface Monitor {
isActive: boolean;
interval: number;
uptimePercentage?: number;
- notifications: string[];
+ notifications: EscalationNotification[];
secret?: string;
cpuAlertThreshold: number;
cpuAlertCounter: number;
diff --git a/server/src/validation/incidentValidation.ts b/server/src/validation/incidentValidation.ts
index acbc857dc0..855fd9bd21 100644
--- a/server/src/validation/incidentValidation.ts
+++ b/server/src/validation/incidentValidation.ts
@@ -13,8 +13,13 @@ export const getIncidentsByTeamQueryValidation = z.object({
status: booleanCoercion.optional(),
monitorId: z.string().optional(),
resolutionType: z.enum(["manual", "automatic"]).optional(),
+ acknowledged: booleanCoercion.optional(),
});
export const getIncidentSummaryQueryValidation = z.object({
limit: z.coerce.number().int().min(1).optional(),
});
+
+export const acknowledgeIncidentValidation = z.object({
+ comment: z.string().max(500).optional(),
+});
diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts
index df000ecef2..0cf6ba8b4e 100644
--- a/server/src/validation/monitorValidation.ts
+++ b/server/src/validation/monitorValidation.ts
@@ -66,7 +66,10 @@ export const createMonitorBodyValidation = z.object({
memoryAlertThreshold: z.number().optional(),
diskAlertThreshold: z.number().optional(),
tempAlertThreshold: z.number().optional(),
- notifications: z.array(z.string()).optional(),
+ notifications: z.array(z.object({
+ delayMinutes: z.number().min(0),
+ channelId: z.string().min(1),
+ })).optional(),
secret: z.string().optional(),
jsonPath: z.union([z.string(), z.literal("")]).optional(),
expectedValue: z.union([z.string(), z.literal("")]).optional(),
@@ -88,7 +91,10 @@ export const editMonitorBodyValidation = z.object({
statusWindowThreshold: z.number().min(1).max(100).default(60),
description: z.union([z.string(), z.literal("")]).optional(),
interval: z.number().optional(),
- notifications: z.array(z.string()).optional(),
+ notifications: z.array(z.object({
+ delayMinutes: z.number().min(0),
+ channelId: z.string().min(1),
+ })).optional(),
secret: z.string().optional(),
ignoreTlsErrors: z.boolean().optional(),
useAdvancedMatching: z.boolean().optional(),
@@ -143,7 +149,10 @@ const importedMonitorSchema = z.object({
isActive: z.boolean().default(true),
interval: z.number().default(60000),
uptimePercentage: z.number().optional(),
- notifications: z.array(z.string()).default([]),
+ notifications: z.array(z.object({
+ delayMinutes: z.number().min(0),
+ channelId: z.string().min(1),
+ })).default([]),
secret: z.string().optional(),
cpuAlertThreshold: z.number().default(100),
cpuAlertCounter: z.number().default(5),