@oneuptime/common 7.0.4346 → 7.0.4358

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 (40) hide show
  1. package/Models/DatabaseModels/StatusPage.ts +37 -0
  2. package/Models/DatabaseModels/StatusPageSubscriber.ts +60 -0
  3. package/Server/API/StatusPageAPI.ts +104 -10
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1749065784320-MigrationName.ts +23 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1749133333893-MigrationName.ts +17 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  7. package/Server/Services/AlertService.ts +51 -0
  8. package/Server/Services/IncidentService.ts +53 -0
  9. package/Server/Services/ScheduledMaintenanceService.ts +75 -0
  10. package/Server/Services/StatusPageSubscriberService.ts +116 -1
  11. package/Server/Utils/OpenAPI.ts +224 -27
  12. package/Server/Utils/Workspace/Slack/Slack.ts +14 -0
  13. package/Utils/Schema/ModelSchema.ts +1303 -11
  14. package/build/dist/Models/DatabaseModels/StatusPage.js +39 -0
  15. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  16. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +62 -0
  17. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  18. package/build/dist/Server/API/StatusPageAPI.js +73 -10
  19. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  20. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1749065784320-MigrationName.js +14 -0
  21. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1749065784320-MigrationName.js.map +1 -0
  22. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1749133333893-MigrationName.js +12 -0
  23. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1749133333893-MigrationName.js.map +1 -0
  24. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  25. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  26. package/build/dist/Server/Services/AlertService.js +46 -0
  27. package/build/dist/Server/Services/AlertService.js.map +1 -1
  28. package/build/dist/Server/Services/IncidentService.js +46 -0
  29. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  30. package/build/dist/Server/Services/ScheduledMaintenanceService.js +65 -0
  31. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  32. package/build/dist/Server/Services/StatusPageSubscriberService.js +98 -1
  33. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  34. package/build/dist/Server/Utils/OpenAPI.js +178 -50
  35. package/build/dist/Server/Utils/OpenAPI.js.map +1 -1
  36. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +15 -0
  37. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  38. package/build/dist/Utils/Schema/ModelSchema.js +1101 -6
  39. package/build/dist/Utils/Schema/ModelSchema.js.map +1 -1
  40. package/package.json +1 -1
@@ -1136,6 +1136,43 @@ export default class StatusPage extends BaseModel {
1136
1136
  })
1137
1137
  public enableSmsSubscribers?: boolean = undefined;
1138
1138
 
