@oneuptime/common 11.3.21 → 11.3.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/Models/AnalyticsModels/AnalyticsBaseModel/CommonModel.ts +2 -0
  2. package/Models/AnalyticsModels/ExceptionInstance.ts +1 -0
  3. package/Models/AnalyticsModels/Log.ts +1 -0
  4. package/Models/AnalyticsModels/Metric.ts +1 -0
  5. package/Models/AnalyticsModels/MonitorLog.ts +1 -0
  6. package/Models/AnalyticsModels/Span.ts +1 -0
  7. package/Models/DatabaseModels/Alert.ts +100 -0
  8. package/Models/DatabaseModels/AlertInternalNote.ts +2 -0
  9. package/Models/DatabaseModels/AlertReminderRule.ts +597 -0
  10. package/Models/DatabaseModels/AlertSeverity.ts +2 -0
  11. package/Models/DatabaseModels/AlertState.ts +2 -0
  12. package/Models/DatabaseModels/AlertStateTimeline.ts +2 -0
  13. package/Models/DatabaseModels/Incident.ts +100 -0
  14. package/Models/DatabaseModels/IncidentInternalNote.ts +2 -0
  15. package/Models/DatabaseModels/IncidentPublicNote.ts +2 -0
  16. package/Models/DatabaseModels/IncidentReminderRule.ts +597 -0
  17. package/Models/DatabaseModels/IncidentSeverity.ts +2 -0
  18. package/Models/DatabaseModels/IncidentStateTimeline.ts +2 -0
  19. package/Models/DatabaseModels/Index.ts +6 -0
  20. package/Models/DatabaseModels/Label.ts +2 -0
  21. package/Models/DatabaseModels/MonitorStatusTimeline.ts +2 -0
  22. package/Models/DatabaseModels/ScheduledMaintenance.ts +100 -0
  23. package/Models/DatabaseModels/ScheduledMaintenanceReminderRule.ts +585 -0
  24. package/Models/DatabaseModels/ScheduledMaintenanceState.ts +2 -0
  25. package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +2 -0
  26. package/Models/DatabaseModels/StatusPageAnnouncement.ts +2 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.ts +211 -0
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.ts +113 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.ts +20 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.ts +69 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  32. package/Server/Services/AlertReminderRuleService.ts +253 -0
  33. package/Server/Services/AlertService.ts +106 -0
  34. package/Server/Services/IncidentReminderRuleService.ts +256 -0
  35. package/Server/Services/IncidentService.ts +143 -0
  36. package/Server/Services/Index.ts +6 -0
  37. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +236 -0
  38. package/Server/Services/ScheduledMaintenanceService.ts +127 -0
  39. package/Server/Services/TelemetryExceptionService.ts +123 -0
  40. package/Server/Services/UserNotificationSettingService.ts +24 -0
  41. package/Server/Utils/AnalyticsDatabase/Statement.ts +7 -3
  42. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +35 -0
  43. package/Server/Utils/EventLoop.ts +28 -0
  44. package/Server/Utils/WhatsAppTemplateUtil.ts +10 -0
  45. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +49 -0
  46. package/Types/Email/EmailTemplateType.ts +3 -0
  47. package/Types/NotificationSetting/NotificationSettingEventType.ts +3 -0
  48. package/Types/Permission.ts +144 -0
  49. package/Types/Reminder/ReminderStopState.ts +6 -0
  50. package/Types/Reminder/ScheduledMaintenanceReminderStopState.ts +6 -0
  51. package/Types/WhatsApp/WhatsAppTemplates.ts +14 -0
  52. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/CommonModel.js.map +1 -1
  53. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -0
  54. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  55. package/build/dist/Models/AnalyticsModels/Log.js +1 -0
  56. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  57. package/build/dist/Models/AnalyticsModels/Metric.js +1 -0
  58. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  59. package/build/dist/Models/AnalyticsModels/MonitorLog.js +1 -0
  60. package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
  61. package/build/dist/Models/AnalyticsModels/Span.js +1 -0
  62. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  63. package/build/dist/Models/DatabaseModels/Alert.js +103 -0
  64. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  65. package/build/dist/Models/DatabaseModels/AlertInternalNote.js +2 -0
  66. package/build/dist/Models/DatabaseModels/AlertInternalNote.js.map +1 -1
  67. package/build/dist/Models/DatabaseModels/AlertReminderRule.js +605 -0
  68. package/build/dist/Models/DatabaseModels/AlertReminderRule.js.map +1 -0
  69. package/build/dist/Models/DatabaseModels/AlertSeverity.js +2 -0
  70. package/build/dist/Models/DatabaseModels/AlertSeverity.js.map +1 -1
  71. package/build/dist/Models/DatabaseModels/AlertState.js +2 -0
  72. package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
  73. package/build/dist/Models/DatabaseModels/AlertStateTimeline.js +2 -0
  74. package/build/dist/Models/DatabaseModels/AlertStateTimeline.js.map +1 -1
  75. package/build/dist/Models/DatabaseModels/Incident.js +103 -0
  76. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  77. package/build/dist/Models/DatabaseModels/IncidentInternalNote.js +2 -0
  78. package/build/dist/Models/DatabaseModels/IncidentInternalNote.js.map +1 -1
  79. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +2 -0
  80. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  81. package/build/dist/Models/DatabaseModels/IncidentReminderRule.js +605 -0
  82. package/build/dist/Models/DatabaseModels/IncidentReminderRule.js.map +1 -0
  83. package/build/dist/Models/DatabaseModels/IncidentSeverity.js +2 -0
  84. package/build/dist/Models/DatabaseModels/IncidentSeverity.js.map +1 -1
  85. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +2 -0
  86. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  87. package/build/dist/Models/DatabaseModels/Index.js +6 -0
  88. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  89. package/build/dist/Models/DatabaseModels/Label.js +2 -0
  90. package/build/dist/Models/DatabaseModels/Label.js.map +1 -1
  91. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +2 -0
  92. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  93. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +103 -0
  94. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  95. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js +596 -0
  96. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js.map +1 -0
  97. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceState.js +2 -0
  98. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceState.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +2 -0
  100. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +2 -0
  102. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  103. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.js +78 -0
  104. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.js.map +1 -0
  105. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.js +44 -0
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.js.map +1 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.js +12 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.js.map +1 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.js +30 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.js.map +1 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  113. package/build/dist/Server/Services/AlertReminderRuleService.js +228 -0
  114. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -0
  115. package/build/dist/Server/Services/AlertService.js +87 -0
  116. package/build/dist/Server/Services/AlertService.js.map +1 -1
  117. package/build/dist/Server/Services/IncidentReminderRuleService.js +231 -0
  118. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -0
  119. package/build/dist/Server/Services/IncidentService.js +116 -0
  120. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  121. package/build/dist/Server/Services/Index.js +6 -0
  122. package/build/dist/Server/Services/Index.js.map +1 -1
  123. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +214 -0
  124. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -0
  125. package/build/dist/Server/Services/ScheduledMaintenanceService.js +102 -0
  126. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  127. package/build/dist/Server/Services/TelemetryExceptionService.js +101 -0
  128. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  129. package/build/dist/Server/Services/UserNotificationSettingService.js +6 -0
  130. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  131. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +4 -2
  132. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  133. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -0
  134. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  135. package/build/dist/Server/Utils/EventLoop.js +29 -0
  136. package/build/dist/Server/Utils/EventLoop.js.map +1 -0
  137. package/build/dist/Server/Utils/WhatsAppTemplateUtil.js +6 -0
  138. package/build/dist/Server/Utils/WhatsAppTemplateUtil.js.map +1 -1
  139. package/build/dist/Types/Email/EmailTemplateType.js +3 -0
  140. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  141. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
  142. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
  143. package/build/dist/Types/Permission.js +126 -0
  144. package/build/dist/Types/Permission.js.map +1 -1
  145. package/build/dist/Types/Reminder/ReminderStopState.js +7 -0
  146. package/build/dist/Types/Reminder/ReminderStopState.js.map +1 -0
  147. package/build/dist/Types/Reminder/ScheduledMaintenanceReminderStopState.js +7 -0
  148. package/build/dist/Types/Reminder/ScheduledMaintenanceReminderStopState.js.map +1 -0
  149. package/build/dist/Types/WhatsApp/WhatsAppTemplates.js +9 -0
  150. package/build/dist/Types/WhatsApp/WhatsAppTemplates.js.map +1 -1
  151. package/package.json +1 -1
