@oneuptime/common 11.3.19 → 11.3.21

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 (77) hide show
  1. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +173 -0
  2. package/Models/DatabaseModels/GlobalConfig.ts +39 -0
  3. package/Models/DatabaseModels/Index.ts +2 -0
  4. package/Server/API/DashboardAPI.ts +221 -0
  5. package/Server/API/EnterpriseLicenseAPI.ts +279 -12
  6. package/Server/API/GlobalConfigAPI.ts +81 -16
  7. package/Server/API/StatusPageAPI.ts +894 -791
  8. package/Server/EnvironmentConfig.ts +9 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1783161406136-AddInstanceIdToGlobalConfig.ts +23 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.ts +47 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  12. package/Server/Services/EnterpriseLicenseInstanceService.ts +10 -0
  13. package/Server/Services/Index.ts +2 -0
  14. package/Server/Services/IoTDeviceService.ts +99 -4
  15. package/Server/Services/StatusPageService.ts +104 -45
  16. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +6 -4
  17. package/Server/Utils/Monitor/MonitorCriteriaDataExtractor.ts +7 -6
  18. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +6 -4
  19. package/Server/Utils/Telemetry/IoTSnapshotScan.ts +45 -7
  20. package/ServiceRoute.ts +4 -0
  21. package/Tests/Server/Services/IoTDeviceService.test.ts +280 -0
  22. package/Tests/Server/Utils/Monitor/Criteria/TelemetryStepMetricAlias.test.ts +390 -0
  23. package/Tests/Server/Utils/Telemetry/IoTSnapshotScan.test.ts +202 -0
  24. package/Tests/Utils/EnterpriseLicenseUsage.test.ts +165 -0
  25. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +18 -0
  26. package/Types/Monitor/MonitorStep.ts +32 -0
  27. package/UI/Components/EditionLabel/EditionLabel.tsx +172 -0
  28. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +125 -0
  29. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +197 -0
  30. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -0
  31. package/build/dist/Models/DatabaseModels/GlobalConfig.js +39 -0
  32. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  33. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  34. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  35. package/build/dist/Server/API/DashboardAPI.js +166 -16
  36. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  37. package/build/dist/Server/API/EnterpriseLicenseAPI.js +203 -13
  38. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  39. package/build/dist/Server/API/GlobalConfigAPI.js +62 -15
  40. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  41. package/build/dist/Server/API/StatusPageAPI.js +74 -31
  42. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  43. package/build/dist/Server/EnvironmentConfig.js +6 -0
  44. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  45. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783161406136-AddInstanceIdToGlobalConfig.js +14 -0
  46. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783161406136-AddInstanceIdToGlobalConfig.js.map +1 -0
  47. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.js +22 -0
  48. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.js.map +1 -0
  49. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  50. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  51. package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js +9 -0
  52. package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js.map +1 -0
  53. package/build/dist/Server/Services/Index.js +2 -0
  54. package/build/dist/Server/Services/Index.js.map +1 -1
  55. package/build/dist/Server/Services/IoTDeviceService.js +66 -4
  56. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  57. package/build/dist/Server/Services/StatusPageService.js +91 -43
  58. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  59. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +4 -5
  60. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  61. package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js +6 -5
  62. package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js.map +1 -1
  63. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -9
  64. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  65. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +33 -2
  66. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
  67. package/build/dist/ServiceRoute.js +1 -0
  68. package/build/dist/ServiceRoute.js.map +1 -1
  69. package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.js +2 -0
  70. package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.js.map +1 -0
  71. package/build/dist/Types/Monitor/MonitorStep.js +25 -0
  72. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  73. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +92 -0
  74. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  75. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +83 -0
  76. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -0
  77. package/package.json +1 -1
@@ -1492,171 +1492,242 @@ export default class StatusPageAPI extends BaseAPI<
1492
1492
  },
1493
1493
  );
1494
1494
 