1139
+ @ColumnAccessControl({
1140
+ create: [
1141
+ Permission.ProjectOwner,
1142
+ Permission.ProjectAdmin,
1143
+ Permission.ProjectMember,
1144
+ Permission.CreateProjectStatusPage,
1145
+ ],
1146
+ read: [
1147
+ Permission.ProjectOwner,
1148
+ Permission.ProjectAdmin,
1149
+ Permission.ProjectMember,
1150
+ Permission.ReadProjectStatusPage,
1151
+ ],
1152
+ update: [
1153
+ Permission.ProjectOwner,
1154
+ Permission.ProjectAdmin,
1155
+ Permission.ProjectMember,
1156
+ Permission.EditProjectStatusPage,
1157
+ ],
1158
+ })
1159
+ @TableColumn({
1160
+ isDefaultValueColumn: true,
1161
+ type: TableColumnType.Boolean,
1162
+ title: "Enable Slack Subscribers",
1163
+ description: "Can Slack subscribers subscribe to this Status Page?",
1164
+ })
1165
+ @Column({
1166
+ type: ColumnType.Boolean,
1167
+ default: false,
1168
+ })
1169
+ @ColumnBillingAccessControl({
1170
+ read: PlanType.Free,
1171
+ update: PlanType.Scale,
1172
+ create: PlanType.Free,
1173
+ })
1174
+ public enableSlackSubscribers?: boolean = undefined;
1175
+
1139
1176
  @ColumnAccessControl({
1140
1177
  create: [
1141
1178
  Permission.ProjectOwner,
@@ -319,6 +319,66 @@ export default class StatusPageSubscriber extends BaseModel {
319
319
  })
320
320
  public subscriberWebhook?: URL = undefined;
321
321
 
322
+ @ColumnAccessControl({
323
+ create: [
324
+ Permission.ProjectOwner,
325
+ Permission.ProjectAdmin,
326
+ Permission.ProjectMember,
327
+ Permission.CreateStatusPageSubscriber,
328
+ Permission.Public,
329
+ ],
330
+ read: [],
331
+ update: [],
332
+ })
333
+ @TableColumn({
334
+ required: false,
335
+ type: TableColumnType.ShortURL,
336
+ title: "Slack Incoming Webhook URL",
337
+ description:
338
+ "Slack incoming webhook URL to send notifications to Slack channel",
339
+ })
340
+ @Column({
341
+ nullable: true,
342
+ type: ColumnType.ShortURL,
343
+ transformer: URL.getDatabaseTransformer(),
344
+ })
345
+ public slackIncomingWebhookUrl?: URL = undefined;
346
+
347
+ @ColumnAccessControl({
348
+ create: [
349
+ Permission.ProjectOwner,
350
+ Permission.ProjectAdmin,
351
+ Permission.ProjectMember,
352
+ Permission.CreateStatusPageSubscriber,
353
+ Permission.Public,
354
+ ],
355
+ read: [
356
+ Permission.ProjectOwner,
357
+ Permission.ProjectAdmin,
358
+ Permission.ProjectMember,
359
+ Permission.ReadStatusPageSubscriber,
360
+ ],
361
+ update: [
362
+ Permission.ProjectOwner,
363
+ Permission.ProjectAdmin,
364
+ Permission.ProjectMember,
365
+ Permission.EditStatusPageSubscriber,
366
+ ],
367
+ })
368
+ @TableColumn({
369
+ required: false,
370
+ type: TableColumnType.ShortText,
371
+ title: "Slack Workspace Name",
372
+ description:
373
+ "Name of the Slack workspace for validation and identification",
374
+ })
375
+ @Column({
376
+ nullable: true,
377
+ type: ColumnType.ShortText,
378
+ length: ColumnLength.ShortText,
379
+ })
380
+ public slackWorkspaceName?: string = undefined;
381
+
322
382
  @ColumnAccessControl({
323
383
  create: [
324
384
  Permission.ProjectOwner,
@@ -89,6 +89,7 @@ import DatabaseConfig from "../DatabaseConfig";
89
89
  import { FileRoute } from "../../ServiceRoute";
90
90
  import ProjectSmtpConfigService from "../Services/ProjectSmtpConfigService";
91
91
  import ForbiddenException from "../../Types/Exception/ForbiddenException";
92
+ import SlackUtil from "../Utils/Workspace/Slack/Slack";
92
93
 
93
94
  export default class StatusPageAPI extends BaseAPI<
94
95
  StatusPage,
@@ -495,6 +496,7 @@ export default class StatusPageAPI extends BaseAPI<
495
496
  headerHTML: true,
496
497
  footerHTML: true,
497
498
  enableEmailSubscribers: true,
499
+ enableSlackSubscribers: true,
498
500
  enableSmsSubscribers: true,
499
501
  isPublicStatusPage: true,
500
502
  allowSubscribersToChooseResources: true,
@@ -2135,6 +2137,7 @@ export default class StatusPageAPI extends BaseAPI<
2135
2137
  _id: true,
2136
2138
  projectId: true,
2137
2139
  enableEmailSubscribers: true,
2140
+ enableSlackSubscribers: true,
2138
2141
  enableSmsSubscribers: true,
2139
2142
  allowSubscribersToChooseResources: true,
2140
2143
  allowSubscribersToChooseEventTypes: true,
@@ -2173,6 +2176,18 @@ export default class StatusPageAPI extends BaseAPI<
2173
2176
  );
2174
2177
  }
2175
2178
 
2179
+ if (
2180
+ req.body.data["slackIncomingWebhookUrl"] &&
2181
+ !statusPage.enableSlackSubscribers
2182
+ ) {
2183
+ logger.debug(
2184
+ `Slack subscribers not enabled for status page with ID: ${statusPageId}`,
2185
+ );
2186
+ throw new BadDataException(
2187
+ "Slack subscribers not enabled for this status page.",
2188
+ );
2189
+ }
2190
+
2176
2191
  if (req.body.data["subscriberPhone"] && !statusPage.enableSmsSubscribers) {
2177
2192
  logger.debug(
2178
2193
  `SMS subscribers not enabled for status page with ID: ${statusPageId}`,
@@ -2186,13 +2201,14 @@ export default class StatusPageAPI extends BaseAPI<
2186
2201
 
2187
2202
  if (
2188
2203
  !req.body.data["subscriberEmail"] &&
2189
- !req.body.data["subscriberPhone"]
2204
+ !req.body.data["subscriberPhone"] &&
2205
+ !req.body.data["slackWorkspaceName"]
2190
2206
  ) {
2191
2207
  logger.debug(
2192
- `No email or phone provided for subscription to status page with ID: ${statusPageId}`,
2208
+ `No email, slack workspace name or phone provided for subscription to status page with ID: ${statusPageId}`,
2193
2209
  );
2194
2210
  throw new BadDataException(
2195
- "Email or phone is required to subscribe to this status page.",
2211
+ "Email, phone or slack workspace name is required to subscribe to this status page.",
2196
2212
  );
2197
2213
  }
2198
2214
 
@@ -2204,6 +2220,12 @@ export default class StatusPageAPI extends BaseAPI<
2204
2220
  ? new Phone(req.body.data["subscriberPhone"] as string)
2205
2221
  : undefined;
2206
2222
 
2223
+ const slackWorkspaceName: string | undefined = req.body.data[
2224
+ "slackWorkspaceName"
2225
+ ]
2226
+ ? (req.body.data["slackWorkspaceName"] as string)
2227
+ : undefined;
2228
+
2207
2229
  let statusPageSubscriber: StatusPageSubscriber | null = null;
2208
2230
 
2209
2231
  if (email) {
@@ -2240,19 +2262,39 @@ export default class StatusPageAPI extends BaseAPI<
2240
2262
  });
2241
2263
  }
2242
2264
 
2265
+ if (slackWorkspaceName) {
2266
+ logger.debug(`Setting subscriber slack workspace: ${slackWorkspaceName}`);
2267
+ statusPageSubscriber = await StatusPageSubscriberService.findOneBy({
2268
+ query: {
2269
+ slackWorkspaceName: slackWorkspaceName,
2270
+ statusPageId: statusPageId,
2271
+ },
2272
+ select: {
2273
+ _id: true,
2274
+ slackWorkspaceName: true,
2275
+ slackIncomingWebhookUrl: true,
2276
+ },
2277
+ props: {
2278
+ isRoot: true,
2279
+ },
2280
+ });
2281
+ }
2282
+
2243
2283
  if (!statusPageSubscriber) {
2244
2284
  // not found, return bad data
2245
2285
  logger.debug(
2246
- `Subscriber not found for email: ${email} or phone: ${phone}`,
2286
+ `Subscriber not found for email: ${email}, phone: ${phone}, or slack workspace: ${slackWorkspaceName}`,
2247
2287
  );
2248
2288
 
2249
- let emailOrPhone: string = "email";
2289
+ let identifierType: string = "email";
2250
2290
  if (phone) {
2251
- emailOrPhone = "phone";
2291
+ identifierType = "phone";
2292
+ } else if (slackWorkspaceName) {
2293
+ identifierType = "slack workspace name";
2252
2294
  }
2253
2295
 
2254
2296
  throw new BadDataException(
2255
- `Subscription not found for this status page. Please make sure your ${emailOrPhone} is correct.`,
2297
+ `Subscription not found for this status page. Please make sure your ${identifierType} is correct.`,
2256
2298
  );
2257
2299
  }
2258
2300
 
@@ -2327,6 +2369,17 @@ export default class StatusPageAPI extends BaseAPI<
2327
2369
  });
2328
2370
  }