@@ -56,6 +56,8 @@ import logger, { LogAttributes } from "../Utils/Logger";
56
56
  import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
57
57
  import IncidentFeedService from "./IncidentFeedService";
58
58
  import IncidentSlaService from "./IncidentSlaService";
59
+ import IncidentReminderRuleService from "./IncidentReminderRuleService";
60
+ import IncidentReminderRule from "../../Models/DatabaseModels/IncidentReminderRule";
59
61
  import { setIsPublicForMarkdownImages } from "../Utils/InlineImageAccessTokenSync";
60
62
  import { IncidentFeedEventType } from "../../Models/DatabaseModels/IncidentFeed";
61
63
  import IncidentGroupingEngineService from "./IncidentGroupingEngineService";
@@ -230,6 +232,64 @@ export class Service extends DatabaseService<Model> {
230
232
  return false;
231
233
  }
232
234
 
235
+ @CaptureSpan()
236
+ public async refreshReminderSchedule(data: {
237
+ incidentId: ObjectID;
238
+ projectId: ObjectID;
239
+ }): Promise<void> {
240
+ const incident: Model | null = await this.findOneById({
241
+ id: data.incidentId,
242
+ select: {
243
+ enableReminders: true,
244
+ incidentSeverityId: true,
245
+ labels: {
246
+ _id: true,
247
+ },
248
+ },
249
+ props: {
250
+ isRoot: true,
251
+ },
252
+ });
253
+
254
+ if (!incident) {
255
+ return;
256
+ }
257
+
258
+ let nextReminderNotificationAt: Date | null = null;
259
+
260
+ if (incident.enableReminders !== false) {
261
+ const matchingRule: IncidentReminderRule | null =
262
+ await IncidentReminderRuleService.findMatchingRule({
263
+ projectId: data.projectId,
264
+ incidentSeverityId: incident.incidentSeverityId,
265
+ labelIds: incident.labels?.map((label: Label) => {
266
+ return label.id!;
267
+ }),
268
+ });
269
+
270
+ if (
271
+ matchingRule &&
272
+ matchingRule.reminderIntervalInMinutes &&
273
+ !(await this.isIncidentResolved({ incidentId: data.incidentId }))
274
+ ) {
275
+ nextReminderNotificationAt = OneUptimeDate.addRemoveMinutes(
276
+ OneUptimeDate.getCurrentDate(),
277
+ matchingRule.reminderIntervalInMinutes,
278
+ );
279
+ }
280
+ }
281
+
282
+ await this.updateOneById({
283
+ id: data.incidentId,
284
+ data: {
285
+ nextReminderNotificationAt: nextReminderNotificationAt,
286
+ },
287
+ props: {
288
+ isRoot: true,
289
+ },
290
+ });
291
+ }
292
+
233
293
  @CaptureSpan()
