@oneuptime/common 9.5.2 → 9.5.3

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 (100) hide show
  1. package/Models/DatabaseModels/Alert.ts +1 -0
  2. package/Models/DatabaseModels/AlertEpisode.ts +1 -0
  3. package/Models/DatabaseModels/AlertEpisodeStateTimeline.ts +1 -0
  4. package/Models/DatabaseModels/AlertStateTimeline.ts +1 -0
  5. package/Models/DatabaseModels/Incident.ts +1 -0
  6. package/Models/DatabaseModels/IncidentEpisode.ts +156 -0
  7. package/Models/DatabaseModels/IncidentEpisodeFeed.ts +2 -0
  8. package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +611 -0
  9. package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +84 -0
  10. package/Models/DatabaseModels/IncidentGroupingRule.ts +36 -0
  11. package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
  12. package/Models/DatabaseModels/Index.ts +2 -0
  13. package/Models/DatabaseModels/MonitorStatusTimeline.ts +1 -0
  14. package/Models/DatabaseModels/Project.ts +2 -1
  15. package/Models/DatabaseModels/ProjectCallSMSConfig.ts +1 -0
  16. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -0
  17. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -0
  18. package/Models/DatabaseModels/StatusPage.ts +120 -0
  19. package/Server/API/IncidentEpisodePublicNoteAPI.ts +98 -0
  20. package/Server/API/StatusPageAPI.ts +1092 -45
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1770232207959-MigrationName.ts +181 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1770237245069-MigrationName.ts +35 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  24. package/Server/Services/IncidentEpisodePublicNoteService.ts +254 -0
  25. package/Server/Services/IncidentEpisodeService.ts +26 -0
  26. package/Server/Services/Index.ts +2 -0
  27. package/Server/Utils/Monitor/MonitorIncident.ts +6 -0
  28. package/Types/Email/EmailTemplateType.ts +4 -0
  29. package/Types/Icon/IconProp.ts +172 -0
  30. package/Types/Monitor/CriteriaIncident.ts +2 -0
  31. package/Types/Permission.ts +40 -0
  32. package/Types/StatusPage/StatusPageSubscriberNotificationEventType.ts +5 -0
  33. package/UI/Components/Icon/Icon.tsx +1333 -1
  34. package/build/dist/Models/DatabaseModels/Alert.js +1 -0
  35. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  36. package/build/dist/Models/DatabaseModels/AlertEpisode.js +1 -0
  37. package/build/dist/Models/DatabaseModels/AlertEpisode.js.map +1 -1
  38. package/build/dist/Models/DatabaseModels/AlertEpisodeStateTimeline.js +1 -0
  39. package/build/dist/Models/DatabaseModels/AlertEpisodeStateTimeline.js.map +1 -1
  40. package/build/dist/Models/DatabaseModels/AlertStateTimeline.js +1 -0
  41. package/build/dist/Models/DatabaseModels/AlertStateTimeline.js.map +1 -1
  42. package/build/dist/Models/DatabaseModels/Incident.js +1 -0
  43. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  44. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +161 -0
  45. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  46. package/build/dist/Models/DatabaseModels/IncidentEpisodeFeed.js +2 -0
  47. package/build/dist/Models/DatabaseModels/IncidentEpisodeFeed.js.map +1 -1
  48. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +626 -0
  49. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -0
  50. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +86 -0
  51. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
  52. package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js +37 -0
  53. package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js.map +1 -1
  54. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
  55. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  56. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  57. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  58. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
  59. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  60. package/build/dist/Models/DatabaseModels/Project.js +2 -1
  61. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  62. package/build/dist/Models/DatabaseModels/ProjectCallSMSConfig.js +1 -0
  63. package/build/dist/Models/DatabaseModels/ProjectCallSMSConfig.js.map +1 -1
  64. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
  65. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  66. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
  67. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  68. package/build/dist/Models/DatabaseModels/StatusPage.js +126 -0
  69. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  70. package/build/dist/Server/API/IncidentEpisodePublicNoteAPI.js +68 -0
  71. package/build/dist/Server/API/IncidentEpisodePublicNoteAPI.js.map +1 -0
  72. package/build/dist/Server/API/StatusPageAPI.js +874 -47
  73. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  74. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770232207959-MigrationName.js +68 -0
  75. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770232207959-MigrationName.js.map +1 -0
  76. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770237245069-MigrationName.js +18 -0
  77. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1770237245069-MigrationName.js.map +1 -0
  78. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  79. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  80. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js +223 -0
  81. package/build/dist/Server/Services/IncidentEpisodePublicNoteService.js.map +1 -0
  82. package/build/dist/Server/Services/IncidentEpisodeService.js +22 -0
  83. package/build/dist/Server/Services/IncidentEpisodeService.js.map +1 -1
  84. package/build/dist/Server/Services/Index.js +2 -0
  85. package/build/dist/Server/Services/Index.js.map +1 -1
  86. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +5 -0
  87. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  88. package/build/dist/Types/Email/EmailTemplateType.js +3 -0
  89. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  90. package/build/dist/Types/Icon/IconProp.js +172 -0
  91. package/build/dist/Types/Icon/IconProp.js.map +1 -1
  92. package/build/dist/Types/Monitor/CriteriaIncident.js +1 -0
  93. package/build/dist/Types/Monitor/CriteriaIncident.js.map +1 -1
  94. package/build/dist/Types/Permission.js +34 -0
  95. package/build/dist/Types/Permission.js.map +1 -1
  96. package/build/dist/Types/StatusPage/StatusPageSubscriberNotificationEventType.js +4 -0
  97. package/build/dist/Types/StatusPage/StatusPageSubscriberNotificationEventType.js.map +1 -1
  98. package/build/dist/UI/Components/Icon/Icon.js +502 -1
  99. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  100. package/package.json +1 -1
@@ -9,6 +9,10 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  };
10
10
  import UserMiddleware from "../Middleware/UserAuthorization";
11
11
  import AcmeChallengeService from "../Services/AcmeChallengeService";
12
+ import IncidentEpisodeService from "../Services/IncidentEpisodeService";
13
+ import IncidentEpisodeMemberService from "../Services/IncidentEpisodeMemberService";
14
+ import IncidentEpisodePublicNoteService from "../Services/IncidentEpisodePublicNoteService";
15
+ import IncidentEpisodeStateTimelineService from "../Services/IncidentEpisodeStateTimelineService";
12
16
  import IncidentPublicNoteService from "../Services/IncidentPublicNoteService";
13
17
  import IncidentService from "../Services/IncidentService";
14
18
  import IncidentStateService from "../Services/IncidentStateService";
@@ -49,6 +53,9 @@ import Phone from "../../Types/Phone";
49
53
  import PositiveNumber from "../../Types/PositiveNumber";
50
54
  import HashedString from "../../Types/HashedString";
51
55
  import Incident from "../../Models/DatabaseModels/Incident";
56
+ import IncidentEpisode from "../../Models/DatabaseModels/IncidentEpisode";
57
+ import IncidentEpisodePublicNote from "../../Models/DatabaseModels/IncidentEpisodePublicNote";
58
+ import IncidentEpisodeStateTimeline from "../../Models/DatabaseModels/IncidentEpisodeStateTimeline";
52
59
  import IncidentPublicNote from "../../Models/DatabaseModels/IncidentPublicNote";
53
60
  import IncidentState from "../../Models/DatabaseModels/IncidentState";
54
61
  import IncidentStateTimeline from "../../Models/DatabaseModels/IncidentStateTimeline";
@@ -120,7 +127,7 @@ const resolveStatusPageIdOrThrow = async (statusPageIdOrDomain) => {
120
127
  };