2329
2371
 
2372
+ if (statusPageSubscriber.slackIncomingWebhookUrl) {
2373
+ const slackMessage: string = `You have selected to manage your subscription for the status page: ${statusPage.name}. You can manage your subscription here: ${manageUrlink}`;
2374
+
2375
+ SlackUtil.sendMessageToChannelViaIncomingWebhook({
2376
+ url: statusPageSubscriber.slackIncomingWebhookUrl,
2377
+ text: slackMessage,
2378
+ }).catch((err: Error) => {
2379
+ logger.error(err);
2380
+ });
2381
+ }
2382
+
2330
2383
  logger.debug(
2331
2384
  `Subscription management link sent to subscriber with ID: ${statusPageSubscriber.id}`,
2332
2385
  );
@@ -2355,6 +2408,7 @@ export default class StatusPageAPI extends BaseAPI<
2355
2408
  projectId: true,
2356
2409
  enableEmailSubscribers: true,
2357
2410
  enableSmsSubscribers: true,
2411
+ enableSlackSubscribers: true,
2358
2412
  allowSubscribersToChooseResources: true,
2359
2413
  allowSubscribersToChooseEventTypes: true,
2360
2414
  showSubscriberPageOnStatusPage: true,
@@ -2403,15 +2457,28 @@ export default class StatusPageAPI extends BaseAPI<
2403
2457
 