234
294
  public async resolveIncident(
235
295
  incidentId: ObjectID,
@@ -1161,6 +1221,26 @@ export class Service extends DatabaseService<Model> {
1161
1221
  );
1162
1222
  }
1163
1223
  })
1224
+ .then(async () => {
1225
+ // Schedule reminder notifications for incident if a matching reminder rule exists
1226
+ try {
1227
+ if (createdItem.projectId && createdItem.id) {
1228
+ await this.refreshReminderSchedule({
1229
+ incidentId: createdItem.id,
1230
+ projectId: createdItem.projectId,
1231
+ });
1232
+ }
1233
+ } catch (error) {
1234
+ logger.error(
1235
+ `Reminder scheduling failed in IncidentService.onCreateSuccess: ${error}`,
1236
+ {
1237
+ projectId: createdItem.projectId?.toString(),
1238
+ incidentId: createdItem.id?.toString(),
1239
+ userId: createdItem.createdByUserId?.toString(),
1240
+ } as LogAttributes,
1241
+ );
1242
+ }
1243
+ })
1164
1244
  .catch((error: Error) => {
1165
1245
  logger.error(
1166
1246
  `Critical error in IncidentService sequential operations: ${error}`,
@@ -1728,6 +1808,29 @@ ${incident.remediationNotes || "No remediation notes provided."}
1728
1808
  });
1729
1809
  }