1495
- this.router.post(
1496
- `${new this.entityType()
1497
- .getCrudApiPath()
1498
- ?.toString()}/overview/:statusPageIdOrDomain`,
1499
- UserMiddleware.getUserMiddleware,
1500
- async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
1501
- try {
1502
- const statusPageId: ObjectID = await resolveStatusPageIdOrThrow(
1503
- req.params["statusPageIdOrDomain"] as string,
1504
- );
1495
+ /*
1496
+ * Shared handler for the overview endpoint. Registered for both POST
1497
+ * (used by the status page SPA) and GET (machine-readable access for
1498
+ * crawlers / AI agents) so the authentication and authorization path
1499
+ * can never drift between the two methods. It does not read req.body.
1500
+ */
1501
+ const overviewHandler: (
1502
+ req: ExpressRequest,
1503
+ res: ExpressResponse,
1504
+ next: NextFunction,
1505
+ ) => Promise<void> = async (
1506
+ req: ExpressRequest,
1507
+ res: ExpressResponse,
1508
+ next: NextFunction,
1509
+ ): Promise<void> => {
1510
+ try {
1511
+ const statusPageId: ObjectID = await resolveStatusPageIdOrThrow(
1512
+ req.params["statusPageIdOrDomain"] as string,
1513
+ );
1505
1514
 
1506
- await this.checkHasReadAccess({
1507
- statusPageId: statusPageId,
1508
- req: req,
1515
+ await this.checkHasReadAccess({
1516
+ statusPageId: statusPageId,
1517
+ req: req,
1518
+ });
1519
+
1520
+ // First fetch the status page to get the configured uptime history days
1521
+ const statusPageForDays: StatusPage | null =
1522
+ await StatusPageService.findOneBy({
1523
+ query: {
1524
+ _id: statusPageId.toString(),
1525
+ },
1526
+ select: {
1527
+ showUptimeHistoryInDays: true,
1528
+ },
1529
+ props: {
1530
+ isRoot: true,
1531
+ },
1509
1532
  });
1510
1533
 
1511
- // First fetch the status page to get the configured uptime history days
1512
- const statusPageForDays: StatusPage | null =
1513
- await StatusPageService.findOneBy({
1534
+ let uptimeHistoryDays: number =
1535
+ statusPageForDays?.showUptimeHistoryInDays || 90;
1536
+
1537
+ if (uptimeHistoryDays > 90) {
1538
+ uptimeHistoryDays = 90;
1539
+ }
1540
+
1541
+ if (uptimeHistoryDays < 1) {
1542
+ uptimeHistoryDays = 1;
1543
+ }
1544
+
1545
+ const startDate: Date = OneUptimeDate.getSomeDaysAgo(uptimeHistoryDays);
1546
+ const endDate: Date = OneUptimeDate.getCurrentDate();
1547
+
1548
+ const {
1549
+ monitorStatuses,
1550
+ monitorGroupCurrentStatuses,
1551
+ statusPageResources,
1552
+ statusPage,
1553
+ monitorsOnStatusPage,
1554
+ monitorStatusTimelines,
1555
+ statusPageGroups,
1556
+ monitorsInGroup,
1557
+ } = await this.getStatusPageResourcesAndTimelines({
1558
+ statusPageId: statusPageId,
1559
+ startDateForMonitorTimeline: startDate,
1560
+ endDateForMonitorTimeline: endDate,
1561
+ });
1562
+
1563
+ // check if status page has active incident.
1564
+ let activeIncidents: Array<Incident> = [];
1565
+ if (monitorsOnStatusPage.length > 0) {
1566
+ let select: Select<Incident> = {
1567
+ createdAt: true,
1568
+ declaredAt: true,
1569
+ updatedAt: true,
1570
+ title: true,
1571
+ description: true,
1572
+ _id: true,
1573
+ postmortemNote: true,
1574
+ postmortemPostedAt: true,
1575
+ showPostmortemOnStatusPage: true,
1576
+ postmortemAttachments: {
1577
+ _id: true,
1578
+ name: true,
1579
+ },
1580
+ incidentSeverity: {
1581
+ name: true,
1582
+ color: true,
1583
+ },
1584
+ currentIncidentState: {
1585
+ _id: true,
1586
+ name: true,
1587
+ color: true,
1588
+ order: true,
1589
+ },
1590
+ monitors: {
1591
+ _id: true,
1592
+ },
1593
+ };
1594
+
1595
+ if (statusPage.showIncidentLabelsOnStatusPage) {
1596
+ select = {
1597
+ ...select,
1598
+ labels: {
1599
+ name: true,
1600
+ color: true,
1601
+ },
1602
+ };
1603
+ }
1604
+
1605
+ const unresolvedIncidentStates: Array<IncidentState> =
1606
+ await IncidentStateService.getUnresolvedIncidentStates(
1607
+ statusPage.projectId!,
1608
+ {
1609
+ isRoot: true,
1610
+ },
1611
+ );
1612
+
1613
+ const unresolvedIncidentStateIds: Array<ObjectID> =
1614
+ unresolvedIncidentStates.map((state: IncidentState) => {
1615
+ return state.id!;
1616
+ });
1617
+
1618
+ if (statusPage.showIncidentsOnStatusPage) {
1619
+ activeIncidents = await IncidentService.findBy({
1514
1620
  query: {
1515
- _id: statusPageId.toString(),
1621
+ monitors: monitorsOnStatusPage as any,
1622
+ currentIncidentStateId: QueryHelper.any(
1623
+ unresolvedIncidentStateIds,
1624
+ ),
1625
+ isVisibleOnStatusPage: true,
1626
+ projectId: statusPage.projectId!,
1516
1627
  },
1517
- select: {
1518
- showUptimeHistoryInDays: true,
1628
+ select: select,
1629
+ sort: {
1630
+ declaredAt: SortOrder.Descending,
1631
+ createdAt: SortOrder.Descending,
1519
1632
  },
1633
+
1634
+ skip: 0,
1635
+ limit: LIMIT_PER_PROJECT,
1520
1636
  props: {
1521
1637
  isRoot: true,
1522
1638
  },
1523
1639
  });
1524
-
1525
- let uptimeHistoryDays: number =
1526
- statusPageForDays?.showUptimeHistoryInDays || 90;
1527
-
1528
- if (uptimeHistoryDays > 90) {
1529
- uptimeHistoryDays = 90;
1530
- }
1531
-
1532
- if (uptimeHistoryDays < 1) {
1533
- uptimeHistoryDays = 1;
1534
1640
  }
1641
+ }
1535
1642
 
1536
- const startDate: Date =
1537
- OneUptimeDate.getSomeDaysAgo(uptimeHistoryDays);
1538
- const endDate: Date = OneUptimeDate.getCurrentDate();
1643
+ const incidentsOnStatusPage: Array<ObjectID> = activeIncidents.map(
1644
+ (incident: Incident) => {
1645
+ return incident.id!;
1646
+ },
1647
+ );
1539
1648
 
1540
- const {
1541
- monitorStatuses,
1542
- monitorGroupCurrentStatuses,
1543
- statusPageResources,
1544
- statusPage,
1545
- monitorsOnStatusPage,
1546
- monitorStatusTimelines,
1547
- statusPageGroups,
1548
- monitorsInGroup,
1549
- } = await this.getStatusPageResourcesAndTimelines({
1550
- statusPageId: statusPageId,
1551
- startDateForMonitorTimeline: startDate,
1552
- endDateForMonitorTimeline: endDate,
1553
- });
1649
+ let incidentPublicNotes: Array<IncidentPublicNote> = [];
1554
1650
 
1555
- // check if status page has active incident.
1556
- let activeIncidents: Array<Incident> = [];
1557
- if (monitorsOnStatusPage.length > 0) {
1558
- let select: Select<Incident> = {
1559
- createdAt: true,
1560
- declaredAt: true,
1561
- updatedAt: true,
1562
- title: true,
1563
- description: true,
1564
- _id: true,
1565
- postmortemNote: true,
1566
- postmortemPostedAt: true,
1567
- showPostmortemOnStatusPage: true,
1568
- postmortemAttachments: {
1651
+ if (incidentsOnStatusPage.length > 0) {
1652
+ incidentPublicNotes = await IncidentPublicNoteService.findBy({
1653
+ query: {
1654
+ incidentId: QueryHelper.any(incidentsOnStatusPage),
1655
+ projectId: statusPage.projectId!,
1656
+ },
1657
+ select: {
1658
+ note: true,
1659
+ incidentId: true,
1660
+ postedAt: true,
1661
+ attachments: {
1569
1662
  _id: true,
1570
1663
  name: true,
1571
1664
  },
1572
- incidentSeverity: {
1573
- name: true,
1574
- color: true,
1575
- },
1576
- currentIncidentState: {
1665
+ },
1666
+ sort: {
1667
+ postedAt: SortOrder.Descending, // new note first
1668
+ },
1669
+ skip: 0,
1670
+ limit: LIMIT_PER_PROJECT,
1671
+ props: {
1672
+ isRoot: true,
1673
+ },
1674
+ });
1675
+ }
1676
+
1677
+ let incidentStateTimelines: Array<IncidentStateTimeline> = [];
1678
+
1679
+ if (incidentsOnStatusPage.length > 0) {
1680
+ incidentStateTimelines = await IncidentStateTimelineService.findBy({
1681
+ query: {
1682
+ incidentId: QueryHelper.any(incidentsOnStatusPage),
1683
+ projectId: statusPage.projectId!,
1684
+ },
1685
+ select: {
1686
+ _id: true,
1687
+ createdAt: true,
1688
+ startsAt: true,
1689
+ incidentId: true,
1690
+ incidentState: {
1577
1691
  _id: true,
1578
1692
  name: true,
1579
1693
  color: true,
1580
- order: true,
1694
+ isCreatedState: true,
1695
+ isResolvedState: true,
1696
+ isAcknowledgedState: true,
1581
1697
  },
1582
- monitors: {
1583
- _id: true,
1584
- },
1585
- };
1586
-
1587
- if (statusPage.showIncidentLabelsOnStatusPage) {
1588
- select = {
1589
- ...select,
1590
- labels: {
1591
- name: true,
1592
- color: true,
1593
- },
1594
- };
1595
- }
1596
-
1597
- const unresolvedIncidentStates: Array<IncidentState> =
1598
- await IncidentStateService.getUnresolvedIncidentStates(
1599
- statusPage.projectId!,
1600
- {
1601
- isRoot: true,
1602
- },
1603
- );
1604
-
1605
- const unresolvedIncidentStateIds: Array<ObjectID> =
1606
- unresolvedIncidentStates.map((state: IncidentState) => {
1607
- return state.id!;
1608
- });
1609
-
1610
- if (statusPage.showIncidentsOnStatusPage) {
1611
- activeIncidents = await IncidentService.findBy({
1612
- query: {
1613
- monitors: monitorsOnStatusPage as any,
1614
- currentIncidentStateId: QueryHelper.any(
1615
- unresolvedIncidentStateIds,
1616
- ),
1617
- isVisibleOnStatusPage: true,
1618
- projectId: statusPage.projectId!,
1619
- },
1620
- select: select,
1621
- sort: {
1622
- declaredAt: SortOrder.Descending,
1623
- createdAt: SortOrder.Descending,
1624
- },
1625
-
1626
- skip: 0,
1627
- limit: LIMIT_PER_PROJECT,
1628
- props: {
1629
- isRoot: true,
1630
- },
1631
- });
1632
- }
1633
- }
1698
+ },
1634
1699
 
1635
- const incidentsOnStatusPage: Array<ObjectID> = activeIncidents.map(
1636
- (incident: Incident) => {
1637
- return incident.id!;
1700
+ sort: {
1701
+ startsAt: SortOrder.Descending, // newer state changes first
1638
1702
  },
1639
- );
1703
+ skip: 0,
1704
+ limit: LIMIT_PER_PROJECT,
1705
+ props: {
1706
+ isRoot: true,
1707
+ },
1708
+ });
1709
+ }
1640
1710
 
1641
- let incidentPublicNotes: Array<IncidentPublicNote> = [];
1711
+ // Fetch active episodes (similar to incidents)
1712
+ let activeEpisodes: Array<IncidentEpisode> = [];
1713
+ let activeEpisodesJson: JSONArray = [];
1714
+ let episodePublicNotes: Array<IncidentEpisodePublicNote> = [];
1715
+ let episodeStateTimelines: Array<IncidentEpisodeStateTimeline> = [];
1642
1716
 
1643
- if (incidentsOnStatusPage.length > 0) {
1644
- incidentPublicNotes = await IncidentPublicNoteService.findBy({
1717
+ if (
1718
+ statusPage.showEpisodesOnStatusPage &&
1719
+ monitorsOnStatusPage.length > 0
1720
+ ) {
1721
+ // First, get incidents that have monitors on status page
1722
+ const incidentsForEpisodes: Array<Incident> =
1723
+ await IncidentService.findBy({
1645
1724
  query: {
1646
- incidentId: QueryHelper.any(incidentsOnStatusPage),
1725
+ monitors: monitorsOnStatusPage as any,
1726
+ isVisibleOnStatusPage: true,
1647
1727
  projectId: statusPage.projectId!,
1648
1728
  },
1649
1729
  select: {
1650
- note: true,
1651
- incidentId: true,
1652
- postedAt: true,
1653
- attachments: {
1654
- _id: true,
1655
- name: true,
1656
- },
1657
- },
1658
- sort: {
1659
- postedAt: SortOrder.Descending, // new note first
1730
+ _id: true,
1660
1731
  },
1661
1732
  skip: 0,
1662
1733
  limit: LIMIT_PER_PROJECT,
@@ -1664,33 +1735,23 @@ export default class StatusPageAPI extends BaseAPI<
1664
1735
  isRoot: true,
1665
1736
  },
1666
1737
  });
1667
- }
1668
1738
 
1669
- let incidentStateTimelines: Array<IncidentStateTimeline> = [];
1739
+ const incidentIdsForEpisodes: Array<ObjectID> =
1740
+ incidentsForEpisodes.map((incident: Incident) => {
1741
+ return incident.id!;
1742
+ });
1670
1743
 
1671
- if (incidentsOnStatusPage.length > 0) {
1672
- incidentStateTimelines = await IncidentStateTimelineService.findBy({
1744
+ // Get episode members for these incidents
1745
+ let episodeMembers: Array<IncidentEpisodeMember> = [];
1746
+ if (incidentIdsForEpisodes.length > 0) {
1747
+ episodeMembers = await IncidentEpisodeMemberService.findBy({
1673
1748
  query: {
1674
- incidentId: QueryHelper.any(incidentsOnStatusPage),
1749
+ incidentId: QueryHelper.any(incidentIdsForEpisodes),
1675
1750
  projectId: statusPage.projectId!,
1676
1751
  },
1677
1752
  select: {
1678
- _id: true,
1679
- createdAt: true,
1680
- startsAt: true,
1753
+ incidentEpisodeId: true,
1681
1754
  incidentId: true,
1682
- incidentState: {
1683
- _id: true,
1684
- name: true,
1685
- color: true,
1686
- isCreatedState: true,
1687
- isResolvedState: true,
1688
- isAcknowledgedState: true,
1689
- },
1690
- },
1691
-
1692
- sort: {
1693
- startsAt: SortOrder.Descending, // newer state changes first
1694
1755
  },
1695
1756
  skip: 0,
1696
1757
  limit: LIMIT_PER_PROJECT,
@@ -1700,534 +1761,418 @@ export default class StatusPageAPI extends BaseAPI<
1700
1761
  });
1701
1762
  }
1702
1763
 
1703
- // Fetch active episodes (similar to incidents)
1704
- let activeEpisodes: Array<IncidentEpisode> = [];
1705
- let activeEpisodesJson: JSONArray = [];
1706
- let episodePublicNotes: Array<IncidentEpisodePublicNote> = [];
1707
- let episodeStateTimelines: Array<IncidentEpisodeStateTimeline> = [];
1708
-
1709
- if (
1710
- statusPage.showEpisodesOnStatusPage &&
1711
- monitorsOnStatusPage.length > 0
1712
- ) {
1713
- // First, get incidents that have monitors on status page
1714
- const incidentsForEpisodes: Array<Incident> =
1715
- await IncidentService.findBy({
1716
- query: {
1717
- monitors: monitorsOnStatusPage as any,
1718
- isVisibleOnStatusPage: true,
1719
- projectId: statusPage.projectId!,
1720
- },
1721
- select: {
1722
- _id: true,
1723
- },
1724
- skip: 0,
1725
- limit: LIMIT_PER_PROJECT,
1726
- props: {
1727
- isRoot: true,
1728
- },
1729
- });
1730
-
1731
- const incidentIdsForEpisodes: Array<ObjectID> =
1732
- incidentsForEpisodes.map((incident: Incident) => {
1733
- return incident.id!;
1734
- });
1735
-
1736
- // Get episode members for these incidents
1737
- let episodeMembers: Array<IncidentEpisodeMember> = [];
1738
- if (incidentIdsForEpisodes.length > 0) {
1739
- episodeMembers = await IncidentEpisodeMemberService.findBy({
1740
- query: {
1741
- incidentId: QueryHelper.any(incidentIdsForEpisodes),
1742
- projectId: statusPage.projectId!,
1743
- },
1744
- select: {
1745
- incidentEpisodeId: true,
1746
- incidentId: true,
1747
- },
1748
- skip: 0,
1749
- limit: LIMIT_PER_PROJECT,
1750
- props: {
1751
- isRoot: true,
1752
- },
1753
- });
1764
+ // Get unique episode IDs
1765
+ const episodeIdsFromMembers: Set<string> = new Set();
1766
+ for (const member of episodeMembers) {
1767
+ if (member.incidentEpisodeId) {
1768
+ episodeIdsFromMembers.add(member.incidentEpisodeId.toString());
1754
1769
  }
1770
+ }
1755
1771
 
1756
- // Get unique episode IDs
1757
- const episodeIdsFromMembers: Set<string> = new Set();
1758
- for (const member of episodeMembers) {
1759
- if (member.incidentEpisodeId) {
1760
- episodeIdsFromMembers.add(member.incidentEpisodeId.toString());
1761
- }
1762
- }
1772
+ // Fetch active (unresolved) episodes
1773
+ if (episodeIdsFromMembers.size > 0) {
1774
+ const unresolvedIncidentStates: Array<IncidentState> =
1775
+ await IncidentStateService.getUnresolvedIncidentStates(
1776
+ statusPage.projectId!,
1777
+ { isRoot: true },
1778
+ );
1763
1779
 
1764
- // Fetch active (unresolved) episodes
1765
- if (episodeIdsFromMembers.size > 0) {
1766
- const unresolvedIncidentStates: Array<IncidentState> =
1767
- await IncidentStateService.getUnresolvedIncidentStates(
1768
- statusPage.projectId!,
1769
- { isRoot: true },
1770
- );
1771
-
1772
- const unresolvedIncidentStateIds: Array<ObjectID> =
1773
- unresolvedIncidentStates.map((state: IncidentState) => {
1774
- return state.id!;
1775
- });
1780
+ const unresolvedIncidentStateIds: Array<ObjectID> =
1781
+ unresolvedIncidentStates.map((state: IncidentState) => {
1782
+ return state.id!;
1783
+ });
1776
1784
 
1777
- let selectEpisodes: Select<IncidentEpisode> = {
1778
- createdAt: true,
1779
- declaredAt: true,
1780
- updatedAt: true,
1781
- title: true,
1782
- description: true,
1785
+ let selectEpisodes: Select<IncidentEpisode> = {
1786
+ createdAt: true,
1787
+ declaredAt: true,
1788
+ updatedAt: true,
1789
+ title: true,
1790
+ description: true,
1791
+ _id: true,
1792
+ episodeNumber: true,
1793
+ incidentSeverity: {
1794
+ name: true,
1795
+ color: true,
1796
+ },
1797
+ currentIncidentState: {
1798
+ name: true,
1799
+ color: true,
1783
1800
  _id: true,
1784
- episodeNumber: true,
1785
- incidentSeverity: {
1786
- name: true,
1787
- color: true,
1788
- },
1789
- currentIncidentState: {
1801
+ order: true,
1802
+ isCreatedState: true,
1803
+ isAcknowledgedState: true,
1804
+ isResolvedState: true,
1805
+ },
1806
+ incidentCount: true,
1807
+ };
1808
+
1809
+ if (statusPage.showEpisodeLabelsOnStatusPage) {
1810
+ selectEpisodes = {
1811
+ ...selectEpisodes,
1812
+ labels: {
1790
1813
  name: true,
1791
1814
  color: true,
1792
- _id: true,
1793
- order: true,
1794
- isCreatedState: true,
1795
- isAcknowledgedState: true,
1796
- isResolvedState: true,
1797
1815
  },
1798
- incidentCount: true,
1799
1816
  };
1817
+ }
1818
+
1819
+ activeEpisodes = await IncidentEpisodeService.findBy({
1820
+ query: {
1821
+ _id: QueryHelper.any(
1822
+ Array.from(episodeIdsFromMembers).map((id: string) => {
1823
+ return new ObjectID(id);
1824
+ }),
1825
+ ),
1826
+ currentIncidentStateId: QueryHelper.any(
1827
+ unresolvedIncidentStateIds,
1828
+ ),
1829
+ isVisibleOnStatusPage: true,
1830
+ projectId: statusPage.projectId!,
1831
+ },
1832
+ select: selectEpisodes,
1833
+ sort: {
1834
+ declaredAt: SortOrder.Descending,
1835
+ createdAt: SortOrder.Descending,
1836
+ },
1837
+ skip: 0,
1838
+ limit: LIMIT_PER_PROJECT,
1839
+ props: {
1840
+ isRoot: true,
1841
+ },
1842
+ });
1843
+
1844
+ // Build episode monitors map
1845
+ if (activeEpisodes.length > 0) {
1846
+ // Collect all incident IDs from episode members for active episodes
1847
+ const activeEpisodeIds: Set<string> = new Set(
1848
+ activeEpisodes.map((e: IncidentEpisode) => {
1849
+ return e.id!.toString();
1850
+ }),
1851
+ );
1852
+
1853
+ const memberIncidentIds: Array<ObjectID> = [];
1854
+ for (const member of episodeMembers) {
1855
+ if (
1856
+ member.incidentEpisodeId &&
1857
+ activeEpisodeIds.has(member.incidentEpisodeId.toString()) &&
1858
+ member.incidentId &&
1859
+ !memberIncidentIds.some((id: ObjectID) => {
1860
+ return id.toString() === member.incidentId!.toString();
1861
+ })
1862
+ ) {
1863
+ memberIncidentIds.push(member.incidentId);
1864
+ }
1865
+ }
1800
1866
 
1801
- if (statusPage.showEpisodeLabelsOnStatusPage) {
1802
- selectEpisodes = {
1803
- ...selectEpisodes,
1804
- labels: {
1805
- name: true,
1806
- color: true,
1867
+ // Fetch incidents with monitors
1868
+ let memberIncidents: Array<Incident> = [];
1869
+ if (memberIncidentIds.length > 0) {
1870
+ memberIncidents = await IncidentService.findBy({
1871
+ query: {
1872
+ _id: QueryHelper.any(memberIncidentIds),
1873
+ isVisibleOnStatusPage: true,
1874
+ projectId: statusPage.projectId!,
1807
1875
  },
1808
- };
1876
+ select: {
1877
+ _id: true,
1878
+ monitors: {
1879
+ _id: true,
1880
+ },
1881
+ },
1882
+ skip: 0,
1883
+ limit: LIMIT_PER_PROJECT,
1884
+ props: {
1885
+ isRoot: true,
1886
+ },
1887
+ });
1809
1888
  }
1810
1889
 
1811
- activeEpisodes = await IncidentEpisodeService.findBy({
1812
- query: {
1813
- _id: QueryHelper.any(
1814
- Array.from(episodeIdsFromMembers).map((id: string) => {
1815
- return new ObjectID(id);
1816
- }),
1817
- ),
1818
- currentIncidentStateId: QueryHelper.any(
1819
- unresolvedIncidentStateIds,
1820
- ),
1821
- isVisibleOnStatusPage: true,
1822
- projectId: statusPage.projectId!,
1823
- },
1824
- select: selectEpisodes,
1825
- sort: {
1826
- declaredAt: SortOrder.Descending,
1827
- createdAt: SortOrder.Descending,
1828
- },
1829
- skip: 0,
1830
- limit: LIMIT_PER_PROJECT,
1831
- props: {
1832
- isRoot: true,
1833
- },
1834
- });
1890
+ // Build incident -> monitors map
1891
+ const incidentMonitorsMap: Map<
1892
+ string,
1893
+ Array<ObjectID>
1894
+ > = new Map();
1895
+ for (const incident of memberIncidents) {
1896
+ const incidentIdStr: string = incident.id!.toString();
1897
+ const monitorIds: Array<ObjectID> = (incident.monitors || [])
1898
+ .map((m: Monitor) => {
1899
+ return new ObjectID(
1900
+ m._id?.toString() || m.id?.toString() || "",
1901
+ );
1902
+ })
1903
+ .filter((id: ObjectID) => {
1904
+ return id.toString() !== "";
1905
+ });
1906
+ incidentMonitorsMap.set(incidentIdStr, monitorIds);
1907
+ }
1835
1908
 
1836
- // Build episode monitors map
1837
- if (activeEpisodes.length > 0) {
1838
- // Collect all incident IDs from episode members for active episodes
1839
- const activeEpisodeIds: Set<string> = new Set(
1840
- activeEpisodes.map((e: IncidentEpisode) => {
1841
- return e.id!.toString();
1842
- }),
1843
- );
1844
-
1845
- const memberIncidentIds: Array<ObjectID> = [];
1846
- for (const member of episodeMembers) {
1847
- if (
1848
- member.incidentEpisodeId &&
1849
- activeEpisodeIds.has(member.incidentEpisodeId.toString()) &&
1850
- member.incidentId &&
1851
- !memberIncidentIds.some((id: ObjectID) => {
1852
- return id.toString() === member.incidentId!.toString();
1853
- })
1854
- ) {
1855
- memberIncidentIds.push(member.incidentId);
1909
+ // Build episode -> monitors map
1910
+ const episodeMonitorsMap: Map<
1911
+ string,
1912
+ Array<ObjectID>
1913
+ > = new Map();
1914
+ for (const member of episodeMembers) {
1915
+ if (
1916
+ member.incidentEpisodeId &&
1917
+ member.incidentId &&
1918
+ activeEpisodeIds.has(member.incidentEpisodeId.toString())
1919
+ ) {
1920
+ const episodeIdStr: string =
1921
+ member.incidentEpisodeId.toString();
1922
+ const incidentIdStr: string = member.incidentId.toString();
1923
+
1924
+ if (!episodeMonitorsMap.has(episodeIdStr)) {
1925
+ episodeMonitorsMap.set(episodeIdStr, []);
1926
+ }
1927
+
1928
+ const episodeMonitors: Array<ObjectID> =
1929
+ episodeMonitorsMap.get(episodeIdStr)!;
1930
+ const incidentMonitors: Array<ObjectID> =
1931
+ incidentMonitorsMap.get(incidentIdStr) || [];
1932
+
1933
+ for (const monitorId of incidentMonitors) {
1934
+ if (
1935
+ !episodeMonitors.some((m: ObjectID) => {
1936
+ return m.toString() === monitorId.toString();
1937
+ })
1938
+ ) {
1939
+ episodeMonitors.push(monitorId);
1940
+ }
1856
1941
  }
1857
1942
  }
1943
+ }
1944
+
1945
+ // Serialize episodes and add monitors
1946
+ activeEpisodesJson = BaseModel.toJSONArray(
1947
+ activeEpisodes,
1948
+ IncidentEpisode,
1949
+ );
1950
+ for (const episodeJson of activeEpisodesJson) {
1951
+ const episodeObj: JSONObject = episodeJson as JSONObject;
1952
+ const episodeId: string | undefined =
1953
+ episodeObj["_id"]?.toString();
1954
+ if (episodeId) {
1955
+ const monitorIds: Array<ObjectID> =
1956
+ episodeMonitorsMap.get(episodeId) || [];
1957
+ episodeObj["monitors"] = monitorIds.map((id: ObjectID) => {
1958
+ return { _id: id.toString() };
1959
+ });
1960
+ }
1961
+ }
1858
1962
 
1859
- // Fetch incidents with monitors
1860
- let memberIncidents: Array<Incident> = [];
1861
- if (memberIncidentIds.length > 0) {
1862
- memberIncidents = await IncidentService.findBy({
1963
+ // Get episode public notes
1964
+ const episodesOnStatusPage: Array<ObjectID> = activeEpisodes.map(
1965
+ (episode: IncidentEpisode) => {
1966
+ return episode.id!;
1967
+ },
1968
+ );
1969
+
1970
+ if (episodesOnStatusPage.length > 0) {
1971
+ episodePublicNotes =
1972
+ await IncidentEpisodePublicNoteService.findBy({
1863
1973
  query: {
1864
- _id: QueryHelper.any(memberIncidentIds),
1865
- isVisibleOnStatusPage: true,
1974
+ incidentEpisodeId: QueryHelper.any(episodesOnStatusPage),
1866
1975
  projectId: statusPage.projectId!,
1867
1976
  },
1868
1977
  select: {
1869
- _id: true,
1870
- monitors: {
1978
+ postedAt: true,
1979
+ note: true,
1980
+ incidentEpisodeId: true,
1981
+ attachments: {
1871
1982
  _id: true,
1983
+ name: true,
1872
1984
  },
1873
1985
  },
1986
+ sort: {
1987
+ postedAt: SortOrder.Descending,
1988
+ },
1874
1989
  skip: 0,
1875
1990
  limit: LIMIT_PER_PROJECT,
1876
1991
  props: {
1877
1992
  isRoot: true,
1878
1993
  },
1879
1994
  });
1880
- }
1881
-
1882
- // Build incident -> monitors map
1883
- const incidentMonitorsMap: Map<
1884
- string,
1885
- Array<ObjectID>
1886
- > = new Map();
1887
- for (const incident of memberIncidents) {
1888
- const incidentIdStr: string = incident.id!.toString();
1889
- const monitorIds: Array<ObjectID> = (incident.monitors || [])
1890
- .map((m: Monitor) => {
1891
- return new ObjectID(
1892
- m._id?.toString() || m.id?.toString() || "",
1893
- );
1894
- })
1895
- .filter((id: ObjectID) => {
1896
- return id.toString() !== "";
1897
- });
1898
- incidentMonitorsMap.set(incidentIdStr, monitorIds);
1899
- }
1900
1995
 
1901
- // Build episode -> monitors map
1902
- const episodeMonitorsMap: Map<
1903
- string,
1904
- Array<ObjectID>
1905
- > = new Map();
1906
- for (const member of episodeMembers) {
1907
- if (
1908
- member.incidentEpisodeId &&
1909
- member.incidentId &&
1910
- activeEpisodeIds.has(member.incidentEpisodeId.toString())
1911
- ) {
1912
- const episodeIdStr: string =
1913
- member.incidentEpisodeId.toString();
1914
- const incidentIdStr: string = member.incidentId.toString();
1915
-
1916
- if (!episodeMonitorsMap.has(episodeIdStr)) {
1917
- episodeMonitorsMap.set(episodeIdStr, []);
1918
- }
1996
+ // Get episode state timelines
1997
+ episodeStateTimelines =
1998
+ await IncidentEpisodeStateTimelineService.findBy({
1999
+ query: {
2000
+ incidentEpisodeId: QueryHelper.any(episodesOnStatusPage),
2001
+ projectId: statusPage.projectId!,
2002
+ },
2003
+ select: {
2004
+ _id: true,
2005
+ createdAt: true,
2006
+ startsAt: true,
2007
+ incidentEpisodeId: true,
2008
+ incidentState: {
2009
+ name: true,
2010
+ color: true,
2011
+ isCreatedState: true,
2012
+ isAcknowledgedState: true,
2013
+ isResolvedState: true,
2014
+ },
2015
+ },
2016
+ sort: {
2017
+ startsAt: SortOrder.Descending,
2018
+ },
2019
+ skip: 0,
2020
+ limit: LIMIT_PER_PROJECT,
2021
+ props: {
2022
+ isRoot: true,
2023
+ },
2024
+ });
2025
+ }
2026
+ }
2027
+ }
2028
+ }
1919
2029
 
1920
- const episodeMonitors: Array<ObjectID> =
1921
- episodeMonitorsMap.get(episodeIdStr)!;
1922
- const incidentMonitors: Array<ObjectID> =
1923
- incidentMonitorsMap.get(incidentIdStr) || [];
1924
-
1925
- for (const monitorId of incidentMonitors) {
1926
- if (
1927
- !episodeMonitors.some((m: ObjectID) => {
1928
- return m.toString() === monitorId.toString();
1929
- })
1930
- ) {
1931
- episodeMonitors.push(monitorId);
1932
- }
1933
- }
1934
- }
1935
- }
2030
+ // check if status page has active announcement.
1936
2031
 
1937
- // Serialize episodes and add monitors
1938
- activeEpisodesJson = BaseModel.toJSONArray(
1939
- activeEpisodes,
1940
- IncidentEpisode,
1941
- );
1942
- for (const episodeJson of activeEpisodesJson) {
1943
- const episodeObj: JSONObject = episodeJson as JSONObject;
1944
- const episodeId: string | undefined =
1945
- episodeObj["_id"]?.toString();
1946
- if (episodeId) {
1947
- const monitorIds: Array<ObjectID> =
1948
- episodeMonitorsMap.get(episodeId) || [];
1949
- episodeObj["monitors"] = monitorIds.map((id: ObjectID) => {
1950
- return { _id: id.toString() };
1951
- });
1952
- }
1953
- }
2032
+ const today: Date = OneUptimeDate.getCurrentDate();
1954
2033
 
1955
- // Get episode public notes
1956
- const episodesOnStatusPage: Array<ObjectID> =
1957
- activeEpisodes.map((episode: IncidentEpisode) => {
1958
- return episode.id!;
1959
- });
2034
+ let activeAnnouncements: Array<StatusPageAnnouncement> = [];
1960
2035
 
1961
- if (episodesOnStatusPage.length > 0) {
1962
- episodePublicNotes =
1963
- await IncidentEpisodePublicNoteService.findBy({
1964
- query: {
1965
- incidentEpisodeId:
1966
- QueryHelper.any(episodesOnStatusPage),
1967
- projectId: statusPage.projectId!,
1968
- },
1969
- select: {
1970
- postedAt: true,
1971
- note: true,
1972
- incidentEpisodeId: true,
1973
- attachments: {
1974
- _id: true,
1975
- name: true,
1976
- },
1977
- },
1978
- sort: {
1979
- postedAt: SortOrder.Descending,
1980
- },
1981
- skip: 0,
1982
- limit: LIMIT_PER_PROJECT,
1983
- props: {
1984
- isRoot: true,
1985
- },
1986
- });
2036
+ if (statusPage.showAnnouncementsOnStatusPage) {
2037
+ activeAnnouncements = await StatusPageAnnouncementService.findBy({
2038
+ query: {
2039
+ statusPages: statusPageId as any,
2040
+ showAnnouncementAt: QueryHelper.lessThan(today),
2041
+ endAnnouncementAt: QueryHelper.greaterThanOrNull(today),
2042
+ projectId: statusPage.projectId!,
2043
+ },
2044
+ select: {
2045
+ createdAt: true,
2046
+ title: true,
2047
+ description: true,
2048
+ _id: true,
2049
+ showAnnouncementAt: true,
2050
+ endAnnouncementAt: true,
2051
+ },
2052
+ skip: 0,
2053
+ limit: LIMIT_PER_PROJECT,
2054
+ props: {
2055
+ isRoot: true,
2056
+ },
2057
+ });
2058
+ }
1987
2059
 
1988
- // Get episode state timelines
1989
- episodeStateTimelines =
1990
- await IncidentEpisodeStateTimelineService.findBy({
1991
- query: {
1992
- incidentEpisodeId:
1993
- QueryHelper.any(episodesOnStatusPage),
1994
- projectId: statusPage.projectId!,
1995
- },
1996
- select: {
1997
- _id: true,
1998
- createdAt: true,
1999
- startsAt: true,
2000
- incidentEpisodeId: true,
2001
- incidentState: {
2002
- name: true,
2003
- color: true,
2004
- isCreatedState: true,
2005
- isAcknowledgedState: true,
2006
- isResolvedState: true,
2007
- },
2008
- },
2009
- sort: {
2010
- startsAt: SortOrder.Descending,
2011
- },
2012
- skip: 0,
2013
- limit: LIMIT_PER_PROJECT,
2014
- props: {
2015
- isRoot: true,
2016
- },
2017
- });
2018
- }
2019
- }
2020
- }
2021
- }
2060
+ // check if status page has active scheduled events.
2022
2061
 
2023
- // check if status page has active announcement.
2062
+ let scheduledEventsSelect: Select<ScheduledMaintenance> = {
2063
+ createdAt: true,
2064
+ title: true,
2065
+ description: true,
2066
+ _id: true,
2067
+ endsAt: true,
2068
+ startsAt: true,
2069
+ currentScheduledMaintenanceState: {
2070
+ name: true,
2071
+ color: true,
2072
+ isScheduledState: true,
2073
+ isResolvedState: true,
2074
+ isOngoingState: true,
2075
+ },
2076
+ monitors: {
2077
+ _id: true,
2078
+ },
2079
+ };
2024
2080
 
2025
- const today: Date = OneUptimeDate.getCurrentDate();
2081
+ if (statusPage.showScheduledEventLabelsOnStatusPage) {
2082
+ scheduledEventsSelect = {
2083
+ ...scheduledEventsSelect,
2084
+ labels: {
2085
+ name: true,
2086
+ color: true,
2087
+ },
2088
+ };
2089
+ }
2026
2090
 
2027
- let activeAnnouncements: Array<StatusPageAnnouncement> = [];
2091
+ let scheduledMaintenanceEvents: Array<ScheduledMaintenance> = [];
2028
2092
 
2029
- if (statusPage.showAnnouncementsOnStatusPage) {
2030
- activeAnnouncements = await StatusPageAnnouncementService.findBy({
2093
+ if (statusPage.showScheduledMaintenanceEventsOnStatusPage) {
2094
+ scheduledMaintenanceEvents = await ScheduledMaintenanceService.findBy(
2095
+ {
2031
2096
  query: {
2097
+ currentScheduledMaintenanceState: {
2098
+ isOngoingState: true,
2099
+ } as any,
2032
2100
  statusPages: statusPageId as any,
2033
- showAnnouncementAt: QueryHelper.lessThan(today),
2034
- endAnnouncementAt: QueryHelper.greaterThanOrNull(today),
2035
2101
  projectId: statusPage.projectId!,
2102
+ isVisibleOnStatusPage: true,
2036
2103
  },
2037
- select: {
2038
- createdAt: true,
2039
- title: true,
2040
- description: true,
2041
- _id: true,
2042
- showAnnouncementAt: true,
2043
- endAnnouncementAt: true,
2104
+ select: scheduledEventsSelect,
2105
+ sort: {
2106
+ startsAt: SortOrder.Ascending,
2044
2107
  },
2045
2108
  skip: 0,
2046
2109
  limit: LIMIT_PER_PROJECT,
2047
2110
  props: {
2048
2111
  isRoot: true,
2049
2112
  },
2050
- });
2051
- }
2052
-
2053
- // check if status page has active scheduled events.
2054
-
2055
- let scheduledEventsSelect: Select<ScheduledMaintenance> = {
2056
- createdAt: true,
2057
- title: true,
2058
- description: true,
2059
- _id: true,
2060
- endsAt: true,
2061
- startsAt: true,
2062
- currentScheduledMaintenanceState: {
2063
- name: true,
2064
- color: true,
2065
- isScheduledState: true,
2066
- isResolvedState: true,
2067
- isOngoingState: true,
2068
- },
2069
- monitors: {
2070
- _id: true,
2071
- },
2072
- };
2073
-
2074
- if (statusPage.showScheduledEventLabelsOnStatusPage) {
2075
- scheduledEventsSelect = {
2076
- ...scheduledEventsSelect,
2077
- labels: {
2078
- name: true,
2079
- color: true,
2080
- },
2081
- };
2082
- }
2083
-
2084
- let scheduledMaintenanceEvents: Array<ScheduledMaintenance> = [];
2085
-
2086
- if (statusPage.showScheduledMaintenanceEventsOnStatusPage) {
2087
- scheduledMaintenanceEvents =
2088
- await ScheduledMaintenanceService.findBy({
2089
- query: {
2090
- currentScheduledMaintenanceState: {
2091
- isOngoingState: true,
2092
- } as any,
2093
- statusPages: statusPageId as any,
2094
- projectId: statusPage.projectId!,
2095
- isVisibleOnStatusPage: true,
2096
- },
2097
- select: scheduledEventsSelect,
2098
- sort: {
2099
- startsAt: SortOrder.Ascending,
2100
- },
2101
- skip: 0,
2102
- limit: LIMIT_PER_PROJECT,
2103
- props: {
2104
- isRoot: true,
2105
- },
2106
- });
2107
- }
2108
-
2109
- let futureScheduledMaintenanceEvents: Array<ScheduledMaintenance> =
2110
- [];
2111
-
2112
- if (statusPage.showScheduledMaintenanceEventsOnStatusPage) {
2113
- futureScheduledMaintenanceEvents =
2114
- await ScheduledMaintenanceService.findBy({
2115
- query: {
2116
- currentScheduledMaintenanceState: {
2117
- isScheduledState: true,
2118
- } as any,
2119
- statusPages: statusPageId as any,
2120
- projectId: statusPage.projectId!,
2121
- isVisibleOnStatusPage: true,
2122
- },
2123
- select: scheduledEventsSelect,
2124
- sort: {
2125
- startsAt: SortOrder.Ascending,
2126
- },
2127
- skip: 0,
2128
- limit: LIMIT_PER_PROJECT,
2129
- props: {
2130
- isRoot: true,
2131
- },
2132
- });
2133
- }
2134
-
2135
- futureScheduledMaintenanceEvents.forEach(
2136
- (event: ScheduledMaintenance) => {
2137
- scheduledMaintenanceEvents.push(event);
2138
2113
  },
2139
2114
  );
2115
+ }
2140
2116
 
2141
- const scheduledMaintenanceEventsOnStatusPage: Array<ObjectID> =
2142
- scheduledMaintenanceEvents.map((event: ScheduledMaintenance) => {
2143
- return event.id!;
2144
- });
2145
-
2146
- let scheduledMaintenanceEventsPublicNotes: Array<ScheduledMaintenancePublicNote> =
2147
- [];
2117
+ let futureScheduledMaintenanceEvents: Array<ScheduledMaintenance> = [];
2148
2118
 
2149
- if (scheduledMaintenanceEventsOnStatusPage.length > 0) {
2150
- scheduledMaintenanceEventsPublicNotes =
2151
- await ScheduledMaintenancePublicNoteService.findBy({
2152
- query: {
2153
- scheduledMaintenanceId: QueryHelper.any(
2154
- scheduledMaintenanceEventsOnStatusPage,
2155
- ),
2156
- projectId: statusPage.projectId!,
2157
- },
2158
- select: {
2159
- postedAt: true,
2160
- note: true,
2161
- scheduledMaintenanceId: true,
2162
- attachments: {
2163
- _id: true,
2164
- name: true,
2165
- },
2166
- },
2167
- sort: {
2168
- postedAt: SortOrder.Ascending,
2169
- },
2170
- skip: 0,
2171
- limit: LIMIT_PER_PROJECT,
2172
- props: {
2173
- isRoot: true,
2174
- },
2175
- });
2176
- }
2119
+ if (statusPage.showScheduledMaintenanceEventsOnStatusPage) {
2120
+ futureScheduledMaintenanceEvents =
2121
+ await ScheduledMaintenanceService.findBy({
2122
+ query: {
2123
+ currentScheduledMaintenanceState: {
2124
+ isScheduledState: true,
2125
+ } as any,
2126
+ statusPages: statusPageId as any,
2127
+ projectId: statusPage.projectId!,
2128
+ isVisibleOnStatusPage: true,
2129
+ },
2130
+ select: scheduledEventsSelect,
2131
+ sort: {
2132
+ startsAt: SortOrder.Ascending,
2133
+ },
2134
+ skip: 0,
2135
+ limit: LIMIT_PER_PROJECT,
2136
+ props: {
2137
+ isRoot: true,
2138
+ },
2139
+ });
2140
+ }
2177
2141
 
2178
- let scheduledMaintenanceStateTimelines: Array<ScheduledMaintenanceStateTimeline> =
2179
- [];
2142
+ futureScheduledMaintenanceEvents.forEach(
2143
+ (event: ScheduledMaintenance) => {
2144
+ scheduledMaintenanceEvents.push(event);
2145
+ },
2146
+ );
2180
2147
 
2181
- if (scheduledMaintenanceEventsOnStatusPage.length > 0) {
2182
- scheduledMaintenanceStateTimelines =
2183
- await ScheduledMaintenanceStateTimelineService.findBy({
2184
- query: {
2185
- scheduledMaintenanceId: QueryHelper.any(
2186
- scheduledMaintenanceEventsOnStatusPage,
2187
- ),
2188
- projectId: statusPage.projectId!,
2189
- },
2190
- select: {
2191
- _id: true,
2192
- createdAt: true,
2193
- startsAt: true,
2194
- scheduledMaintenanceId: true,
2195
- scheduledMaintenanceState: {
2196
- _id: true,
2197
- color: true,
2198
- name: true,
2199
- isScheduledState: true,
2200
- isResolvedState: true,
2201
- isOngoingState: true,
2202
- },
2203
- },
2148
+ const scheduledMaintenanceEventsOnStatusPage: Array<ObjectID> =
2149
+ scheduledMaintenanceEvents.map((event: ScheduledMaintenance) => {
2150
+ return event.id!;
2151
+ });
2204
2152
 
2205
- sort: {
2206
- startsAt: SortOrder.Descending, // newer state changes first
2207
- },
2208
- skip: 0,
2209
- limit: LIMIT_PER_PROJECT,
2210
- props: {
2211
- isRoot: true,
2212
- },
2213
- });
2214
- }
2153
+ let scheduledMaintenanceEventsPublicNotes: Array<ScheduledMaintenancePublicNote> =
2154
+ [];
2215
2155
 
2216
- // get all status page bar chart rules
2217
- const statusPageHistoryChartBarColorRules: Array<StatusPageHistoryChartBarColorRule> =
2218
- await StatusPageHistoryChartBarColorRuleService.findBy({
2156
+ if (scheduledMaintenanceEventsOnStatusPage.length > 0) {
2157
+ scheduledMaintenanceEventsPublicNotes =
2158
+ await ScheduledMaintenancePublicNoteService.findBy({
2219
2159
  query: {
2220
- statusPageId: statusPageId,
2160
+ scheduledMaintenanceId: QueryHelper.any(
2161
+ scheduledMaintenanceEventsOnStatusPage,
2162
+ ),
2163
+ projectId: statusPage.projectId!,
2221
2164
  },
2222
2165
  select: {
2223
- _id: true,
2224
- barColor: true,
2225
- order: true,
2226
- statusPageId: true,
2227
- uptimePercentGreaterThanOrEqualTo: true,
2166
+ postedAt: true,
2167
+ note: true,
2168
+ scheduledMaintenanceId: true,
2169
+ attachments: {
2170
+ _id: true,
2171
+ name: true,
2172
+ },
2228
2173
  },
2229
2174
  sort: {
2230
- order: SortOrder.Ascending,
2175
+ postedAt: SortOrder.Ascending,
2231
2176
  },
2232
2177
  skip: 0,
2233
2178
  limit: LIMIT_PER_PROJECT,
@@ -2235,42 +2180,37 @@ export default class StatusPageAPI extends BaseAPI<
2235
2180
  isRoot: true,
2236
2181
  },
2237
2182
  });
2183
+ }
2238
2184
 
2239
- /*
2240
- * Fetch all incidents (active + resolved) in the timeline date range
2241
- * for the uptime bar tooltip and click-through
2242
- */
2243
- let timelineIncidents: Array<Incident> = [];
2244
- if (
2245
- monitorsOnStatusPage.length > 0 &&
2246
- statusPage.showIncidentsOnStatusPage
2247
- ) {
2248
- timelineIncidents = await IncidentService.findBy({
2185
+ let scheduledMaintenanceStateTimelines: Array<ScheduledMaintenanceStateTimeline> =
2186
+ [];
2187
+
2188
+ if (scheduledMaintenanceEventsOnStatusPage.length > 0) {
2189
+ scheduledMaintenanceStateTimelines =
2190
+ await ScheduledMaintenanceStateTimelineService.findBy({
2249
2191
  query: {
2250
- monitors: monitorsOnStatusPage as any,
2251
- declaredAt: QueryHelper.inBetween(startDate, endDate),
2252
- isVisibleOnStatusPage: true,
2192
+ scheduledMaintenanceId: QueryHelper.any(
2193
+ scheduledMaintenanceEventsOnStatusPage,
2194
+ ),
2253
2195
  projectId: statusPage.projectId!,
2254
2196
  },
2255
2197
  select: {
2256
2198
  _id: true,
2257
- title: true,
2258
- declaredAt: true,
2259
- incidentSeverity: {
2260
- name: true,
2261
- color: true,
2262
- },
2263
- currentIncidentState: {
2199
+ createdAt: true,
2200
+ startsAt: true,
2201
+ scheduledMaintenanceId: true,
2202
+ scheduledMaintenanceState: {
2264
2203
  _id: true,
2265
- name: true,
2266
2204
  color: true,
2267
- },
2268
- monitors: {
2269
- _id: true,
2205
+ name: true,
2206
+ isScheduledState: true,
2207
+ isResolvedState: true,
2208
+ isOngoingState: true,
2270
2209
  },
2271
2210
  },
2211
+
2272
2212
  sort: {
2273
- declaredAt: SortOrder.Descending,
2213
+ startsAt: SortOrder.Descending, // newer state changes first
2274
2214
  },
2275
2215
  skip: 0,
2276
2216
  limit: LIMIT_PER_PROJECT,
@@ -2278,101 +2218,188 @@ export default class StatusPageAPI extends BaseAPI<
2278
2218
  isRoot: true,
2279
2219
  },
2280
2220
  });
2281
- }
2221
+ }
2282
2222
 
2283
- const overallStatus: MonitorStatus | null =
2284
- StatusPageService.getOverallMonitorStatus({
2285
- statusPageResources,
2286
- monitorStatuses,
2287
- monitorGroupCurrentStatuses,
2288
- });
2223
+ // get all status page bar chart rules
2224
+ const statusPageHistoryChartBarColorRules: Array<StatusPageHistoryChartBarColorRule> =
2225
+ await StatusPageHistoryChartBarColorRuleService.findBy({
2226
+ query: {
2227
+ statusPageId: statusPageId,
2228
+ },
2229
+ select: {
2230
+ _id: true,
2231
+ barColor: true,
2232
+ order: true,
2233
+ statusPageId: true,
2234
+ uptimePercentGreaterThanOrEqualTo: true,
2235
+ },
2236
+ sort: {
2237
+ order: SortOrder.Ascending,
2238
+ },
2239
+ skip: 0,
2240
+ limit: LIMIT_PER_PROJECT,
2241
+ props: {
2242
+ isRoot: true,
2243
+ },
2244
+ });
2289
2245
 
2290
- const response: JSONObject = {
2291
- overallStatus: overallStatus
2292
- ? BaseModel.toJSON(overallStatus, MonitorStatus)
2293
- : null,
2246
+ /*
2247
+ * Fetch all incidents (active + resolved) in the timeline date range
2248
+ * for the uptime bar tooltip and click-through
2249
+ */
2250
+ let timelineIncidents: Array<Incident> = [];
2251
+ if (
2252
+ monitorsOnStatusPage.length > 0 &&
2253
+ statusPage.showIncidentsOnStatusPage
2254
+ ) {
2255
+ timelineIncidents = await IncidentService.findBy({
2256
+ query: {
2257
+ monitors: monitorsOnStatusPage as any,
2258
+ declaredAt: QueryHelper.inBetween(startDate, endDate),
2259
+ isVisibleOnStatusPage: true,
2260
+ projectId: statusPage.projectId!,
2261
+ },
2262
+ select: {
2263
+ _id: true,
2264
+ title: true,
2265
+ declaredAt: true,
2266
+ incidentSeverity: {
2267
+ name: true,
2268
+ color: true,
2269
+ },
2270
+ currentIncidentState: {
2271
+ _id: true,
2272
+ name: true,
2273
+ color: true,
2274
+ },
2275
+ monitors: {
2276
+ _id: true,
2277
+ },
2278
+ },
2279
+ sort: {
2280
+ declaredAt: SortOrder.Descending,
2281
+ },
2282
+ skip: 0,
2283
+ limit: LIMIT_PER_PROJECT,
2284
+ props: {
2285
+ isRoot: true,
2286
+ },
2287
+ });
2288
+ }
2294
2289
 
2295
- scheduledMaintenanceEventsPublicNotes: BaseModel.toJSONArray(
2296
- scheduledMaintenanceEventsPublicNotes,
2297
- ScheduledMaintenancePublicNote,
2298
- ),
2299
- statusPageHistoryChartBarColorRules: BaseModel.toJSONArray(
2300
- statusPageHistoryChartBarColorRules,
2301
- StatusPageHistoryChartBarColorRule,
2302
- ),
2303
- scheduledMaintenanceEvents: BaseModel.toJSONArray(
2304
- scheduledMaintenanceEvents,
2305
- ScheduledMaintenance,
2306
- ),
2307
- activeAnnouncements: BaseModel.toJSONArray(
2308
- activeAnnouncements,
2309
- StatusPageAnnouncement,
2310
- ),
2311
- incidentPublicNotes: BaseModel.toJSONArray(
2312
- incidentPublicNotes,
2313
- IncidentPublicNote,
2314
- ),
2290
+ const overallStatus: MonitorStatus | null =
2291
+ StatusPageService.getOverallMonitorStatus({
2292
+ statusPageResources,
2293
+ monitorStatuses,
2294
+ monitorGroupCurrentStatuses,
2295
+ });
2315
2296
 
2316
- activeIncidents: BaseModel.toJSONArray(activeIncidents, Incident),
2297
+ const response: JSONObject = {
2298
+ overallStatus: overallStatus
2299
+ ? BaseModel.toJSON(overallStatus, MonitorStatus)
2300
+ : null,
2317
2301
 
2318
- activeEpisodes: activeEpisodesJson,
2319
- episodePublicNotes: BaseModel.toJSONArray(
2320
- episodePublicNotes,
2321
- IncidentEpisodePublicNote,
2322
- ),
2323
- episodeStateTimelines: BaseModel.toJSONArray(
2324
- episodeStateTimelines,
2325
- IncidentEpisodeStateTimeline,
2326
- ),
2302
+ scheduledMaintenanceEventsPublicNotes: BaseModel.toJSONArray(
2303
+ scheduledMaintenanceEventsPublicNotes,
2304
+ ScheduledMaintenancePublicNote,
2305
+ ),
2306
+ statusPageHistoryChartBarColorRules: BaseModel.toJSONArray(
2307
+ statusPageHistoryChartBarColorRules,
2308
+ StatusPageHistoryChartBarColorRule,
2309
+ ),
2310
+ scheduledMaintenanceEvents: BaseModel.toJSONArray(
2311
+ scheduledMaintenanceEvents,
2312
+ ScheduledMaintenance,
2313
+ ),
2314
+ activeAnnouncements: BaseModel.toJSONArray(
2315
+ activeAnnouncements,
2316
+ StatusPageAnnouncement,
2317
+ ),
2318
+ incidentPublicNotes: BaseModel.toJSONArray(
2319
+ incidentPublicNotes,
2320
+ IncidentPublicNote,
2321
+ ),
2327
2322
 
2328
- monitorStatusTimelines: BaseModel.toJSONArray(
2329
- monitorStatusTimelines,
2330
- MonitorStatusTimeline,
2331
- ),
2332
- resourceGroups: BaseModel.toJSONArray(
2333
- statusPageGroups,
2334
- StatusPageGroup,
2335
- ),
2336
- monitorStatuses: BaseModel.toJSONArray(
2337
- monitorStatuses,
2338
- MonitorStatus,
2339
- ),
2340
- statusPageResources: BaseModel.toJSONArray(
2341
- statusPageResources,
2342
- StatusPageResource,
2343
- ),
2344
- incidentStateTimelines: BaseModel.toJSONArray(
2345
- incidentStateTimelines,
2346
- IncidentStateTimeline,
2347
- ),
2348
- statusPage: (() => {
2349
- const statusPageJson: JSONObject = BaseModel.toJSONObject(
2350
- statusPage,
2351
- StatusPage,
2352
- );
2353
- delete statusPageJson["projectId"];
2354
- return statusPageJson;
2355
- })(),
2356
- scheduledMaintenanceStateTimelines: BaseModel.toJSONArray(
2357
- scheduledMaintenanceStateTimelines,
2358
- ScheduledMaintenanceStateTimeline,
2359
- ),
2323
+ activeIncidents: BaseModel.toJSONArray(activeIncidents, Incident),
2360
2324
 
2361
- monitorGroupCurrentStatuses: JSONFunctions.serialize(
2362
- monitorGroupCurrentStatuses,
2363
- ),
2364
- monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
2365
- timelineIncidents: BaseModel.toJSONArray(
2366
- timelineIncidents,
2367
- Incident,
2368
- ),
2369
- };
2325
+ activeEpisodes: activeEpisodesJson,
2326
+ episodePublicNotes: BaseModel.toJSONArray(
2327
+ episodePublicNotes,
2328
+ IncidentEpisodePublicNote,
2329
+ ),
2330
+ episodeStateTimelines: BaseModel.toJSONArray(
2331
+ episodeStateTimelines,
2332
+ IncidentEpisodeStateTimeline,
2333
+ ),
2370
2334
 
2371
- return Response.sendJsonObjectResponse(req, res, response);
2372
- } catch (err) {
2373
- next(err);
2374
- }
2375
- },
2335
+ monitorStatusTimelines: BaseModel.toJSONArray(
2336
+ monitorStatusTimelines,
2337
+ MonitorStatusTimeline,
2338
+ ),
2339
+ resourceGroups: BaseModel.toJSONArray(
2340
+ statusPageGroups,
2341
+ StatusPageGroup,
2342
+ ),
2343
+ monitorStatuses: BaseModel.toJSONArray(
2344
+ monitorStatuses,
2345
+ MonitorStatus,
2346
+ ),
2347
+ statusPageResources: BaseModel.toJSONArray(
2348
+ statusPageResources,
2349
+ StatusPageResource,
2350
+ ),
2351
+ incidentStateTimelines: BaseModel.toJSONArray(
2352
+ incidentStateTimelines,
2353
+ IncidentStateTimeline,
2354
+ ),
2355
+ statusPage: (() => {
2356
+ const statusPageJson: JSONObject = BaseModel.toJSONObject(
2357
+ statusPage,
2358
+ StatusPage,
2359
+ );
2360
+ delete statusPageJson["projectId"];
2361
+ return statusPageJson;
2362
+ })(),
2363
+ scheduledMaintenanceStateTimelines: BaseModel.toJSONArray(
2364
+ scheduledMaintenanceStateTimelines,
2365
+ ScheduledMaintenanceStateTimeline,
2366
+ ),
2367
+
2368
+ monitorGroupCurrentStatuses: JSONFunctions.serialize(
2369
+ monitorGroupCurrentStatuses,
2370
+ ),
2371
+ monitorsInGroup: JSONFunctions.serialize(monitorsInGroup),
2372
+ timelineIncidents: BaseModel.toJSONArray(timelineIncidents, Incident),
2373
+ };
2374
+
2375
+ // These can serve private-page data on a GET; never let shared caches store them.
2376
+ Response.setNoCacheHeaders(res);
2377
+
2378
+ return Response.sendJsonObjectResponse(req, res, response);
2379
+ } catch (err) {
2380
+ next(err);
2381
+ }
2382
+ };
2383
+
2384
+ const overviewApiPath: string = `${new this.entityType()
2385
+ .getCrudApiPath()
2386
+ ?.toString()}/overview/:statusPageIdOrDomain`;
2387
+
2388
+ this.router.post(
2389
+ overviewApiPath,
2390
+ UserMiddleware.getUserMiddleware,
2391
+ overviewHandler,
2392
+ );
2393
+
2394
+ /*
2395
+ * GET variant of the overview endpoint. Same middleware and same handler
2396
+ * as the POST route above — private status pages are rejected for
2397
+ * unauthenticated callers exactly as they are on POST.
2398
+ */
2399
+ this.router.get(
2400
+ overviewApiPath,
2401
+ UserMiddleware.getUserMiddleware,
2402
+ overviewHandler,
2376
2403
  );
2377
2404
 
2378
2405
  this.router.put(
@@ -2444,78 +2471,154 @@ export default class StatusPageAPI extends BaseAPI<
2444
2471
  },
2445
2472
  );
2446
2473
 
2447
- this.router.post(
2448
- `${new this.entityType()
2449
- .getCrudApiPath()
2450
- ?.toString()}/incidents/:statusPageIdOrDomain`,
2451
- UserMiddleware.getUserMiddleware,
2452
- async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
2453
- try {
2454
- const objectId: ObjectID = await resolveStatusPageIdOrThrow(
2455
- req.params["statusPageIdOrDomain"] as string,
2456
- );
2474
+ /*
2475
+ * Shared handler for the incidents list endpoint. Registered for both
2476
+ * POST and GET (same auth path). It does not read req.body.
2477
+ */
2478
+ const incidentsListHandler: (
2479
+ req: ExpressRequest,
2480
+ res: ExpressResponse,
2481
+ next: NextFunction,
2482
+ ) => Promise<void> = async (
2483
+ req: ExpressRequest,
2484
+ res: ExpressResponse,
2485
+ next: NextFunction,
2486
+ ): Promise<void> => {
2487
+ try {
2488
+ const objectId: ObjectID = await resolveStatusPageIdOrThrow(
2489
+ req.params["statusPageIdOrDomain"] as string,
2490
+ );
2457
2491
 
2458
- const response: JSONObject = await this.getIncidents(
2459
- objectId,
2460
- null,
2461
- req,
2462
- );
2492
+ const response: JSONObject = await this.getIncidents(
2493
+ objectId,
2494
+ null,
2495
+ req,
2496
+ );
2463
2497
 
2464
- return Response.sendJsonObjectResponse(req, res, response);
2465
- } catch (err) {
2466
- next(err);
2467
- }
2468
- },
2469
- );
2498
+ // These can serve private-page data on a GET; never let shared caches store them.
2499
+ Response.setNoCacheHeaders(res);
2500
+
2501
+ return Response.sendJsonObjectResponse(req, res, response);
2502
+ } catch (err) {
2503
+ next(err);
2504
+ }
2505
+ };
2506
+
2507
+ const incidentsListApiPath: string = `${new this.entityType()
2508
+ .getCrudApiPath()
2509
+ ?.toString()}/incidents/:statusPageIdOrDomain`;
2470
2510
 
2471
2511
  this.router.post(
2472
- `${new this.entityType()
2473
- .getCrudApiPath()
2474
- ?.toString()}/scheduled-maintenance-events/:statusPageIdOrDomain`,
2512
+ incidentsListApiPath,
2475
2513
  UserMiddleware.getUserMiddleware,
2476
- async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
2477
- try {
2478
- const objectId: ObjectID = await resolveStatusPageIdOrThrow(
2479
- req.params["statusPageIdOrDomain"] as string,
2480
- );
2514
+ incidentsListHandler,
2515
+ );
2481
2516
 
2482
- const response: JSONObject = await this.getScheduledMaintenanceEvents(
2483
- objectId,
2484
- null,
2517
+ this.router.get(
2518
+ incidentsListApiPath,
2519
+ UserMiddleware.getUserMiddleware,
2520
+ incidentsListHandler,
2521
+ );
2485
2522
 
2486
- req,
2487
- );
2523
+ /*
2524
+ * Shared handler for the scheduled maintenance events list endpoint.
2525
+ * Registered for both POST and GET (same auth path). It does not read
2526
+ * req.body.
2527
+ */
2528
+ const scheduledMaintenanceEventsListHandler: (
2529
+ req: ExpressRequest,
2530
+ res: ExpressResponse,
2531
+ next: NextFunction,
2532
+ ) => Promise<void> = async (
2533
+ req: ExpressRequest,
2534
+ res: ExpressResponse,
2535
+ next: NextFunction,
2536
+ ): Promise<void> => {
2537
+ try {
2538
+ const objectId: ObjectID = await resolveStatusPageIdOrThrow(
2539
+ req.params["statusPageIdOrDomain"] as string,
2540
+ );
2488
2541
 
2489
- return Response.sendJsonObjectResponse(req, res, response);
2490
- } catch (err) {
2491
- next(err);
2492
- }
2493
- },
2494
- );
2542
+ const response: JSONObject = await this.getScheduledMaintenanceEvents(
2543
+ objectId,
2544
+ null,
2545
+
2546
+ req,
2547
+ );
2548
+
2549
+ // These can serve private-page data on a GET; never let shared caches store them.
2550
+ Response.setNoCacheHeaders(res);
2551
+
2552
+ return Response.sendJsonObjectResponse(req, res, response);
2553
+ } catch (err) {
2554
+ next(err);
2555
+ }
2556
+ };
2557
+
2558
+ const scheduledMaintenanceEventsListApiPath: string = `${new this.entityType()
2559
+ .getCrudApiPath()
2560
+ ?.toString()}/scheduled-maintenance-events/:statusPageIdOrDomain`;
2495
2561
 
2496
2562
  this.router.post(
2497
- `${new this.entityType()
2498
- .getCrudApiPath()
2499
- ?.toString()}/announcements/:statusPageIdOrDomain`,
2563
+ scheduledMaintenanceEventsListApiPath,
2500
2564
  UserMiddleware.getUserMiddleware,
2501
- async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
2502
- try {
2503
- const objectId: ObjectID = await resolveStatusPageIdOrThrow(
2504
- req.params["statusPageIdOrDomain"] as string,
2505
- );
2565
+ scheduledMaintenanceEventsListHandler,
2566
+ );
2506
2567
 
2507
- const response: JSONObject = await this.getAnnouncements(
2508
- objectId,
2509
- null,
2568
+ this.router.get(
2569
+ scheduledMaintenanceEventsListApiPath,
2570
+ UserMiddleware.getUserMiddleware,
2571
+ scheduledMaintenanceEventsListHandler,
2572
+ );
2510
2573
 
2511
- req,
2512
- );
2574
+ /*
2575
+ * Shared handler for the announcements list endpoint. Registered for
2576
+ * both POST and GET (same auth path). It does not read req.body.
2577
+ */
2578
+ const announcementsListHandler: (
2579
+ req: ExpressRequest,
2580
+ res: ExpressResponse,
2581
+ next: NextFunction,
2582
+ ) => Promise<void> = async (
2583
+ req: ExpressRequest,
2584
+ res: ExpressResponse,
2585
+ next: NextFunction,
2586
+ ): Promise<void> => {
2587
+ try {
2588
+ const objectId: ObjectID = await resolveStatusPageIdOrThrow(
2589
+ req.params["statusPageIdOrDomain"] as string,
2590
+ );
2513
2591
 
2514
- return Response.sendJsonObjectResponse(req, res, response);
2515
- } catch (err) {
2516
- next(err);
2517
- }
2518
- },
2592
+ const response: JSONObject = await this.getAnnouncements(
2593
+ objectId,
2594
+ null,
2595
+
2596
+ req,
2597
+ );
2598
+
2599
+ // These can serve private-page data on a GET; never let shared caches store them.
2600
+ Response.setNoCacheHeaders(res);
2601
+
2602
+ return Response.sendJsonObjectResponse(req, res, response);
2603
+ } catch (err) {
2604
+ next(err);
2605
+ }
2606
+ };
2607
+
2608
+ const announcementsListApiPath: string = `${new this.entityType()
2609
+ .getCrudApiPath()
2610
+ ?.toString()}/announcements/:statusPageIdOrDomain`;
2611
+
2612
+ this.router.post(
2613
+ announcementsListApiPath,
2614
+ UserMiddleware.getUserMiddleware,
2615
+ announcementsListHandler,
2616
+ );
2617
+
2618
+ this.router.get(
2619
+ announcementsListApiPath,
2620
+ UserMiddleware.getUserMiddleware,
2621
+ announcementsListHandler,
2519
2622
  );
2520
2623
 
2521
2624
  this.router.post(