2404
2458
  // if no email or phone, throw error.
2405
2459
 
2460
+ if (
2461
+ req.body.data["slackWorkspaceName"] &&
2462
+ !statusPage.enableSlackSubscribers
2463
+ ) {
2464
+ logger.debug(
2465
+ `Slack subscribers not enabled for status page with ID: ${objectId}`,
2466
+ );
2467
+ throw new BadDataException(
2468
+ "Slack subscribers not enabled for this status page.",
2469
+ );
2470
+ }
2471
+
2406
2472
  if (
2407
2473
  !req.body.data["subscriberEmail"] &&
2408
- !req.body.data["subscriberPhone"]
2474
+ !req.body.data["subscriberPhone"] &&
2475
+ !req.body.data["slackWorkspaceName"]
2409
2476
  ) {
2410
2477
  logger.debug(
2411
- `No email or phone provided for subscription to status page with ID: ${objectId}`,
2478
+ `No email, phone, or slack workspace name provided for subscription to status page with ID: ${objectId}`,
2412
2479
  );
2413
2480
  throw new BadDataException(
2414
- "Email or phone is required to subscribe to this status page.",
2481
+ "Email, phone or slack workspace name is required to subscribe to this status page.",
2415
2482
  );
2416
2483
  }
2417
2484
 
@@ -2423,6 +2490,18 @@ export default class StatusPageAPI extends BaseAPI<
2423
2490
  ? new Phone(req.body.data["subscriberPhone"] as string)
2424
2491
  : undefined;
2425
2492
 
2493
+ const slackIncomingWebhookUrl: string | undefined = req.body.data[
2494
+ "slackIncomingWebhookUrl"
2495
+ ]
2496
+ ? (req.body.data["slackIncomingWebhookUrl"] as string)
2497
+ : undefined;
2498
+
2499
+ const slackWorkspaceName: string | undefined = req.body.data[
2500
+ "slackWorkspaceName"
2501
+ ]
2502
+ ? (req.body.data["slackWorkspaceName"] as string)
2503
+ : undefined;
2504
+
2426
2505
  let statusPageSubscriber: StatusPageSubscriber | null = null;
2427
2506
 
2428
2507
  let isUpdate: boolean = false;
@@ -2467,6 +2546,20 @@ export default class StatusPageAPI extends BaseAPI<
2467
2546
  statusPageSubscriber.subscriberPhone = phone;
2468
2547
  }
2469
2548
 