1730
1810
 
1811
+ // Re-evaluate reminder schedule when reminders are enabled or disabled for this incident
1812
+ if (
1813
+ Object.prototype.hasOwnProperty.call(
1814
+ updatedIncidentData,
1815
+ "enableReminders",
1816
+ )
1817
+ ) {
1818
+ try {
1819
+ await this.refreshReminderSchedule({
1820
+ incidentId: incidentId,
1821
+ projectId: projectId,
1822
+ });
1823
+ } catch (reminderError) {
1824
+ logger.error(
1825
+ `Reminder rescheduling failed in IncidentService.onUpdateSuccess: ${reminderError}`,
1826
+ {
1827
+ projectId: projectId?.toString(),
1828
+ incidentId: incidentId?.toString(),
1829
+ } as LogAttributes,
1830
+ );
1831
+ }
1832
+ }
1833
+
1731
1834
  // emit postmortem completion time metric when postmortemPostedAt is set
1732
1835
  if (
1733
1836
  Object.prototype.hasOwnProperty.call(
@@ -1976,6 +2079,30 @@ ${labels
1976
2079
  }
1977
2080
  }
1978
2081
 
2082
+ /*
2083
+ * Re-match reminder rule on any labels change (including clearing all
2084
+ * labels), since labels can change which reminder rule matches.
2085
+ */
2086
+ if (
2087
+ updatedIncidentData.labels &&
2088
+ Array.isArray(updatedIncidentData.labels)
2089
+ ) {
2090
+ try {
2091
+ await this.refreshReminderSchedule({
2092
+ incidentId: incidentId,
2093
+ projectId: projectId,
2094
+ });
2095
+ } catch (reminderError) {
2096
+ logger.error(
2097
+ `Reminder rescheduling failed in IncidentService.onUpdateSuccess: ${reminderError}`,
2098
+ {
2099
+ projectId: projectId?.toString(),
2100
+ incidentId: incidentId?.toString(),
2101
+ } as LogAttributes,
2102
+ );
2103
+ }
2104
+ }
2105
+
1979
2106
  if (
1980
2107
  updatedIncidentData.incidentSeverity &&
1981
2108
  (updatedIncidentData.incidentSeverity as any)._id
@@ -2017,6 +2144,22 @@ ${incidentSeverity.name}
2017
2144
  );
2018
2145
  }
2019
2146
 
2147
+ // Re-match reminder rule when severity changes
2148
+ try {
2149
+ await this.refreshReminderSchedule({
2150
+ incidentId: incidentId,
2151
+ projectId: projectId,
2152
+ });
2153
+ } catch (reminderError) {
2154
+ logger.error(
2155
+ `Reminder rescheduling failed in IncidentService.onUpdateSuccess: ${reminderError}`,
2156
+ {
2157
+ projectId: projectId?.toString(),
2158
+ incidentId: incidentId?.toString(),
2159
+ } as LogAttributes,
2160
+ );
2161
+ }
2162
+
2020
2163
  // emit severity change metric