121
128
  export default class StatusPageAPI extends BaseAPI {
122
129
  constructor() {
123
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5;
130
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8;
124
131
  super(StatusPage, StatusPageService);
125
132
  // get title, description of the page. This is used for SEO.
126
133
  this.router.get(`${(_a = new this.entityType().getCrudApiPath()) === null || _a === void 0 ? void 0 : _a.toString()}/seo/:statusPageIdOrDomain`, UserMiddleware.getUserMiddleware, async (req, res) => {
@@ -290,25 +297,34 @@ export default class StatusPageAPI extends BaseAPI {
290
297
  }
291
298
  });
292
299
  this.router.get(`${(_f = new this.entityType()
293
- .getCrudApiPath()) === null || _f === void 0 ? void 0 : _f.toString()}/incident/postmortem/attachment/:statusPageId/:incidentId/:fileId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
300
+ .getCrudApiPath()) === null || _f === void 0 ? void 0 : _f.toString()}/incident-episode-public-note/attachment/:statusPageId/:episodeId/:noteId/:fileId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
294
301
  try {
295
- await this.getIncidentPostmortemAttachment(req, res);
302
+ await this.getIncidentEpisodePublicNoteAttachment(req, res);
296
303
  }
297
304
  catch (err) {
298
305
  next(err);
299
306
  }
300
307
  });
301
308
  this.router.get(`${(_g = new this.entityType()
302
- .getCrudApiPath()) === null || _g === void 0 ? void 0 : _g.toString()}/scheduled-maintenance-public-note/attachment/:statusPageId/:scheduledMaintenanceId/:noteId/:fileId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
309
+ .getCrudApiPath()) === null || _g === void 0 ? void 0 : _g.toString()}/incident/postmortem/attachment/:statusPageId/:incidentId/:fileId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
303
310
  try {
304
- await this.getScheduledMaintenancePublicNoteAttachment(req, res);
311
+ await this.getIncidentPostmortemAttachment(req, res);
305
312
  }
306
313
  catch (err) {
307
314
  next(err);
308
315
  }
309
316
  });
310
317
  this.router.get(`${(_h = new this.entityType()
311
- .getCrudApiPath()) === null || _h === void 0 ? void 0 : _h.toString()}/status-page-announcement/attachment/:statusPageId/:announcementId/:fileId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
318
+ .getCrudApiPath()) === null || _h === void 0 ? void 0 : _h.toString()}/scheduled-maintenance-public-note/attachment/:statusPageId/:scheduledMaintenanceId/:noteId/:fileId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
319
+ try {
320
+ await this.getScheduledMaintenancePublicNoteAttachment(req, res);
321
+ }
322
+ catch (err) {
323
+ next(err);
324
+ }
325
+ });
326
+ this.router.get(`${(_j = new this.entityType()
327
+ .getCrudApiPath()) === null || _j === void 0 ? void 0 : _j.toString()}/status-page-announcement/attachment/:statusPageId/:announcementId/:fileId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
312
328
  try {
313
329
  await this.getStatusPageAnnouncementAttachment(req, res);
314
330
  }
@@ -317,8 +333,8 @@ export default class StatusPageAPI extends BaseAPI {
317
333
  }
318
334
  });
319
335
  // embedded overall status badge api