2549
+ if (slackIncomingWebhookUrl) {
2550
+ logger.debug(`Setting subscriber slack: ${slackIncomingWebhookUrl}`);
2551
+ statusPageSubscriber.slackIncomingWebhookUrl = URL.fromString(
2552
+ slackIncomingWebhookUrl,
2553
+ );
2554
+ }
2555
+
2556
+ if (slackWorkspaceName) {
2557
+ logger.debug(
2558
+ `Setting subscriber slack workspace name: ${slackWorkspaceName}`,
2559
+ );
2560
+ statusPageSubscriber.slackWorkspaceName = slackWorkspaceName;
2561
+ }
2562
+
2470
2563
  if (
2471
2564
  req.body.data["statusPageResources"] &&
2472
2565
  !statusPage.allowSubscribersToChooseResources
@@ -2606,6 +2699,7 @@ export default class StatusPageAPI extends BaseAPI<
2606
2699
  isUnsubscribed: true,
2607
2700
  subscriberEmail: true,
2608
2701
  subscriberPhone: true,
2702
+ slackWorkspaceName: true,
2609
2703
  statusPageId: true,
2610
2704
  statusPageResources: true,
2611
2705
  isSubscribedToAllResources: true,
@@ -0,0 +1,23 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1749065784320 implements MigrationInterface {
4
+ public name = "MigrationName1749065784320";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "StatusPage" ADD "enableSlackSubscribers" boolean NOT NULL DEFAULT false`,
9
+ );
10
+ await queryRunner.query(
11
+ `ALTER TABLE "StatusPageSubscriber" ADD "slackIncomingWebhookUrl" character varying`,
12
+ );
13
+ }
14
+
15
+ public async down(queryRunner: QueryRunner): Promise<void> {
16
+ await queryRunner.query(
17
+ `ALTER TABLE "StatusPageSubscriber" DROP COLUMN "slackIncomingWebhookUrl"`,
18
+ );
19
+ await queryRunner.query(
20
+ `ALTER TABLE "StatusPage" DROP COLUMN "enableSlackSubscribers"`,
21
+ );
22
+ }
23
+ }
@@ -0,0 +1,17 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class MigrationName1749133333893 implements MigrationInterface {
4
+ public name = "MigrationName1749133333893";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "StatusPageSubscriber" ADD "slackWorkspaceName" character varying(100)`,
9
+ );
10
+ }
11
+
12
+ public async down(queryRunner: QueryRunner): Promise<void> {
13
+ await queryRunner.query(
14
+ `ALTER TABLE "StatusPageSubscriber" DROP COLUMN "slackWorkspaceName"`,
15
+ );
16
+ }
17
+ }
@@ -134,6 +134,8 @@ import { MigrationName1744809770336 } from "./1744809770336-MigrationName";
134
134
  import { MigrationName1747305098533 } from "./1747305098533-MigrationName";
135
135
  import { MigrationName1747674762672 } from "./1747674762672-MigrationName";
136
136
  import { MigrationName1748456937826 } from "./1748456937826-MigrationName";
137
+ import { MigrationName1749065784320 } from "./1749065784320-MigrationName";
138
+ import { MigrationName1749133333893 } from "./1749133333893-MigrationName";
137
139
 
138
140
  export default [
139
141
  InitialMigration,
@@ -272,4 +274,6 @@ export default [
272
274
  MigrationName1747305098533,
273
275
  MigrationName1747674762672,
274
276
  MigrationName1748456937826,
277
+ MigrationName1749065784320,
278
+ MigrationName1749133333893,
275
279
  ];
@@ -1338,5 +1338,56 @@ ${alertSeverity.name}
1338
1338
 
1339
1339
  return alert;
1340
1340
  }