2021
2164
  try {
2022
2165
  const severityChangeMetric: MutableMetric = new MutableMetric();
@@ -226,6 +226,9 @@ import IncidentEpisodePublicNoteService from "./IncidentEpisodePublicNoteService
226
226
  import AlertGroupingRuleService from "./AlertGroupingRuleService";
227
227
  import IncidentSlaRuleService from "./IncidentSlaRuleService";
228
228
  import IncidentSlaService from "./IncidentSlaService";
229
+ import IncidentReminderRuleService from "./IncidentReminderRuleService";
230
+ import AlertReminderRuleService from "./AlertReminderRuleService";
231
+ import ScheduledMaintenanceReminderRuleService from "./ScheduledMaintenanceReminderRuleService";
229
232
 
230
233
  import TableViewService from "./TableViewService";
231
234
  import ScheduledMaintenanceFeedService from "./ScheduledMaintenanceFeedService";
@@ -481,6 +484,9 @@ const services: Array<BaseService> = [
481
484
  AlertGroupingRuleService,
482
485
  IncidentSlaRuleService,
483
486
  IncidentSlaService,
487
+ IncidentReminderRuleService,
488
+ AlertReminderRuleService,
489
+ ScheduledMaintenanceReminderRuleService,
484
490
 
485
491
  TableViewService,
486
492
  MonitorTestService,
@@ -0,0 +1,236 @@
1
+ import CreateBy from "../Types/Database/CreateBy";
2
+ import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
3
+ import SortOrder from "../../Types/BaseDatabase/SortOrder";
4
+ import DatabaseService from "./DatabaseService";
5
+ import Model from "../../Models/DatabaseModels/ScheduledMaintenanceReminderRule";
6
+ import ScheduledMaintenance from "../../Models/DatabaseModels/ScheduledMaintenance";
7
+ import Label from "../../Models/DatabaseModels/Label";
8
+ import ScheduledMaintenanceState from "../../Models/DatabaseModels/ScheduledMaintenanceState";
9
+ import ScheduledMaintenanceService from "./ScheduledMaintenanceService";
10
+ import ScheduledMaintenanceStateService from "./ScheduledMaintenanceStateService";
11
+ import QueryHelper from "../Types/Database/QueryHelper";
12
+ import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
13
+ import ObjectID from "../../Types/ObjectID";
14
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
15
+ import logger, { LogAttributes } from "../Utils/Logger";
16
+ import { IsBillingEnabled } from "../EnvironmentConfig";
17
+
18
+ export class Service extends DatabaseService<Model> {
19
+ public constructor() {
20
+ super(Model);
21
+ if (IsBillingEnabled) {
22
+ this.hardDeleteItemsOlderThanInDays("createdAt", 3 * 365); // 3 years
23
+ }
24
+ }
25
+
26
+ @CaptureSpan()
27
+ protected override async onBeforeCreate(
28
+ createBy: CreateBy<Model>,
29
+ ): Promise<OnCreate<Model>> {
30
+ // Auto-assign order on creation
31
+ if (!createBy.data.order) {
32
+ const highestOrderRule: Model | null = await this.findOneBy({
33
+ query: {
34
+ projectId: createBy.data.projectId!,
35
+ },
36
+ select: {
37
+ order: true,
38
+ },
39
+ sort: {
40
+ order: SortOrder.Descending,
41
+ },
42
+ props: {
43
+ isRoot: true,
44
+ },
45
+ });
46
+
47
+ createBy.data.order = (highestOrderRule?.order || 0) + 1;
48
+ }
49
+
50
+ return {
51
+ createBy,
52
+ carryForward: null,
53
+ };
54
+ }
55
+
56
+ @CaptureSpan()
57
+ protected override async onCreateSuccess(
58
+ _onCreate: OnCreate<Model>,
59
+ createdItem: Model,
60
+ ): Promise<Model> {
61
+ if (createdItem.projectId) {
62
+ await this.refreshSchedulesForOpenScheduledMaintenances(
63
+ createdItem.projectId,
64
+ );
65
+ }
66
+
67
+ return createdItem;
68
+ }
69
+
70
+ @CaptureSpan()
71
+ protected override async onUpdateSuccess(
72
+ onUpdate: OnUpdate<Model>,
73
+ _updatedItemIds: Array<ObjectID>,
74
+ ): Promise<OnUpdate<Model>> {
75
+ const projectId: ObjectID | undefined = onUpdate.updateBy.props.tenantId as
76
+ | ObjectID
77
+ | undefined;
78
+
79
+ if (projectId) {
80
+ await this.refreshSchedulesForOpenScheduledMaintenances(projectId);
81
+ }
82
+
83
+ return onUpdate;
84
+ }
85
+
86
+ @CaptureSpan()
87
+ protected override async onDeleteSuccess(
88
+ onDelete: OnDelete<Model>,
89
+ _itemIdsBeforeDelete: Array<ObjectID>,
90
+ ): Promise<OnDelete<Model>> {
91
+ const projectId: ObjectID | undefined = onDelete.deleteBy.props.tenantId as
92
+ | ObjectID
93
+ | undefined;
94
+
95
+ if (projectId) {
96
+ await this.refreshSchedulesForOpenScheduledMaintenances(projectId);
97
+ }
98
+
99
+ return onDelete;
100
+ }
101
+
102
+ @CaptureSpan()
103
+ public async refreshSchedulesForOpenScheduledMaintenances(
104
+ projectId: ObjectID,
105
+ ): Promise<void> {
106
+ try {
107
+ const openStates: Array<ScheduledMaintenanceState> =
108
+ await ScheduledMaintenanceStateService.findBy({
109
+ query: {
110
+ projectId: projectId,
111
+ isResolvedState: false,
112
+ },
113
+ select: {
114
+ _id: true,
115
+ },
116
+ limit: LIMIT_PER_PROJECT,
117
+ skip: 0,
118
+ props: {
119
+ isRoot: true,
120
+ },
121
+ });
122
+
123
+ const openStateIds: Array<ObjectID> = openStates
124
+ .map((state: ScheduledMaintenanceState) => {
125
+ return state.id!;
126
+ })
127
+ .filter(Boolean);
128
+
129
+ if (openStateIds.length === 0) {
130
+ return;
131
+ }
132
+
133
+ const openScheduledMaintenances: Array<ScheduledMaintenance> =
134
+ await ScheduledMaintenanceService.findBy({
135
+ query: {
136
+ projectId: projectId,
137
+ currentScheduledMaintenanceStateId: QueryHelper.any(openStateIds),
138
+ },
139
+ select: {
140
+ _id: true,
141
+ },
142
+ limit: LIMIT_MAX,
143
+ skip: 0,
144
+ props: {
145
+ isRoot: true,
146
+ },
147
+ });
148
+
149
+ for (const scheduledMaintenance of openScheduledMaintenances) {
150
+ try {
151
+ await ScheduledMaintenanceService.refreshReminderSchedule({
152
+ scheduledMaintenanceId: scheduledMaintenance.id!,
153
+ projectId: projectId,
154
+ });
155
+ } catch (error) {
156
+ logger.error(
157
+ `Failed to refresh reminder schedule for scheduled maintenance ${scheduledMaintenance.id}: ${error}`,
158
+ { projectId: projectId?.toString() } as LogAttributes,
159
+ );
160
+ }
161
+ }
162
+ } catch (error) {
163
+ logger.error(
164
+ `Failed to refresh reminder schedules for open scheduled maintenances: ${error}`,
165
+ { projectId: projectId?.toString() } as LogAttributes,
166
+ );
167
+ }
168
+ }
169
+
170
+ @CaptureSpan()
171
+ public async findMatchingRule(data: {
172
+ projectId: ObjectID;
173
+ labelIds?: Array<ObjectID> | undefined;
174
+ }): Promise<Model | null> {
175
+ // Get all enabled rules sorted by order. First matching rule wins.
176
+ const rules: Array<Model> = await this.findBy({
177
+ query: {
178
+ projectId: data.projectId,
179
+ isEnabled: true,
180
+ },
181
+ sort: {
182
+ order: SortOrder.Ascending,
183
+ },
184
+ select: {
185
+ _id: true,
186
+ name: true,
187
+ order: true,
188
+ reminderIntervalInMinutes: true,
189
+ stopRemindersOnState: true,
190
+ remindWhileScheduled: true,
191
+ labels: {
192
+ _id: true,
193
+ },
194
+ },
195
+ limit: 100,
196
+ skip: 0,
197
+ props: {
198
+ isRoot: true,
199
+ },
200
+ });
201
+
202
+ for (const rule of rules) {
203
+ if (rule.labels && rule.labels.length > 0) {
204
+ if (!data.labelIds || data.labelIds.length === 0) {
205
+ continue;
206
+ }
207
+
208
+ const ruleLabelIds: Array<string> = rule.labels.map((label: Label) => {
209
+ return label.id?.toString() || "";
210
+ });
211
+
212
+ const hasMatchingLabel: boolean = data.labelIds.some(
213
+ (labelId: ObjectID) => {
214
+ return ruleLabelIds.includes(labelId.toString());
215
+ },
216
+ );
217
+
218
+ if (!hasMatchingLabel) {
219
+ continue;
220
+ }
221
+ }
222
+
223
+ // Rule with no labels matches all scheduled maintenances.
224
+ logger.debug(
225
+ `Scheduled maintenance reminder rule ${rule.name || rule.id} matched for project ${data.projectId}`,
226
+ { projectId: data.projectId?.toString() } as LogAttributes,
227
+ );
228
+
229
+ return rule;
230
+ }
231
+
232
+ return null;
233
+ }
234
+ }
235
+
236
+ export default new Service();
@@ -8,6 +8,8 @@ import ScheduledMaintenanceOwnerTeamService from "./ScheduledMaintenanceOwnerTea
8
8
  import ScheduledMaintenanceOwnerUserService from "./ScheduledMaintenanceOwnerUserService";
9
9
  import ScheduledMaintenanceStateService from "./ScheduledMaintenanceStateService";
10
10
  import ScheduledMaintenanceStateTimelineService from "./ScheduledMaintenanceStateTimelineService";
11
+ import ScheduledMaintenanceReminderRuleService from "./ScheduledMaintenanceReminderRuleService";
12
+ import ScheduledMaintenanceReminderRule from "../../Models/DatabaseModels/ScheduledMaintenanceReminderRule";
11
13
  import TeamMemberService from "./TeamMemberService";
12
14
  import URL from "../../Types/API/URL";
13
15
  import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
@@ -809,6 +811,80 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
809
811
  return { createBy, carryForward: null };
810
812
  }
811
813
 
814
+ @CaptureSpan()
815
+ public async refreshReminderSchedule(data: {
816
+ scheduledMaintenanceId: ObjectID;
817
+ projectId: ObjectID;
818
+ }): Promise<void> {
819
+ const scheduledMaintenance: Model | null = await this.findOneById({
820
+ id: data.scheduledMaintenanceId,
821
+ select: {
822
+ enableReminders: true,
823
+ startsAt: true,
824
+ labels: {
825
+ _id: true,
826
+ },
827
+ },
828
+ props: {
829
+ isRoot: true,
830
+ },
831
+ });
832
+
833
+ if (!scheduledMaintenance) {
834
+ return;
835
+ }
836
+
837
+ let nextReminderNotificationAt: Date | null = null;
838
+
839
+ if (scheduledMaintenance.enableReminders !== false) {
840
+ const matchingRule: ScheduledMaintenanceReminderRule | null =
841
+ await ScheduledMaintenanceReminderRuleService.findMatchingRule({
842
+ projectId: data.projectId,
843
+ labelIds: scheduledMaintenance.labels?.map((label: Label) => {
844
+ return label.id!;
845
+ }),
846
+ });
847
+
848
+ if (
849
+ matchingRule &&
850
+ matchingRule.reminderIntervalInMinutes &&
851
+ !(await this.isScheduledMaintenanceCompleted({
852
+ scheduledMaintenanceId: data.scheduledMaintenanceId,
853
+ }))
854
+ ) {
855
+ /*
856
+ * When the rule does not remind while the event is still scheduled,
857
+ * defer the first reminder until after the event has started so that
858
+ * owners are not notified about an event that has not begun yet.
859
+ */
860
+ let referenceDate: Date = OneUptimeDate.getCurrentDate();
861
+
862
+ if (
863
+ !matchingRule.remindWhileScheduled &&
864
+ scheduledMaintenance.startsAt &&
865
+ OneUptimeDate.isInTheFuture(scheduledMaintenance.startsAt)
866
+ ) {
867
+ referenceDate = scheduledMaintenance.startsAt;
868
+ }
869
+
870
+ nextReminderNotificationAt = OneUptimeDate.addRemoveMinutes(
871
+ referenceDate,
872
+ matchingRule.reminderIntervalInMinutes,
873
+ );
874
+ }
875
+ }
876
+
877
+ await this.updateOneById({
878
+ id: data.scheduledMaintenanceId,
879
+ data: {
880
+ nextReminderNotificationAt: nextReminderNotificationAt,
881
+ },
882
+ props: {
883
+ isRoot: true,
884
+ },
885
+ });
886
+ }
887
+
812
888
  @CaptureSpan()
813
889
  protected override async onCreateSuccess(
814
890
  onCreate: OnCreate<Model>,
@@ -985,6 +1061,26 @@ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
985
1061
  );
986
1062
  }