320
- this.router.get(`${(_j = new this.entityType()
321
- .getCrudApiPath()) === null || _j === void 0 ? void 0 : _j.toString()}/badge/:statusPageId`, async (req, res) => {
336
+ this.router.get(`${(_k = new this.entityType()
337
+ .getCrudApiPath()) === null || _k === void 0 ? void 0 : _k.toString()}/badge/:statusPageId`, async (req, res) => {
322
338
  var _a;
323
339
  try {
324
340
  const statusPageId = new ObjectID(req.params["statusPageId"]);
@@ -431,8 +447,8 @@ export default class StatusPageAPI extends BaseAPI {
431
447
  }
432
448
  });
433
449
  // confirm subscription api
434
- this.router.get(`${(_k = new this.entityType()
435
- .getCrudApiPath()) === null || _k === void 0 ? void 0 : _k.toString()}/confirm-subscription/:statusPageSubscriberId`, async (req, res) => {
450
+ this.router.get(`${(_l = new this.entityType()
451
+ .getCrudApiPath()) === null || _l === void 0 ? void 0 : _l.toString()}/confirm-subscription/:statusPageSubscriberId`, async (req, res) => {
436
452
  const token = req.query["verification-token"];
437
453
  const statusPageSubscriberId = new ObjectID(req.params["statusPageSubscriberId"]);
438
454
  const subscriber = await StatusPageSubscriberService.findOneBy({
@@ -469,8 +485,8 @@ export default class StatusPageAPI extends BaseAPI {
469
485
  return Response.sendEmptySuccessResponse(req, res);
470
486
  });
471
487
  // CNAME verification api
472
- this.router.get(`${(_l = new this.entityType()
473
- .getCrudApiPath()) === null || _l === void 0 ? void 0 : _l.toString()}/cname-verification/:token`, async (req, res) => {
488
+ this.router.get(`${(_m = new this.entityType()
489
+ .getCrudApiPath()) === null || _m === void 0 ? void 0 : _m.toString()}/cname-verification/:token`, async (req, res) => {
474
490
  const host = req.get("host");
475
491
  if (!host) {
476
492
  throw new BadDataException("Host not found");
@@ -495,8 +511,8 @@ export default class StatusPageAPI extends BaseAPI {
495
511
  return Response.sendEmptySuccessResponse(req, res);
496
512
  });
497
513
  // ACME Challenge Validation.
498
- this.router.get(`${(_m = new this.entityType()
499
- .getCrudApiPath()) === null || _m === void 0 ? void 0 : _m.toString()}/.well-known/acme-challenge/:token`, async (req, res) => {
514
+ this.router.get(`${(_o = new this.entityType()
515
+ .getCrudApiPath()) === null || _o === void 0 ? void 0 : _o.toString()}/.well-known/acme-challenge/:token`, async (req, res) => {
500
516
  const challenge = await AcmeChallengeService.findOneBy({
501
517
  query: {
502
518
  token: req.params["token"],
@@ -513,7 +529,7 @@ export default class StatusPageAPI extends BaseAPI {
513
529
  }
514
530
  return Response.sendTextResponse(req, res, challenge.challenge);
515
531
  });
516
- this.router.post(`${(_o = new this.entityType().getCrudApiPath()) === null || _o === void 0 ? void 0 : _o.toString()}/test-email-report`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
532
+ this.router.post(`${(_p = new this.entityType().getCrudApiPath()) === null || _p === void 0 ? void 0 : _p.toString()}/test-email-report`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
517
533
  try {
518
534
  const email = new Email(req.body["email"]);
519
535
  const statusPageId = new ObjectID(req.body["statusPageId"].toString());
@@ -527,7 +543,7 @@ export default class StatusPageAPI extends BaseAPI {
527
543
  next(err);
528
544
  }
529
545
  });
530
- this.router.post(`${(_p = new this.entityType().getCrudApiPath()) === null || _p === void 0 ? void 0 : _p.toString()}/domain`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
546
+ this.router.post(`${(_q = new this.entityType().getCrudApiPath()) === null || _q === void 0 ? void 0 : _q.toString()}/domain`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
531
547
  try {
532
548
  if (!req.body["domain"]) {
533
549
  throw new BadDataException("domain is required in request body");
@@ -559,8 +575,8 @@ export default class StatusPageAPI extends BaseAPI {
559
575
  next(err);
560
576
  }
561
577
  });
562
- this.router.post(`${(_q = new this.entityType()
563
- .getCrudApiPath()) === null || _q === void 0 ? void 0 : _q.toString()}/master-page/:statusPageId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
578
+ this.router.post(`${(_r = new this.entityType()
579
+ .getCrudApiPath()) === null || _r === void 0 ? void 0 : _r.toString()}/master-page/:statusPageId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
564
580
  try {
565
581
  const objectId = new ObjectID(req.params["statusPageId"]);
566
582
  const select = {
@@ -677,8 +693,8 @@ export default class StatusPageAPI extends BaseAPI {
677
693
  next(err);
678
694
  }
679
695
  });
680
- this.router.post(`${(_r = new this.entityType()
681
- .getCrudApiPath()) === null || _r === void 0 ? void 0 : _r.toString()}/master-password/:statusPageId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
696
+ this.router.post(`${(_s = new this.entityType()
697
+ .getCrudApiPath()) === null || _s === void 0 ? void 0 : _s.toString()}/master-password/:statusPageId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
682
698
  try {
683
699
  if (!req.params["statusPageId"]) {
684
700
  throw new BadDataException("Status Page ID not found");
@@ -724,7 +740,7 @@ export default class StatusPageAPI extends BaseAPI {
724
740
  next(err);
725
741
  }
726
742
  });
727
- this.router.post(`${(_s = new this.entityType().getCrudApiPath()) === null || _s === void 0 ? void 0 : _s.toString()}/sso/:statusPageId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
743
+ this.router.post(`${(_t = new this.entityType().getCrudApiPath()) === null || _t === void 0 ? void 0 : _t.toString()}/sso/:statusPageId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
728
744
  try {
729
745
  const objectId = new ObjectID(req.params["statusPageId"]);
730
746
  const sso = await StatusPageSsoService.findBy({
@@ -751,8 +767,8 @@ export default class StatusPageAPI extends BaseAPI {
751
767
  }
752
768
  });
753
769
  // Get all status page resources for subscriber to subscribe to.
754
- this.router.post(`${(_t = new this.entityType()
755
- .getCrudApiPath()) === null || _t === void 0 ? void 0 : _t.toString()}/resources/:statusPageId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
770
+ this.router.post(`${(_u = new this.entityType()
771
+ .getCrudApiPath()) === null || _u === void 0 ? void 0 : _u.toString()}/resources/:statusPageId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
756
772
  try {
757
773
  const statusPageId = new ObjectID(req.params["statusPageId"]);
758
774
  await this.checkHasReadAccess({
@@ -785,8 +801,8 @@ export default class StatusPageAPI extends BaseAPI {
785
801
  next(err);
786
802
  }
787
803
  });
788
- this.router.post(`${(_u = new this.entityType()
789
- .getCrudApiPath()) === null || _u === void 0 ? void 0 : _u.toString()}/uptime/:statusPageId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
804
+ this.router.post(`${(_v = new this.entityType()
805
+ .getCrudApiPath()) === null || _v === void 0 ? void 0 : _v.toString()}/uptime/:statusPageId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
790
806
  try {
791
807
  // This reosurce ID can be of a status page resource OR a status page group.
792
808
  const statusPageResourceId = new ObjectID(req.params["statusPageResourceId"]);
@@ -966,8 +982,9 @@ export default class StatusPageAPI extends BaseAPI {
966
982
  next(err);
967
983
  }
968
984
  });
969
- this.router.post(`${(_v = new this.entityType()
970
- .getCrudApiPath()) === null || _v === void 0 ? void 0 : _v.toString()}/overview/:statusPageIdOrDomain`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
985
+ this.router.post(`${(_w = new this.entityType()
986
+ .getCrudApiPath()) === null || _w === void 0 ? void 0 : _w.toString()}/overview/:statusPageIdOrDomain`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
987
+ var _a;
971
988
  try {
972
989
  const statusPageId = await resolveStatusPageIdOrThrow(req.params["statusPageIdOrDomain"]);
973
990
  await this.checkHasReadAccess({
@@ -1105,6 +1122,260 @@ export default class StatusPageAPI extends BaseAPI {
1105
1122
  },
1106
1123
  });
1107
1124
  }
1125
+ // Fetch active episodes (similar to incidents)
1126
+ let activeEpisodes = [];
1127
+ let activeEpisodesJson = [];
1128
+ let episodePublicNotes = [];
1129
+ let episodeStateTimelines = [];
1130
+ if (statusPage.showEpisodesOnStatusPage &&
1131
+ monitorsOnStatusPage.length > 0) {
1132
+ // First, get incidents that have monitors on status page
1133
+ const incidentsForEpisodes = await IncidentService.findBy({
1134
+ query: {
1135
+ monitors: monitorsOnStatusPage,
1136
+ projectId: statusPage.projectId,
1137
+ },
1138
+ select: {
1139
+ _id: true,
1140
+ },
1141
+ skip: 0,
1142
+ limit: LIMIT_PER_PROJECT,
1143
+ props: {
1144
+ isRoot: true,
1145
+ },
1146
+ });
1147
+ const incidentIdsForEpisodes = incidentsForEpisodes.map((incident) => {
1148
+ return incident.id;
1149
+ });
1150
+ // Get episode members for these incidents
1151
+ let episodeMembers = [];
1152
+ if (incidentIdsForEpisodes.length > 0) {
1153
+ episodeMembers = await IncidentEpisodeMemberService.findBy({
1154
+ query: {
1155
+ incidentId: QueryHelper.any(incidentIdsForEpisodes),
1156
+ projectId: statusPage.projectId,
1157
+ },
1158
+ select: {
1159
+ incidentEpisodeId: true,
1160
+ incidentId: true,
1161
+ },
1162
+ skip: 0,
1163
+ limit: LIMIT_PER_PROJECT,
1164
+ props: {
1165
+ isRoot: true,
1166
+ },
1167
+ });
1168
+ }
1169
+ // Get unique episode IDs
1170
+ const episodeIdsFromMembers = new Set();
1171
+ for (const member of episodeMembers) {
1172
+ if (member.incidentEpisodeId) {
1173
+ episodeIdsFromMembers.add(member.incidentEpisodeId.toString());
1174
+ }
1175
+ }
1176
+ // Fetch active (unresolved) episodes
1177
+ if (episodeIdsFromMembers.size > 0) {
1178
+ const unresolvedIncidentStates = await IncidentStateService.getUnresolvedIncidentStates(statusPage.projectId, { isRoot: true });
1179
+ const unresolvedIncidentStateIds = unresolvedIncidentStates.map((state) => {
1180
+ return state.id;
1181
+ });
1182
+ let selectEpisodes = {
1183
+ createdAt: true,
1184
+ declaredAt: true,
1185
+ updatedAt: true,
1186
+ title: true,
1187
+ description: true,
1188
+ _id: true,
1189
+ episodeNumber: true,
1190
+ incidentSeverity: {
1191
+ name: true,
1192
+ color: true,
1193
+ },
1194
+ currentIncidentState: {
1195
+ name: true,
1196
+ color: true,
1197
+ _id: true,
1198
+ order: true,
1199
+ isCreatedState: true,
1200
+ isAcknowledgedState: true,
1201
+ isResolvedState: true,
1202
+ },
1203
+ incidentCount: true,
1204
+ };
1205
+ if (statusPage.showEpisodeLabelsOnStatusPage) {
1206
+ selectEpisodes = Object.assign(Object.assign({}, selectEpisodes), { labels: {
1207
+ name: true,
1208
+ color: true,
1209
+ } });
1210
+ }
1211
+ activeEpisodes = await IncidentEpisodeService.findBy({
1212
+ query: {
1213
+ _id: QueryHelper.any(Array.from(episodeIdsFromMembers).map((id) => {
1214
+ return new ObjectID(id);
1215
+ })),
1216
+ currentIncidentStateId: QueryHelper.any(unresolvedIncidentStateIds),
1217
+ isVisibleOnStatusPage: true,
1218
+ projectId: statusPage.projectId,
1219
+ },
1220
+ select: selectEpisodes,
1221
+ sort: {
1222
+ declaredAt: SortOrder.Descending,
1223
+ createdAt: SortOrder.Descending,
1224
+ },
1225
+ skip: 0,
1226
+ limit: LIMIT_PER_PROJECT,
1227
+ props: {
1228
+ isRoot: true,
1229
+ },
1230
+ });
1231
+ // Build episode monitors map
1232
+ if (activeEpisodes.length > 0) {
1233
+ // Collect all incident IDs from episode members for active episodes
1234
+ const activeEpisodeIds = new Set(activeEpisodes.map((e) => {
1235
+ return e.id.toString();
1236
+ }));
1237
+ const memberIncidentIds = [];
1238
+ for (const member of episodeMembers) {
1239
+ if (member.incidentEpisodeId &&
1240
+ activeEpisodeIds.has(member.incidentEpisodeId.toString()) &&
1241
+ member.incidentId &&
1242
+ !memberIncidentIds.some((id) => {
1243
+ return id.toString() === member.incidentId.toString();
1244
+ })) {
1245
+ memberIncidentIds.push(member.incidentId);
1246
+ }
1247
+ }
1248
+ // Fetch incidents with monitors
1249
+ let memberIncidents = [];
1250
+ if (memberIncidentIds.length > 0) {
1251
+ memberIncidents = await IncidentService.findBy({
1252
+ query: {
1253
+ _id: QueryHelper.any(memberIncidentIds),
1254
+ projectId: statusPage.projectId,
1255
+ },
1256
+ select: {
1257
+ _id: true,
1258
+ monitors: {
1259
+ _id: true,
1260
+ },
1261
+ },
1262
+ skip: 0,
1263
+ limit: LIMIT_PER_PROJECT,
1264
+ props: {
1265
+ isRoot: true,
1266
+ },
1267
+ });
1268
+ }
1269
+ // Build incident -> monitors map
1270
+ const incidentMonitorsMap = new Map();
1271
+ for (const incident of memberIncidents) {
1272
+ const incidentIdStr = incident.id.toString();
1273
+ const monitorIds = (incident.monitors || [])
1274
+ .map((m) => {
1275
+ var _a, _b;
1276
+ return new ObjectID(((_a = m._id) === null || _a === void 0 ? void 0 : _a.toString()) || ((_b = m.id) === null || _b === void 0 ? void 0 : _b.toString()) || "");
1277
+ })
1278
+ .filter((id) => {
1279
+ return id.toString() !== "";
1280
+ });
1281
+ incidentMonitorsMap.set(incidentIdStr, monitorIds);
1282
+ }
1283
+ // Build episode -> monitors map
1284
+ const episodeMonitorsMap = new Map();
1285
+ for (const member of episodeMembers) {
1286
+ if (member.incidentEpisodeId &&
1287
+ member.incidentId &&
1288
+ activeEpisodeIds.has(member.incidentEpisodeId.toString())) {
1289
+ const episodeIdStr = member.incidentEpisodeId.toString();
1290
+ const incidentIdStr = member.incidentId.toString();
1291
+ if (!episodeMonitorsMap.has(episodeIdStr)) {
1292
+ episodeMonitorsMap.set(episodeIdStr, []);
1293
+ }
1294
+ const episodeMonitors = episodeMonitorsMap.get(episodeIdStr);
1295
+ const incidentMonitors = incidentMonitorsMap.get(incidentIdStr) || [];
1296
+ for (const monitorId of incidentMonitors) {
1297
+ if (!episodeMonitors.some((m) => {
1298
+ return m.toString() === monitorId.toString();
1299
+ })) {
1300
+ episodeMonitors.push(monitorId);
1301
+ }
1302
+ }
1303
+ }
1304
+ }
1305
+ // Serialize episodes and add monitors
1306
+ activeEpisodesJson = BaseModel.toJSONArray(activeEpisodes, IncidentEpisode);
1307
+ for (const episodeJson of activeEpisodesJson) {
1308
+ const episodeObj = episodeJson;
1309
+ const episodeId = (_a = episodeObj["_id"]) === null || _a === void 0 ? void 0 : _a.toString();
1310
+ if (episodeId) {
1311
+ const monitorIds = episodeMonitorsMap.get(episodeId) || [];
1312
+ episodeObj["monitors"] = monitorIds.map((id) => {
1313
+ return { _id: id.toString() };
1314
+ });
1315
+ }
1316
+ }
1317
+ // Get episode public notes
1318
+ const episodesOnStatusPage = activeEpisodes.map((episode) => {
1319
+ return episode.id;
1320
+ });
1321
+ if (episodesOnStatusPage.length > 0) {
1322
+ episodePublicNotes =
1323
+ await IncidentEpisodePublicNoteService.findBy({
1324
+ query: {
1325
+ incidentEpisodeId: QueryHelper.any(episodesOnStatusPage),
1326
+ projectId: statusPage.projectId,
1327
+ },
1328
+ select: {
1329
+ postedAt: true,
1330
+ note: true,
1331
+ incidentEpisodeId: true,
1332
+ attachments: {
1333
+ _id: true,
1334
+ name: true,
1335
+ },
1336
+ },
1337
+ sort: {
1338
+ postedAt: SortOrder.Descending,
1339
+ },
1340
+ skip: 0,
1341
+ limit: LIMIT_PER_PROJECT,
1342
+ props: {
1343
+ isRoot: true,
1344
+ },
1345
+ });
1346
+ // Get episode state timelines
1347
+ episodeStateTimelines =
1348
+ await IncidentEpisodeStateTimelineService.findBy({
1349
+ query: {
1350
+ incidentEpisodeId: QueryHelper.any(episodesOnStatusPage),
1351
+ projectId: statusPage.projectId,
1352
+ },
1353
+ select: {
1354
+ _id: true,
1355
+ createdAt: true,
1356
+ startsAt: true,
1357
+ incidentEpisodeId: true,
1358
+ incidentState: {
1359
+ name: true,
1360
+ color: true,
1361
+ isCreatedState: true,
1362
+ isAcknowledgedState: true,
1363
+ isResolvedState: true,
1364
+ },
1365
+ },
1366
+ sort: {
1367
+ startsAt: SortOrder.Descending,
1368
+ },
1369
+ skip: 0,
1370
+ limit: LIMIT_PER_PROJECT,
1371
+ props: {
1372
+ isRoot: true,
1373
+ },
1374
+ });
1375
+ }
1376
+ }
1377
+ }
1378
+ }
1108
1379
  // check if status page has active announcement.
1109
1380
  const today = OneUptimeDate.getCurrentDate();
1110
1381
  let activeAnnouncements = [];
@@ -1303,6 +1574,9 @@ export default class StatusPageAPI extends BaseAPI {
1303
1574
  activeAnnouncements: BaseModel.toJSONArray(activeAnnouncements, StatusPageAnnouncement),
1304
1575
  incidentPublicNotes: BaseModel.toJSONArray(incidentPublicNotes, IncidentPublicNote),
1305
1576
  activeIncidents: BaseModel.toJSONArray(activeIncidents, Incident),
1577
+ activeEpisodes: activeEpisodesJson,
1578
+ episodePublicNotes: BaseModel.toJSONArray(episodePublicNotes, IncidentEpisodePublicNote),
1579
+ episodeStateTimelines: BaseModel.toJSONArray(episodeStateTimelines, IncidentEpisodeStateTimeline),
1306
1580
  monitorStatusTimelines: BaseModel.toJSONArray(monitorStatusTimelines, MonitorStatusTimeline),
1307
1581
  resourceGroups: BaseModel.toJSONArray(statusPageGroups, StatusPageGroup),
1308
1582
  monitorStatuses: BaseModel.toJSONArray(monitorStatuses, MonitorStatus),
@@ -1319,8 +1593,8 @@ export default class StatusPageAPI extends BaseAPI {
1319
1593
  next(err);
1320
1594
  }
1321
1595
  });
1322
- this.router.put(`${(_w = new this.entityType()
1323
- .getCrudApiPath()) === null || _w === void 0 ? void 0 : _w.toString()}/update-subscription/:statusPageId/:subscriberId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1596
+ this.router.put(`${(_x = new this.entityType()
1597
+ .getCrudApiPath()) === null || _x === void 0 ? void 0 : _x.toString()}/update-subscription/:statusPageId/:subscriberId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1324
1598
  try {
1325
1599
  await this.subscribeToStatusPage(req);
1326
1600
  return Response.sendEmptySuccessResponse(req, res);
@@ -1329,8 +1603,8 @@ export default class StatusPageAPI extends BaseAPI {
1329
1603
  next(err);
1330
1604
  }
1331
1605
  });
1332
- this.router.post(`${(_x = new this.entityType()
1333
- .getCrudApiPath()) === null || _x === void 0 ? void 0 : _x.toString()}/get-subscription/:statusPageId/:subscriberId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1606
+ this.router.post(`${(_y = new this.entityType()
1607
+ .getCrudApiPath()) === null || _y === void 0 ? void 0 : _y.toString()}/get-subscription/:statusPageId/:subscriberId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1334
1608
  try {
1335
1609
  const subscriber = await this.getSubscriber(req);
1336
1610
  return Response.sendEntityResponse(req, res, subscriber, StatusPageSubscriber);
@@ -1339,8 +1613,8 @@ export default class StatusPageAPI extends BaseAPI {
1339
1613
  next(err);
1340
1614
  }
1341
1615
  });
1342
- this.router.post(`${(_y = new this.entityType()
1343
- .getCrudApiPath()) === null || _y === void 0 ? void 0 : _y.toString()}/subscribe/:statusPageId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1616
+ this.router.post(`${(_z = new this.entityType()
1617
+ .getCrudApiPath()) === null || _z === void 0 ? void 0 : _z.toString()}/subscribe/:statusPageId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1344
1618
  try {
1345
1619
  await this.subscribeToStatusPage(req);
1346
1620
  return Response.sendEmptySuccessResponse(req, res);
@@ -1349,8 +1623,8 @@ export default class StatusPageAPI extends BaseAPI {
1349
1623
  next(err);
1350
1624
  }
1351
1625
  });
1352
- this.router.post(`${(_z = new this.entityType()
1353
- .getCrudApiPath()) === null || _z === void 0 ? void 0 : _z.toString()}/manage-subscription/:statusPageId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1626
+ this.router.post(`${(_0 = new this.entityType()
1627
+ .getCrudApiPath()) === null || _0 === void 0 ? void 0 : _0.toString()}/manage-subscription/:statusPageId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1354
1628
  try {
1355
1629
  await this.manageExistingSubscription(req);
1356
1630
  return Response.sendEmptySuccessResponse(req, res);
@@ -1359,8 +1633,8 @@ export default class StatusPageAPI extends BaseAPI {
1359
1633
  next(err);
1360
1634
  }
1361
1635
  });
1362
- this.router.post(`${(_0 = new this.entityType()
1363
- .getCrudApiPath()) === null || _0 === void 0 ? void 0 : _0.toString()}/incidents/:statusPageIdOrDomain`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1636
+ this.router.post(`${(_1 = new this.entityType()
1637
+ .getCrudApiPath()) === null || _1 === void 0 ? void 0 : _1.toString()}/incidents/:statusPageIdOrDomain`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1364
1638
  try {
1365
1639
  const objectId = await resolveStatusPageIdOrThrow(req.params["statusPageIdOrDomain"]);
1366
1640
  const response = await this.getIncidents(objectId, null, req);
@@ -1370,8 +1644,8 @@ export default class StatusPageAPI extends BaseAPI {
1370
1644
  next(err);
1371
1645
  }
1372
1646
  });
1373
- this.router.post(`${(_1 = new this.entityType()
1374
- .getCrudApiPath()) === null || _1 === void 0 ? void 0 : _1.toString()}/scheduled-maintenance-events/:statusPageIdOrDomain`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1647
+ this.router.post(`${(_2 = new this.entityType()
1648
+ .getCrudApiPath()) === null || _2 === void 0 ? void 0 : _2.toString()}/scheduled-maintenance-events/:statusPageIdOrDomain`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1375
1649
  try {
1376
1650
  const objectId = await resolveStatusPageIdOrThrow(req.params["statusPageIdOrDomain"]);
1377
1651
  const response = await this.getScheduledMaintenanceEvents(objectId, null, req);
@@ -1381,8 +1655,8 @@ export default class StatusPageAPI extends BaseAPI {
1381
1655
  next(err);
1382
1656
  }
1383
1657
  });
1384
- this.router.post(`${(_2 = new this.entityType()
1385
- .getCrudApiPath()) === null || _2 === void 0 ? void 0 : _2.toString()}/announcements/:statusPageIdOrDomain`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1658
+ this.router.post(`${(_3 = new this.entityType()
1659
+ .getCrudApiPath()) === null || _3 === void 0 ? void 0 : _3.toString()}/announcements/:statusPageIdOrDomain`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1386
1660
  try {
1387
1661
  const objectId = await resolveStatusPageIdOrThrow(req.params["statusPageIdOrDomain"]);
1388
1662
  const response = await this.getAnnouncements(objectId, null, req);
@@ -1392,8 +1666,8 @@ export default class StatusPageAPI extends BaseAPI {
1392
1666
  next(err);
1393
1667
  }
1394
1668
  });
1395
- this.router.post(`${(_3 = new this.entityType()
1396
- .getCrudApiPath()) === null || _3 === void 0 ? void 0 : _3.toString()}/incidents/:statusPageIdOrDomain/:incidentId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1669
+ this.router.post(`${(_4 = new this.entityType()
1670
+ .getCrudApiPath()) === null || _4 === void 0 ? void 0 : _4.toString()}/incidents/:statusPageIdOrDomain/:incidentId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1397
1671
  try {
1398
1672
  const objectId = await resolveStatusPageIdOrThrow(req.params["statusPageIdOrDomain"]);
1399
1673
  const incidentId = new ObjectID(req.params["incidentId"]);
@@ -1404,8 +1678,8 @@ export default class StatusPageAPI extends BaseAPI {
1404
1678
  next(err);
1405
1679
  }
1406
1680
  });
1407
- this.router.post(`${(_4 = new this.entityType()
1408
- .getCrudApiPath()) === null || _4 === void 0 ? void 0 : _4.toString()}/scheduled-maintenance-events/:statusPageIdOrDomain/:scheduledMaintenanceId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1681
+ this.router.post(`${(_5 = new this.entityType()
1682
+ .getCrudApiPath()) === null || _5 === void 0 ? void 0 : _5.toString()}/scheduled-maintenance-events/:statusPageIdOrDomain/:scheduledMaintenanceId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1409
1683
  try {
1410
1684
  const objectId = await resolveStatusPageIdOrThrow(req.params["statusPageIdOrDomain"]);
1411
1685
  const scheduledMaintenanceId = new ObjectID(req.params["scheduledMaintenanceId"]);
@@ -1416,8 +1690,8 @@ export default class StatusPageAPI extends BaseAPI {
1416
1690
  next(err);
1417
1691
  }
1418
1692
  });
1419
- this.router.post(`${(_5 = new this.entityType()
1420
- .getCrudApiPath()) === null || _5 === void 0 ? void 0 : _5.toString()}/announcements/:statusPageIdOrDomain/:announcementId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1693
+ this.router.post(`${(_6 = new this.entityType()
1694
+ .getCrudApiPath()) === null || _6 === void 0 ? void 0 : _6.toString()}/announcements/:statusPageIdOrDomain/:announcementId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1421
1695
  try {
1422
1696
  const objectId = await resolveStatusPageIdOrThrow(req.params["statusPageIdOrDomain"]);
1423
1697
  const announcementId = new ObjectID(req.params["announcementId"]);
@@ -1428,6 +1702,30 @@ export default class StatusPageAPI extends BaseAPI {
1428
1702
  next(err);
1429
1703
  }
1430
1704
  });
1705
+ // Episodes endpoints
1706
+ this.router.post(`${(_7 = new this.entityType()
1707
+ .getCrudApiPath()) === null || _7 === void 0 ? void 0 : _7.toString()}/episodes/:statusPageIdOrDomain`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1708
+ try {
1709
+ const objectId = await resolveStatusPageIdOrThrow(req.params["statusPageIdOrDomain"]);
1710
+ const response = await this.getEpisodes(objectId, null, req);
1711
+ return Response.sendJsonObjectResponse(req, res, response);
1712
+ }
1713
+ catch (err) {
1714
+ next(err);
1715
+ }
1716
+ });
1717
+ this.router.post(`${(_8 = new this.entityType()
1718
+ .getCrudApiPath()) === null || _8 === void 0 ? void 0 : _8.toString()}/episodes/:statusPageIdOrDomain/:episodeId`, UserMiddleware.getUserMiddleware, async (req, res, next) => {
1719
+ try {
1720
+ const objectId = await resolveStatusPageIdOrThrow(req.params["statusPageIdOrDomain"]);
1721
+ const episodeId = new ObjectID(req.params["episodeId"]);
1722
+ const response = await this.getEpisodes(objectId, episodeId, req);
1723
+ return Response.sendJsonObjectResponse(req, res, response);
1724
+ }
1725
+ catch (err) {
1726
+ next(err);
1727
+ }
1728
+ });
1431
1729
  }
1432
1730
  async getScheduledMaintenanceEvents(statusPageId, scheduledMaintenanceId, req) {
1433
1731
  await this.checkHasReadAccess({
@@ -2448,6 +2746,381 @@ export default class StatusPageAPI extends BaseAPI {
2448
2746
  };
2449
2747
  return response;
2450
2748
  }
2749
+ async getEpisodes(statusPageId, episodeId, req) {
2750
+ var _a;
2751
+ await this.checkHasReadAccess({
2752
+ statusPageId: statusPageId,
2753
+ req: req,
2754
+ });
2755
+ const statusPage = await StatusPageService.findOneBy({
2756
+ query: {
2757
+ _id: statusPageId.toString(),
2758
+ },
2759
+ select: {
2760
+ _id: true,
2761
+ projectId: true,
2762
+ showEpisodeHistoryInDays: true,
2763
+ showEpisodesOnStatusPage: true,
2764
+ showEpisodeLabelsOnStatusPage: true,
2765
+ },
2766
+ props: {
2767
+ isRoot: true,
2768
+ },
2769
+ });
2770
+ if (!statusPage) {
2771
+ throw new BadDataException("Status Page not found");
2772
+ }
2773
+ if (!statusPage.showEpisodesOnStatusPage) {
2774
+ throw new BadDataException("Episodes are not enabled on this status page.");
2775
+ }
2776
+ // get monitors on status page.
2777
+ const statusPageResources = await StatusPageService.getStatusPageResources({
2778
+ statusPageId: statusPageId,
2779
+ });
2780
+ const { monitorsOnStatusPage, monitorsInGroup } = await StatusPageService.getMonitorIdsOnStatusPage({
2781
+ statusPageId: statusPageId,
2782
+ });
2783
+ const today = OneUptimeDate.getCurrentDate();
2784
+ const historyDays = OneUptimeDate.getSomeDaysAgo(statusPage.showEpisodeHistoryInDays || 14);
2785
+ /*
2786
+ * Get incidents that have monitors on this status page
2787
+ * Note: We don't filter by incident.isVisibleOnStatusPage here because
2788
+ * episode visibility is independent of incident visibility.
2789
+ * An episode should show if episode.isVisibleOnStatusPage is true,
2790
+ * regardless of whether its member incidents are visible.
2791
+ */
2792
+ const incidentQuery = {
2793
+ monitors: monitorsOnStatusPage,
2794
+ projectId: statusPage.projectId,
2795
+ createdAt: QueryHelper.inBetween(historyDays, today),
2796
+ };
2797
+ let incidents = [];
2798
+ if (monitorsOnStatusPage.length > 0) {
2799
+ incidents = await IncidentService.findBy({
2800
+ query: incidentQuery,
2801
+ select: {
2802
+ _id: true,
2803
+ },
2804
+ skip: 0,
2805
+ limit: LIMIT_PER_PROJECT,
2806
+ props: {
2807
+ isRoot: true,
2808
+ },
2809
+ });
2810
+ }
2811
+ const incidentIds = incidents.map((incident) => {
2812
+ return incident.id;
2813
+ });
2814
+ // Get episode members that link to these incidents
2815
+ let episodeMembers = [];
2816
+ if (incidentIds.length > 0) {
2817
+ episodeMembers = await IncidentEpisodeMemberService.findBy({
2818
+ query: {
2819
+ incidentId: QueryHelper.any(incidentIds),
2820
+ projectId: statusPage.projectId,
2821
+ },
2822
+ select: {
2823
+ incidentEpisodeId: true,
2824
+ incidentId: true,
2825
+ },
2826
+ skip: 0,
2827
+ limit: LIMIT_PER_PROJECT,
2828
+ props: {
2829
+ isRoot: true,
2830
+ },
2831
+ });
2832
+ }
2833
+ // Get unique episode IDs
2834
+ const episodeIdsFromMembers = new Set();
2835
+ for (const member of episodeMembers) {
2836
+ if (member.incidentEpisodeId) {
2837
+ episodeIdsFromMembers.add(member.incidentEpisodeId.toString());
2838
+ }
2839
+ }
2840
+ let episodeQuery = {
2841
+ _id: QueryHelper.any(Array.from(episodeIdsFromMembers).map((id) => {
2842
+ return new ObjectID(id);
2843
+ })),
2844
+ projectId: statusPage.projectId,
2845
+ isVisibleOnStatusPage: true,
2846
+ };
2847
+ if (episodeId) {
2848
+ episodeQuery = {
2849
+ _id: episodeId.toString(),
2850
+ projectId: statusPage.projectId,
2851
+ isVisibleOnStatusPage: true,
2852
+ };
2853
+ // When viewing a specific episode, also fetch its members directly
2854
+ const episodeMembersForSpecificEpisode = await IncidentEpisodeMemberService.findBy({
2855
+ query: {
2856
+ incidentEpisodeId: episodeId,
2857
+ projectId: statusPage.projectId,
2858
+ },
2859
+ select: {
2860
+ incidentEpisodeId: true,
2861
+ incidentId: true,
2862
+ },
2863
+ skip: 0,
2864
+ limit: LIMIT_PER_PROJECT,
2865
+ props: {
2866
+ isRoot: true,
2867
+ },
2868
+ });
2869
+ // Merge with existing episode members
2870
+ for (const member of episodeMembersForSpecificEpisode) {
2871
+ if (!episodeMembers.some((m) => {
2872
+ var _a, _b, _c, _d;
2873
+ return (((_a = m.incidentEpisodeId) === null || _a === void 0 ? void 0 : _a.toString()) ===
2874
+ ((_b = member.incidentEpisodeId) === null || _b === void 0 ? void 0 : _b.toString()) &&
2875
+ ((_c = m.incidentId) === null || _c === void 0 ? void 0 : _c.toString()) === ((_d = member.incidentId) === null || _d === void 0 ? void 0 : _d.toString()));
2876
+ })) {
2877
+ episodeMembers.push(member);
2878
+ }
2879
+ }
2880
+ }
2881
+ // Get episodes
2882
+ let episodes = [];
2883
+ let selectEpisodes = {
2884
+ createdAt: true,
2885
+ declaredAt: true,
2886
+ updatedAt: true,
2887
+ title: true,
2888
+ description: true,
2889
+ _id: true,
2890
+ episodeNumber: true,
2891
+ incidentSeverity: {
2892
+ name: true,
2893
+ color: true,
2894
+ },
2895
+ currentIncidentState: {
2896
+ name: true,
2897
+ color: true,
2898
+ _id: true,
2899
+ order: true,
2900
+ isCreatedState: true,
2901
+ isAcknowledgedState: true,
2902
+ isResolvedState: true,
2903
+ },
2904
+ incidentCount: true,
2905
+ };
2906
+ if (statusPage.showEpisodeLabelsOnStatusPage) {
2907
+ selectEpisodes = Object.assign(Object.assign({}, selectEpisodes), { labels: {
2908
+ name: true,
2909
+ color: true,
2910
+ } });
2911
+ }
2912
+ if (episodeIdsFromMembers.size > 0 || episodeId) {
2913
+ episodes = await IncidentEpisodeService.findBy({
2914
+ query: episodeQuery,
2915
+ select: selectEpisodes,
2916
+ sort: {
2917
+ declaredAt: SortOrder.Descending,
2918
+ createdAt: SortOrder.Descending,
2919
+ },
2920
+ skip: 0,
2921
+ limit: LIMIT_PER_PROJECT,
2922
+ props: {
2923
+ isRoot: true,
2924
+ },
2925
+ });
2926
+ }
2927
+ // If no specific episode, also fetch active (unresolved) episodes
2928
+ if (!episodeId && episodeIdsFromMembers.size > 0) {
2929
+ const unresolvedIncidentStates = await IncidentStateService.getUnresolvedIncidentStates(statusPage.projectId, {
2930
+ isRoot: true,
2931
+ });
2932
+ const unresolvedIncidentStateIds = unresolvedIncidentStates.map((state) => {
2933
+ return state.id;
2934
+ });
2935
+ const activeEpisodes = await IncidentEpisodeService.findBy({
2936
+ query: {
2937
+ _id: QueryHelper.any(Array.from(episodeIdsFromMembers).map((id) => {
2938
+ return new ObjectID(id);
2939
+ })),
2940
+ isVisibleOnStatusPage: true,
2941
+ currentIncidentStateId: QueryHelper.any(unresolvedIncidentStateIds),
2942
+ projectId: statusPage.projectId,
2943
+ },
2944
+ select: selectEpisodes,
2945
+ sort: {
2946
+ declaredAt: SortOrder.Descending,
2947
+ createdAt: SortOrder.Descending,
2948
+ },
2949
+ skip: 0,
2950
+ limit: LIMIT_PER_PROJECT,
2951
+ props: {
2952
+ isRoot: true,
2953
+ },
2954
+ });
2955
+ episodes = [...activeEpisodes, ...episodes];
2956
+ episodes = ArrayUtil.distinctByFieldName(episodes, "_id");
2957
+ }
2958
+ const episodesOnStatusPage = episodes.map((episode) => {
2959
+ return episode.id;
2960
+ });
2961
+ /*
2962
+ * Build a map of episode ID -> monitor IDs from episode members
2963
+ * Collect all unique incident IDs from episode members
2964
+ */
2965
+ const memberIncidentIds = [];
2966
+ for (const member of episodeMembers) {
2967
+ if (member.incidentId &&
2968
+ !memberIncidentIds.some((id) => {
2969
+ return id.toString() === member.incidentId.toString();
2970
+ })) {
2971
+ memberIncidentIds.push(member.incidentId);
2972
+ }
2973
+ }
2974
+ // Fetch incidents with their monitors
2975
+ let memberIncidents = [];
2976
+ if (memberIncidentIds.length > 0) {
2977
+ memberIncidents = await IncidentService.findBy({
2978
+ query: {
2979
+ _id: QueryHelper.any(memberIncidentIds),
2980
+ projectId: statusPage.projectId,
2981
+ },
2982
+ select: {
2983
+ _id: true,
2984
+ monitors: {
2985
+ _id: true,
2986
+ },
2987
+ },
2988
+ skip: 0,
2989
+ limit: LIMIT_PER_PROJECT,
2990
+ props: {
2991
+ isRoot: true,
2992
+ },
2993
+ });
2994
+ }
2995
+ // Build a map of incident ID -> monitors
2996
+ const incidentMonitorsMap = new Map();
2997
+ for (const incident of memberIncidents) {
2998
+ const incidentIdStr = incident.id.toString();
2999
+ const monitorIds = (incident.monitors || [])
3000
+ .map((m) => {
3001
+ var _a, _b;
3002
+ return new ObjectID(((_a = m._id) === null || _a === void 0 ? void 0 : _a.toString()) || ((_b = m.id) === null || _b === void 0 ? void 0 : _b.toString()) || "");
3003
+ })
3004
+ .filter((id) => {
3005
+ return id.toString() !== "";
3006
+ });
3007
+ incidentMonitorsMap.set(incidentIdStr, monitorIds);
3008
+ }
3009
+ // Build episode monitors map from members and incident monitors
3010
+ const episodeMonitorsMap = new Map();
3011
+ for (const member of episodeMembers) {
3012
+ if (member.incidentEpisodeId && member.incidentId) {
3013
+ const episodeIdStr = member.incidentEpisodeId.toString();
3014
+ const incidentIdStr = member.incidentId.toString();
3015
+ if (!episodeMonitorsMap.has(episodeIdStr)) {
3016
+ episodeMonitorsMap.set(episodeIdStr, []);
3017
+ }
3018
+ const episodeMonitors = episodeMonitorsMap.get(episodeIdStr);
3019
+ const incidentMonitors = incidentMonitorsMap.get(incidentIdStr) || [];
3020
+ for (const monitorId of incidentMonitors) {
3021
+ if (!episodeMonitors.some((m) => {
3022
+ return m.toString() === monitorId.toString();
3023
+ })) {
3024
+ episodeMonitors.push(monitorId);
3025
+ }
3026
+ }
3027
+ }
3028
+ }
3029
+ // Get public notes for episodes
3030
+ let episodePublicNotes = [];
3031
+ if (episodesOnStatusPage.length > 0) {
3032
+ episodePublicNotes = await IncidentEpisodePublicNoteService.findBy({
3033
+ query: {
3034
+ incidentEpisodeId: QueryHelper.any(episodesOnStatusPage),
3035
+ projectId: statusPage.projectId,
3036
+ },
3037
+ select: {
3038
+ postedAt: true,
3039
+ note: true,
3040
+ incidentEpisodeId: true,
3041
+ attachments: {
3042
+ _id: true,
3043
+ name: true,
3044
+ },
3045
+ },
3046
+ sort: {
3047
+ postedAt: SortOrder.Descending,
3048
+ },
3049
+ skip: 0,
3050
+ limit: LIMIT_PER_PROJECT,
3051
+ props: {
3052
+ isRoot: true,
3053
+ },
3054
+ });
3055
+ }
3056
+ // Get state timelines for episodes
3057
+ let episodeStateTimelines = [];
3058
+ if (episodesOnStatusPage.length > 0) {
3059
+ episodeStateTimelines = await IncidentEpisodeStateTimelineService.findBy({
3060
+ query: {
3061
+ incidentEpisodeId: QueryHelper.any(episodesOnStatusPage),
3062
+ projectId: statusPage.projectId,
3063
+ },
3064
+ select: {
3065
+ _id: true,
3066
+ createdAt: true,
3067
+ startsAt: true,
3068
+ incidentEpisodeId: true,
3069
+ incidentState: {
3070
+ name: true,
3071
+ color: true,
3072
+ isCreatedState: true,
3073
+ isAcknowledgedState: true,
3074
+ isResolvedState: true,
3075
+ },
3076
+ },
3077
+ sort: {
3078
+ startsAt: SortOrder.Descending,
3079
+ },
3080
+ skip: 0,
3081
+ limit: LIMIT_PER_PROJECT,
3082
+ props: {
3083
+ isRoot: true,
3084
+ },
3085
+ });
3086
+ }
3087
+ // Get all incident states for this project
3088
+ const incidentStates = await IncidentStateService.findBy({
3089
+ query: {
3090
+ projectId: statusPage.projectId,
3091
+ },
3092
+ select: {
3093
+ isResolvedState: true,
3094
+ order: true,
3095
+ },
3096
+ limit: LIMIT_PER_PROJECT,
3097
+ skip: 0,
3098
+ props: {
3099
+ isRoot: true,
3100
+ },
3101
+ });
3102
+ // Serialize episodes and add monitors to each
3103
+ const episodesJson = BaseModel.toJSONArray(episodes, IncidentEpisode);
3104
+ for (const episodeJson of episodesJson) {
3105
+ const episodeObj = episodeJson;
3106
+ const episodeId = (_a = episodeObj["_id"]) === null || _a === void 0 ? void 0 : _a.toString();
3107
+ if (episodeId) {
3108
+ const monitorIds = episodeMonitorsMap.get(episodeId) || [];
3109
+ episodeObj["monitors"] = monitorIds.map((id) => {
3110
+ return { _id: id.toString() };
3111
+ });
3112
+ }
3113
+ }
3114
+ const response = {
3115
+ episodePublicNotes: BaseModel.toJSONArray(episodePublicNotes, IncidentEpisodePublicNote),
3116
+ incidentStates: BaseModel.toJSONArray(incidentStates, IncidentState),
3117
+ episodes: episodesJson,
3118
+ statusPageResources: BaseModel.toJSONArray(statusPageResources, StatusPageResource),
3119
+ episodeStateTimelines: BaseModel.toJSONArray(episodeStateTimelines, IncidentEpisodeStateTimeline),
3120
+ monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
3121
+ };
3122
+ return response;
3123
+ }
2451
3124
  async getStatusPageResourcesAndTimelines(data) {
2452
3125
  const objectId = data.statusPageId;
2453
3126
  const statusPage = await StatusPageService.findOneBy({
@@ -2461,6 +3134,7 @@ export default class StatusPageAPI extends BaseAPI {
2461
3134
  overviewPageDescription: true,
2462
3135
  showIncidentLabelsOnStatusPage: true,
2463
3136
  showScheduledEventLabelsOnStatusPage: true,
3137
+ showEpisodeLabelsOnStatusPage: true,
2464
3138
  downtimeMonitorStatuses: {
2465
3139
  _id: true,
2466
3140
  },
@@ -2469,6 +3143,7 @@ export default class StatusPageAPI extends BaseAPI {
2469
3143
  overallUptimePercentPrecision: true,
2470
3144
  showAnnouncementsOnStatusPage: true,
2471
3145
  showIncidentsOnStatusPage: true,
3146
+ showEpisodesOnStatusPage: true,
2472
3147
  showScheduledMaintenanceEventsOnStatusPage: true,
2473
3148
  },
2474
3149
  props: {
@@ -3017,6 +3692,152 @@ export default class StatusPageAPI extends BaseAPI {
3017
3692
  Response.setNoCacheHeaders(res);
3018
3693
  return Response.sendFileResponse(req, res, attachment);
3019
3694
  }
3695
+ async getIncidentEpisodePublicNoteAttachment(req, res) {
3696
+ var _a;
3697
+ const statusPageIdParam = req.params["statusPageId"];
3698
+ const episodeIdParam = req.params["episodeId"];
3699
+ const noteIdParam = req.params["noteId"];
3700
+ const fileIdParam = req.params["fileId"];
3701
+ if (!statusPageIdParam || !episodeIdParam || !noteIdParam || !fileIdParam) {
3702
+ throw new NotFoundException("Attachment not found");
3703
+ }
3704
+ let statusPageId;
3705
+ let episodeId;
3706
+ let noteId;
3707
+ let fileId;
3708
+ try {
3709
+ statusPageId = new ObjectID(statusPageIdParam);
3710
+ episodeId = new ObjectID(episodeIdParam);
3711
+ noteId = new ObjectID(noteIdParam);
3712
+ fileId = new ObjectID(fileIdParam);
3713
+ }
3714
+ catch (_b) {
3715
+ throw new NotFoundException("Attachment not found");
3716
+ }
3717
+ await this.checkHasReadAccess({
3718
+ statusPageId: statusPageId,
3719
+ req: req,
3720
+ });
3721
+ const statusPage = await StatusPageService.findOneBy({
3722
+ query: {
3723
+ _id: statusPageId.toString(),
3724
+ },
3725
+ select: {
3726
+ _id: true,
3727
+ projectId: true,
3728
+ showEpisodesOnStatusPage: true,
3729
+ },
3730
+ props: {
3731
+ isRoot: true,
3732
+ },
3733
+ });
3734
+ if (!statusPage || !statusPage.projectId) {
3735
+ throw new NotFoundException("Attachment not found");
3736
+ }
3737
+ if (!statusPage.showEpisodesOnStatusPage) {
3738
+ throw new NotFoundException("Attachment not found");
3739
+ }
3740
+ const { monitorsOnStatusPage } = await StatusPageService.getMonitorIdsOnStatusPage({
3741
+ statusPageId: statusPageId,
3742
+ });
3743
+ if (!monitorsOnStatusPage || monitorsOnStatusPage.length === 0) {
3744
+ throw new NotFoundException("Attachment not found");
3745
+ }
3746
+ // Get episode members (incidents) that are linked to monitors on the status page
3747
+ const episodeMembers = await IncidentEpisodeMemberService.findBy({
3748
+ query: {
3749
+ incidentEpisodeId: episodeId,
3750
+ projectId: statusPage.projectId,
3751
+ },
3752
+ select: {
3753
+ incidentId: true,
3754
+ },
3755
+ limit: LIMIT_PER_PROJECT,
3756
+ skip: 0,
3757
+ props: {
3758
+ isRoot: true,
3759
+ },
3760
+ });
3761
+ if (episodeMembers.length === 0) {
3762
+ throw new NotFoundException("Attachment not found");
3763
+ }
3764
+ const incidentIds = episodeMembers
3765
+ .map((member) => {
3766
+ return member.incidentId;
3767
+ })
3768
+ .filter((id) => {
3769
+ return Boolean(id);
3770
+ });
3771
+ // Check if any of the incidents are linked to monitors on the status page
3772
+ const incident = await IncidentService.findOneBy({
3773
+ query: {
3774
+ _id: QueryHelper.any(incidentIds),
3775
+ projectId: statusPage.projectId,
3776
+ isVisibleOnStatusPage: true,
3777
+ monitors: monitorsOnStatusPage,
3778
+ },
3779
+ select: {
3780
+ _id: true,
3781
+ },
3782
+ props: {
3783
+ isRoot: true,
3784
+ },
3785
+ });
3786
+ if (!incident) {
3787
+ throw new NotFoundException("Attachment not found");
3788
+ }
3789
+ // Verify the episode exists and is visible
3790
+ const episode = await IncidentEpisodeService.findOneBy({
3791
+ query: {
3792
+ _id: episodeId.toString(),
3793
+ projectId: statusPage.projectId,
3794
+ isVisibleOnStatusPage: true,
3795
+ },
3796
+ select: {
3797
+ _id: true,
3798
+ },
3799
+ props: {
3800
+ isRoot: true,
3801
+ },
3802
+ });
3803
+ if (!episode) {
3804
+ throw new NotFoundException("Attachment not found");
3805
+ }
3806
+ const episodePublicNote = await IncidentEpisodePublicNoteService.findOneBy({
3807
+ query: {
3808
+ _id: noteId.toString(),
3809
+ incidentEpisodeId: episodeId.toString(),
3810
+ projectId: statusPage.projectId,
3811
+ },
3812
+ select: {
3813
+ attachments: {
3814
+ _id: true,
3815
+ file: true,
3816
+ fileType: true,
3817
+ name: true,
3818
+ },
3819
+ },
3820
+ props: {
3821
+ isRoot: true,
3822
+ },
3823
+ });
3824
+ if (!episodePublicNote) {
3825
+ throw new NotFoundException("Attachment not found");
3826
+ }
3827
+ const attachment = (_a = episodePublicNote.attachments) === null || _a === void 0 ? void 0 : _a.find((file) => {
3828
+ const attachmentId = file._id
3829
+ ? file._id.toString()
3830
+ : file.id
3831
+ ? file.id.toString()
3832
+ : null;
3833
+ return attachmentId === fileId.toString();
3834
+ });
3835
+ if (!attachment || !attachment.file) {
3836
+ throw new NotFoundException("Attachment not found");
3837
+ }
3838
+ Response.setNoCacheHeaders(res);
3839
+ return Response.sendFileResponse(req, res, attachment);
3840
+ }
3020
3841
  async checkHasReadAccess(data) {
3021
3842
  const accessResult = await this.service.hasReadAccess({
3022
3843
  statusPageId: data.statusPageId,
@@ -3064,6 +3885,12 @@ __decorate([
3064
3885
  __metadata("design:paramtypes", [ObjectID, Object, Object]),
3065
3886
  __metadata("design:returntype", Promise)
3066
3887
  ], StatusPageAPI.prototype, "getIncidents", null);
3888
+ __decorate([
3889
+ CaptureSpan(),
3890
+ __metadata("design:type", Function),
3891
+ __metadata("design:paramtypes", [ObjectID, Object, Object]),
3892
+ __metadata("design:returntype", Promise)
3893
+ ], StatusPageAPI.prototype, "getEpisodes", null);
3067
3894
  __decorate([
3068
3895
  CaptureSpan(),
3069
3896
  __metadata("design:type", Function),