1341
+
1342
+ /**
1343
+ * Ensures the currentAlertStateId of the alert matches the latest timeline entry.
1344
+ */
1345
+ public async refreshAlertCurrentStatus(alertId: ObjectID): Promise<void> {
1346
+ const alert: Model | null = await this.findOneById({
1347
+ id: alertId,
1348
+ select: {
1349
+ _id: true,
1350
+ projectId: true,
1351
+ currentAlertStateId: true,
1352
+ },
1353
+ props: { isRoot: true },
1354
+ });
1355
+ if (!alert || !alert.projectId) {
1356
+ return;
1357
+ }
1358
+ const latestTimeline: AlertStateTimeline | null =
1359
+ await AlertStateTimelineService.findOneBy({
1360
+ query: {
1361
+ alertId: alert.id!,
1362
+ projectId: alert.projectId,
1363
+ },
1364
+ sort: {
1365
+ startsAt: SortOrder.Descending,
1366
+ },
1367
+ select: {
1368
+ alertStateId: true,
1369
+ },
1370
+ props: {
1371
+ isRoot: true,
1372
+ },
1373
+ });
1374
+ if (
1375
+ latestTimeline &&
1376
+ latestTimeline.alertStateId &&
1377
+ alert.currentAlertStateId?.toString() !==
1378
+ latestTimeline.alertStateId.toString()
1379
+ ) {
1380
+ await this.updateOneBy({
1381
+ query: { _id: alert.id!.toString() },
1382
+ data: {
1383
+ currentAlertStateId: latestTimeline.alertStateId,
1384
+ },
1385
+ props: { isRoot: true },
1386
+ });
1387
+ logger.info(
1388
+ `Updated Alert ${alert.id} current state to ${latestTimeline.alertStateId}`,
1389
+ );
1390
+ }
1391
+ }
1341
1392
  }
1342
1393
  export default new Service();
@@ -1932,6 +1932,59 @@ ${incidentSeverity.name}
1932
1932
 
1933
1933
  return incident.incidentNumber ? Number(incident.incidentNumber) : null;
1934
1934
  }
1935
+
1936
+ /**
1937
+ * Ensures the currentIncidentStateId of the incident matches the latest timeline entry.
1938
+ */
1939
+ public async refreshIncidentCurrentStatus(
1940
+ incidentId: ObjectID,
1941
+ ): Promise<void> {
1942
+ const incident: Model | null = await this.findOneById({
1943
+ id: incidentId,
1944
+ select: {
1945
+ _id: true,
1946
+ projectId: true,
1947
+ currentIncidentStateId: true,
1948
+ },
1949
+ props: { isRoot: true },
1950
+ });
1951
+ if (!incident || !incident.projectId) {
1952
+ return;
1953
+ }
1954
+ const latestTimeline: IncidentStateTimeline | null =
1955
+ await IncidentStateTimelineService.findOneBy({
1956
+ query: {
1957
+ incidentId: incident.id!,
1958
+ projectId: incident.projectId,
1959
+ },
1960
+ sort: {
1961
+ startsAt: SortOrder.Descending,
1962
+ },
1963
+ select: {
1964
+ incidentStateId: true,
1965
+ },
1966
+ props: {
1967
+ isRoot: true,
1968
+ },
1969
+ });
1970
+ if (
1971
+ latestTimeline &&
1972
+ latestTimeline.incidentStateId &&
1973
+ incident.currentIncidentStateId?.toString() !==
1974
+ latestTimeline.incidentStateId.toString()
1975
+ ) {
1976
+ await this.updateOneBy({
1977
+ query: { _id: incident.id!.toString() },
1978
+ data: {
1979
+ currentIncidentStateId: latestTimeline.incidentStateId,
1980
+ },
1981
+ props: { isRoot: true },
1982
+ });
1983
+ logger.info(
1984
+ `Updated Incident ${incident.id} current state to ${latestTimeline.incidentStateId}`,
1985
+ );
1986
+ }
1987
+ }
1935
1988
  }
1936
1989
 
1937
1990
  export default new Service();
@@ -49,6 +49,7 @@ import { IsBillingEnabled } from "../EnvironmentConfig";
49
49
  import StatusPageEventType from "../../Types/StatusPage/StatusPageEventType";
50
50
  import ScheduledMaintenanceFeedService from "./ScheduledMaintenanceFeedService";
51
51
  import { ScheduledMaintenanceFeedEventType } from "../../Models/DatabaseModels/ScheduledMaintenanceFeed";