987
1063
  })
1064
+ .then(async () => {
1065
+ // Schedule reminder notifications for scheduled maintenance if a matching reminder rule exists
1066
+ try {
1067
+ if (createdItem.projectId && createdItem.id) {
1068
+ await this.refreshReminderSchedule({
1069
+ scheduledMaintenanceId: createdItem.id,
1070
+ projectId: createdItem.projectId,
1071
+ });
1072
+ }
1073
+ } catch (error) {
1074
+ logger.error(
1075
+ `Reminder scheduling failed in ScheduledMaintenanceService.onCreateSuccess: ${error}`,
1076
+ {
1077
+ projectId: createdItem.projectId?.toString(),
1078
+ scheduledMaintenanceId: createdItem.id?.toString(),
1079
+ userId: createdItem.createdByUserId?.toString(),
1080
+ } as LogAttributes,
1081
+ );
1082
+ }
1083
+ })
988
1084
  .catch((error: Error) => {
989
1085
  logger.error(
990
1086
  `Critical error in ScheduledMaintenanceService sequential operations: ${error}`,
@@ -1577,6 +1673,37 @@ ${labels
1577
1673
  },
1578
1674
  );
1579
1675
  }
1676
+
1677
+ /*
1678
+ * Re-evaluate reminder schedule when reminders are enabled or disabled,
1679
+ * or when labels change (which may change the matching reminder rule).
1680
+ */
1681
+ if (
1682
+ onUpdate.updateBy.props.tenantId &&
1683
+ (Object.prototype.hasOwnProperty.call(
1684
+ onUpdate.updateBy.data,
1685
+ "enableReminders",
1686
+ ) ||
1687
+ Object.prototype.hasOwnProperty.call(
1688
+ onUpdate.updateBy.data,
1689
+ "labels",
1690
+ ))
1691
+ ) {
1692
+ try {
1693
+ await this.refreshReminderSchedule({
1694
+ scheduledMaintenanceId: scheduledMaintenanceId,
1695
+ projectId: onUpdate.updateBy.props.tenantId as ObjectID,
1696
+ });
1697
+ } catch (reminderError) {
1698
+ logger.error(
1699
+ `Reminder rescheduling failed in ScheduledMaintenanceService.onUpdateSuccess: ${reminderError}`,
1700
+ {
1701
+ projectId: onUpdate.updateBy.props.tenantId?.toString(),
1702
+ scheduledMaintenanceId: scheduledMaintenanceId?.toString(),
1703
+ } as LogAttributes,
1704
+ );
1705
+ }
1706
+ }
1580
1707
  }
1581
1708
  }
1582
1709