52
+ import SlackUtil from "../Utils/Workspace/Slack/Slack";
52
53
  import { Gray500, Red500 } from "../../Types/BrandColors";
53
54
  import Label from "../../Models/DatabaseModels/Label";
54
55
  import LabelService from "./LabelService";
@@ -254,6 +255,26 @@ export class Service extends DatabaseService<Model> {
254
255
  });
255
256
  }
256
257
 
258
+ if (subscriber.slackIncomingWebhookUrl) {
259
+ const slackMessage: string = `## 🔧 Scheduled Maintenance - ${event.title || ""}
260
+
261
+ **Scheduled Date:** ${OneUptimeDate.getDateAsFormattedString(event.startsAt!)}
262
+
263
+ ${resourcesAffected ? `**Resources Affected:** ${resourcesAffected}` : ""}
264
+
265
+ **Description:** ${event.description || ""}
266
+
267
+ [View Status Page](${statusPageURL}) | [Unsubscribe](${unsubscribeUrl})`;
268
+
269
+ // send Slack notification here.
270
+ SlackUtil.sendMessageToChannelViaIncomingWebhook({
271
+ url: subscriber.slackIncomingWebhookUrl,
272
+ text: SlackUtil.convertMarkdownToSlackRichText(slackMessage),
273
+ }).catch((err: Error) => {
274
+ logger.error(err);
275
+ });
276
+ }
277
+
257
278
  if (subscriber.subscriberEmail) {
258
279
  // send email here.
259
280
 
@@ -1467,5 +1488,59 @@ ${labels
1467
1488
  },
1468
1489
  );
1469
1490
  }
1491
+
1492
+ /**
1493
+ * Ensures the currentScheduledMaintenanceStateId of the scheduled maintenance matches the latest timeline entry.
1494
+ */
1495
+ public async refreshScheduledMaintenanceCurrentStatus(
1496
+ scheduledMaintenanceId: ObjectID,
1497
+ ): Promise<void> {
1498
+ const scheduledMaintenance: Model | null = await this.findOneById({
1499
+ id: scheduledMaintenanceId,
1500
+ select: {
1501
+ _id: true,
1502
+ projectId: true,
1503
+ currentScheduledMaintenanceStateId: true,
1504
+ },
1505
+ props: { isRoot: true },
1506
+ });
1507
+ if (!scheduledMaintenance || !scheduledMaintenance.projectId) {
1508
+ return;
1509
+ }
1510
+ const latestTimeline: ScheduledMaintenanceStateTimeline | null =
1511
+ await ScheduledMaintenanceStateTimelineService.findOneBy({
1512
+ query: {
1513
+ scheduledMaintenanceId: scheduledMaintenance.id!,
1514
+ projectId: scheduledMaintenance.projectId,
1515
+ },
1516
+ sort: {
1517
+ startsAt: SortOrder.Descending,
1518
+ },
1519
+ select: {
1520
+ scheduledMaintenanceStateId: true,
1521
+ },
1522
+ props: {
1523
+ isRoot: true,
1524
+ },
1525
+ });
1526
+ if (
1527
+ latestTimeline &&
1528
+ latestTimeline.scheduledMaintenanceStateId &&
1529
+ scheduledMaintenance.currentScheduledMaintenanceStateId?.toString() !==
1530
+ latestTimeline.scheduledMaintenanceStateId.toString()
1531
+ ) {
1532
+ await this.updateOneBy({
1533
+ query: { _id: scheduledMaintenance.id!.toString() },
1534
+ data: {
1535
+ currentScheduledMaintenanceStateId:
1536
+ latestTimeline.scheduledMaintenanceStateId,
1537
+ },
1538
+ props: { isRoot: true },
1539
+ });
1540
+ logger.info(
1541
+ `Updated ScheduledMaintenance ${scheduledMaintenance.id} current state to ${latestTimeline.scheduledMaintenanceStateId}`,
1542
+ );
1543
+ }
1544
+ }
1470
1545
  }
1471
1546
  export default new Service();