@oneuptime/common 11.3.20 → 11.3.22

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 (212) hide show
  1. package/Models/AnalyticsModels/AnalyticsBaseModel/CommonModel.ts +2 -0
  2. package/Models/AnalyticsModels/ExceptionInstance.ts +1 -0
  3. package/Models/AnalyticsModels/Log.ts +1 -0
  4. package/Models/AnalyticsModels/Metric.ts +1 -0
  5. package/Models/AnalyticsModels/MonitorLog.ts +1 -0
  6. package/Models/AnalyticsModels/Span.ts +1 -0
  7. package/Models/DatabaseModels/Alert.ts +100 -0
  8. package/Models/DatabaseModels/AlertInternalNote.ts +2 -0
  9. package/Models/DatabaseModels/AlertReminderRule.ts +597 -0
  10. package/Models/DatabaseModels/AlertSeverity.ts +2 -0
  11. package/Models/DatabaseModels/AlertState.ts +2 -0
  12. package/Models/DatabaseModels/AlertStateTimeline.ts +2 -0
  13. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +173 -0
  14. package/Models/DatabaseModels/GlobalConfig.ts +19 -0
  15. package/Models/DatabaseModels/Incident.ts +100 -0
  16. package/Models/DatabaseModels/IncidentInternalNote.ts +2 -0
  17. package/Models/DatabaseModels/IncidentPublicNote.ts +2 -0
  18. package/Models/DatabaseModels/IncidentReminderRule.ts +597 -0
  19. package/Models/DatabaseModels/IncidentSeverity.ts +2 -0
  20. package/Models/DatabaseModels/IncidentStateTimeline.ts +2 -0
  21. package/Models/DatabaseModels/Index.ts +8 -0
  22. package/Models/DatabaseModels/Label.ts +2 -0
  23. package/Models/DatabaseModels/MonitorStatusTimeline.ts +2 -0
  24. package/Models/DatabaseModels/ScheduledMaintenance.ts +100 -0
  25. package/Models/DatabaseModels/ScheduledMaintenanceReminderRule.ts +585 -0
  26. package/Models/DatabaseModels/ScheduledMaintenanceState.ts +2 -0
  27. package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +2 -0
  28. package/Models/DatabaseModels/StatusPageAnnouncement.ts +2 -0
  29. package/Server/API/DashboardAPI.ts +221 -0
  30. package/Server/API/EnterpriseLicenseAPI.ts +279 -12
  31. package/Server/API/GlobalConfigAPI.ts +81 -16
  32. package/Server/API/StatusPageAPI.ts +894 -791
  33. package/Server/EnvironmentConfig.ts +9 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.ts +47 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.ts +211 -0
  36. package/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.ts +113 -0
  37. package/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.ts +20 -0
  38. package/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.ts +69 -0
  39. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
  40. package/Server/Services/AlertReminderRuleService.ts +253 -0
  41. package/Server/Services/AlertService.ts +106 -0
  42. package/Server/Services/EnterpriseLicenseInstanceService.ts +10 -0
  43. package/Server/Services/IncidentReminderRuleService.ts +256 -0
  44. package/Server/Services/IncidentService.ts +143 -0
  45. package/Server/Services/Index.ts +8 -0
  46. package/Server/Services/IoTDeviceService.ts +99 -4
  47. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +236 -0
  48. package/Server/Services/ScheduledMaintenanceService.ts +127 -0
  49. package/Server/Services/StatusPageService.ts +104 -45
  50. package/Server/Services/TelemetryExceptionService.ts +123 -0
  51. package/Server/Services/UserNotificationSettingService.ts +24 -0
  52. package/Server/Utils/AnalyticsDatabase/Statement.ts +7 -3
  53. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +35 -0
  54. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +6 -4
  55. package/Server/Utils/Monitor/MonitorCriteriaDataExtractor.ts +7 -6
  56. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +6 -4
  57. package/Server/Utils/Telemetry/IoTSnapshotScan.ts +45 -7
  58. package/Server/Utils/WhatsAppTemplateUtil.ts +10 -0
  59. package/ServiceRoute.ts +4 -0
  60. package/Tests/Server/Services/IoTDeviceService.test.ts +280 -0
  61. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +49 -0
  62. package/Tests/Server/Utils/Monitor/Criteria/TelemetryStepMetricAlias.test.ts +390 -0
  63. package/Tests/Server/Utils/Telemetry/IoTSnapshotScan.test.ts +202 -0
  64. package/Tests/Utils/EnterpriseLicenseUsage.test.ts +165 -0
  65. package/Types/Email/EmailTemplateType.ts +3 -0
  66. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +18 -0
  67. package/Types/Monitor/MonitorStep.ts +32 -0
  68. package/Types/NotificationSetting/NotificationSettingEventType.ts +3 -0
  69. package/Types/Permission.ts +144 -0
  70. package/Types/Reminder/ReminderStopState.ts +6 -0
  71. package/Types/Reminder/ScheduledMaintenanceReminderStopState.ts +6 -0
  72. package/Types/WhatsApp/WhatsAppTemplates.ts +14 -0
  73. package/UI/Components/EditionLabel/EditionLabel.tsx +172 -0
  74. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +125 -0
  75. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/CommonModel.js.map +1 -1
  76. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +1 -0
  77. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  78. package/build/dist/Models/AnalyticsModels/Log.js +1 -0
  79. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  80. package/build/dist/Models/AnalyticsModels/Metric.js +1 -0
  81. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  82. package/build/dist/Models/AnalyticsModels/MonitorLog.js +1 -0
  83. package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
  84. package/build/dist/Models/AnalyticsModels/Span.js +1 -0
  85. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  86. package/build/dist/Models/DatabaseModels/Alert.js +103 -0
  87. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  88. package/build/dist/Models/DatabaseModels/AlertInternalNote.js +2 -0
  89. package/build/dist/Models/DatabaseModels/AlertInternalNote.js.map +1 -1
  90. package/build/dist/Models/DatabaseModels/AlertReminderRule.js +605 -0
  91. package/build/dist/Models/DatabaseModels/AlertReminderRule.js.map +1 -0
  92. package/build/dist/Models/DatabaseModels/AlertSeverity.js +2 -0
  93. package/build/dist/Models/DatabaseModels/AlertSeverity.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/AlertState.js +2 -0
  95. package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/AlertStateTimeline.js +2 -0
  97. package/build/dist/Models/DatabaseModels/AlertStateTimeline.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +197 -0
  99. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -0
  100. package/build/dist/Models/DatabaseModels/GlobalConfig.js +18 -0
  101. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  102. package/build/dist/Models/DatabaseModels/Incident.js +103 -0
  103. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  104. package/build/dist/Models/DatabaseModels/IncidentInternalNote.js +2 -0
  105. package/build/dist/Models/DatabaseModels/IncidentInternalNote.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +2 -0
  107. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/IncidentReminderRule.js +605 -0
  109. package/build/dist/Models/DatabaseModels/IncidentReminderRule.js.map +1 -0
  110. package/build/dist/Models/DatabaseModels/IncidentSeverity.js +2 -0
  111. package/build/dist/Models/DatabaseModels/IncidentSeverity.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +2 -0
  113. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/Index.js +8 -0
  115. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  116. package/build/dist/Models/DatabaseModels/Label.js +2 -0
  117. package/build/dist/Models/DatabaseModels/Label.js.map +1 -1
  118. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +2 -0
  119. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  120. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +103 -0
  121. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  122. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js +596 -0
  123. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceReminderRule.js.map +1 -0
  124. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceState.js +2 -0
  125. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceState.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +2 -0
  127. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +2 -0
  129. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  130. package/build/dist/Server/API/DashboardAPI.js +166 -16
  131. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  132. package/build/dist/Server/API/EnterpriseLicenseAPI.js +203 -13
  133. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  134. package/build/dist/Server/API/GlobalConfigAPI.js +62 -15
  135. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  136. package/build/dist/Server/API/StatusPageAPI.js +74 -31
  137. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  138. package/build/dist/Server/EnvironmentConfig.js +6 -0
  139. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  140. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.js +22 -0
  141. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783226868418-AddEnterpriseLicenseInstance.js.map +1 -0
  142. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.js +78 -0
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783234451106-AddIncidentAlertReminderRules.js.map +1 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.js +44 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783320000000-AddScheduledMaintenanceReminderRules.js.map +1 -0
  146. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.js +12 -0
  147. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783330000000-AddRemindWhileScheduledToScheduledMaintenanceReminderRule.js.map +1 -0
  148. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.js +30 -0
  149. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783340000000-AddLabelsToIncidentAndAlertReminderRules.js.map +1 -0
  150. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  151. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  152. package/build/dist/Server/Services/AlertReminderRuleService.js +228 -0
  153. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -0
  154. package/build/dist/Server/Services/AlertService.js +87 -0
  155. package/build/dist/Server/Services/AlertService.js.map +1 -1
  156. package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js +9 -0
  157. package/build/dist/Server/Services/EnterpriseLicenseInstanceService.js.map +1 -0
  158. package/build/dist/Server/Services/IncidentReminderRuleService.js +231 -0
  159. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -0
  160. package/build/dist/Server/Services/IncidentService.js +116 -0
  161. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  162. package/build/dist/Server/Services/Index.js +8 -0
  163. package/build/dist/Server/Services/Index.js.map +1 -1
  164. package/build/dist/Server/Services/IoTDeviceService.js +66 -4
  165. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  166. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +214 -0
  167. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -0
  168. package/build/dist/Server/Services/ScheduledMaintenanceService.js +102 -0
  169. package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
  170. package/build/dist/Server/Services/StatusPageService.js +91 -43
  171. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  172. package/build/dist/Server/Services/TelemetryExceptionService.js +101 -0
  173. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  174. package/build/dist/Server/Services/UserNotificationSettingService.js +6 -0
  175. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  176. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +4 -2
  177. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  178. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -0
  179. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  180. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +4 -5
  181. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  182. package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js +6 -5
  183. package/build/dist/Server/Utils/Monitor/MonitorCriteriaDataExtractor.js.map +1 -1
  184. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +9 -9
  185. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  186. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +33 -2
  187. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
  188. package/build/dist/Server/Utils/WhatsAppTemplateUtil.js +6 -0
  189. package/build/dist/Server/Utils/WhatsAppTemplateUtil.js.map +1 -1
  190. package/build/dist/ServiceRoute.js +1 -0
  191. package/build/dist/ServiceRoute.js.map +1 -1
  192. package/build/dist/Types/Email/EmailTemplateType.js +3 -0
  193. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  194. package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.js +2 -0
  195. package/build/dist/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.js.map +1 -0
  196. package/build/dist/Types/Monitor/MonitorStep.js +25 -0
  197. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  198. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
  199. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
  200. package/build/dist/Types/Permission.js +126 -0
  201. package/build/dist/Types/Permission.js.map +1 -1
  202. package/build/dist/Types/Reminder/ReminderStopState.js +7 -0
  203. package/build/dist/Types/Reminder/ReminderStopState.js.map +1 -0
  204. package/build/dist/Types/Reminder/ScheduledMaintenanceReminderStopState.js +7 -0
  205. package/build/dist/Types/Reminder/ScheduledMaintenanceReminderStopState.js.map +1 -0
  206. package/build/dist/Types/WhatsApp/WhatsAppTemplates.js +9 -0
  207. package/build/dist/Types/WhatsApp/WhatsAppTemplates.js.map +1 -1
  208. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +92 -0
  209. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  210. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +83 -0
  211. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -0
  212. package/package.json +1 -1
@@ -11,6 +11,7 @@ import CanAccessIfCanReadOn from "../../Types/Database/CanAccessIfCanReadOn";
11
11
  import ColumnType from "../../Types/Database/ColumnType";
12
12
  import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
13
13
  import EnableDocumentation from "../../Types/Database/EnableDocumentation";
14
+ import EnableMCP from "../../Types/Database/EnableMCP";
14
15
  import EnableWorkflow from "../../Types/Database/EnableWorkflow";
15
16
  import TableColumn from "../../Types/Database/TableColumn";
16
17
  import TableColumnType from "../../Types/Database/TableColumnType";
@@ -23,6 +24,7 @@ import StatusPageSubscriberNotificationStatus from "../../Types/StatusPage/Statu
23
24
  import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm";
24
25
 
25
26
  @EnableDocumentation()
27
+ @EnableMCP()
26
28
  @CanAccessIfCanReadOn("scheduledMaintenance")
27
29
  @TenantColumn("projectId")
28
30
  @TableAccessControl({
@@ -14,6 +14,7 @@ import ColumnLength from "../../Types/Database/ColumnLength";
14
14
  import ColumnType from "../../Types/Database/ColumnType";
15
15
  import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
16
16
  import EnableDocumentation from "../../Types/Database/EnableDocumentation";
17
+ import EnableMCP from "../../Types/Database/EnableMCP";
17
18
  import EnableWorkflow from "../../Types/Database/EnableWorkflow";
18
19
  import SlugifyColumn from "../../Types/Database/SlugifyColumn";
19
20
  import TableColumn from "../../Types/Database/TableColumn";
@@ -35,6 +36,7 @@ import {
35
36
  } from "typeorm";
36
37
 
37
38
  @EnableDocumentation()
39
+ @EnableMCP()
38
40
  @TenantColumn("projectId")
39
41
  @CanAccessIfCanReadOn("statusPages")
40
42
  @TableBillingAccessControl({
@@ -375,6 +375,58 @@ const PUBLIC_DASHBOARD_RESOURCES: Record<
375
375
  },
376
376
  };
377
377
 
378
+ type ResolveDashboardIdOrThrowFunction = (
379
+ dashboardIdOrDomain: string,
380
+ ) => Promise<ObjectID>;
381
+
382
+ /*
383
+ * Resolve the :dashboardIdOrDomain route param to a dashboard ID. Accepts
384
+ * either a dashboard ID (UUID) or a verified custom dashboard domain —
385
+ * same contract as the status page overview endpoint.
386
+ */
387
+ const resolveDashboardIdOrThrow: ResolveDashboardIdOrThrowFunction = async (
388
+ dashboardIdOrDomain: string,
389
+ ): Promise<ObjectID> => {
390
+ if (!dashboardIdOrDomain) {
391
+ throw new NotFoundException("Dashboard not found");
392
+ }
393
+
394
+ if (dashboardIdOrDomain.includes(".")) {
395
+ const dashboardDomain: DashboardDomain | null =
396
+ await DashboardDomainService.findOneBy({
397
+ query: {
398
+ fullDomain: dashboardIdOrDomain,
399
+ domain: {
400
+ isVerified: true,
401
+ } as any,
402
+ },
403
+ select: {
404
+ dashboardId: true,
405
+ },
406
+ props: {
407
+ isRoot: true,
408
+ },
409
+ });
410
+
411
+ if (!dashboardDomain || !dashboardDomain.dashboardId) {
412
+ throw new NotFoundException("Dashboard not found");
413
+ }
414
+
415
+ return dashboardDomain.dashboardId;
416
+ }
417
+
418
+ try {
419
+ ObjectID.validateUUID(dashboardIdOrDomain);
420
+ return new ObjectID(dashboardIdOrDomain);
421
+ } catch (err) {
422
+ logger.error(
423
+ `Error converting dashboardIdOrDomain to ObjectID: ${dashboardIdOrDomain}`,
424
+ );
425
+ logger.error(err);
426
+ throw new NotFoundException("Dashboard not found");
427
+ }
428
+ };
429
+
378
430
  export default class DashboardAPI extends BaseAPI<
379
431
  Dashboard,
380
432
  DashboardServiceType
@@ -476,6 +528,111 @@ export default class DashboardAPI extends BaseAPI<
476
528
  },
477
529
  );
478
530
 
531
+ /*
532
+ * Shared handler for the public overview endpoint. Registered for both
533
+ * POST and GET (machine-readable access for crawlers / AI agents that
534
+ * only issue plain GET requests — discovered via llms.txt on public
535
+ * dashboard URLs) so the authentication and authorization path can
536
+ * never drift between the two methods. It does not read req.body.
537
+ */
538
+ const overviewHandler: (
539
+ req: ExpressRequest,
540
+ res: ExpressResponse,
541
+ next: NextFunction,
542
+ ) => Promise<void> = async (
543
+ req: ExpressRequest,
544
+ res: ExpressResponse,
545
+ next: NextFunction,
546
+ ): Promise<void> => {
547
+ try {
548
+ const dashboardId: ObjectID = await resolveDashboardIdOrThrow(
549
+ req.params["dashboardIdOrDomain"] as string,
550
+ );
551
+
552
+ // Check read access (handles public check, IP whitelist, master password)
553
+ const accessResult: {
554
+ hasReadAccess: boolean;
555
+ error?: NotAuthenticatedException | ForbiddenException;
556
+ } = await DashboardService.hasReadAccess({
557
+ dashboardId,
558
+ req,
559
+ });
560
+
561
+ if (!accessResult.hasReadAccess) {
562
+ throw (
563
+ accessResult.error ||
564
+ new BadDataException("Access denied to this dashboard.")
565
+ );
566
+ }
567
+
568
+ const dashboard: Dashboard | null = await DashboardService.findOneById({
569
+ id: dashboardId,
570
+ select: {
571
+ _id: true,
572
+ name: true,
573
+ description: true,
574
+ pageTitle: true,
575
+ pageDescription: true,
576
+ dashboardViewConfig: true,
577
+ },
578
+ props: {
579
+ isRoot: true,
580
+ },
581
+ });
582
+
583
+ if (!dashboard) {
584
+ throw new NotFoundException("Dashboard not found");
585
+ }
586
+
587
+ /*
588
+ * Everything below is derived from columns the public /metadata and
589
+ * /view-config endpoints already serve to anonymous viewers — the
590
+ * overview only reshapes them into a single GET-able document.
591
+ */
592
+ return Response.sendJsonObjectResponse(req, res, {
593
+ _id: dashboard._id?.toString() || "",
594
+ name: dashboard.name || "Dashboard",
595
+ description: dashboard.description || "",
596
+ pageTitle: dashboard.pageTitle || "",
597
+ pageDescription: dashboard.pageDescription || "",
598
+ components: DashboardAPI.buildPublicComponentSummaries(
599
+ dashboard.dashboardViewConfig,
600
+ ),
601
+ metricsUsed: Array.from(
602
+ DashboardAPI.collectDashboardMetricNames(
603
+ dashboard.dashboardViewConfig,
604
+ ),
605
+ ).sort(),
606
+ dashboardViewConfig: dashboard.dashboardViewConfig
607
+ ? JSONFunctions.serialize(dashboard.dashboardViewConfig as any)
608
+ : null,
609
+ });
610
+ } catch (err) {
611
+ next(err);
612
+ }
613
+ };
614
+
615
+ const overviewApiPath: string = `${new this.entityType()
616
+ .getCrudApiPath()
617
+ ?.toString()}/overview/:dashboardIdOrDomain`;
618
+
619
+ this.router.post(
620
+ overviewApiPath,
621
+ UserMiddleware.getUserMiddleware,
622
+ overviewHandler,
623
+ );
624
+
625
+ /*
626
+ * GET variant of the overview endpoint. Same middleware and same handler
627
+ * as the POST route above — non-public dashboards are rejected for
628
+ * unauthenticated callers exactly as they are on POST.
629
+ */
630
+ this.router.get(
631
+ overviewApiPath,
632
+ UserMiddleware.getUserMiddleware,
633
+ overviewHandler,
634
+ );
635
+
479
636
  // Domain resolution endpoint
480
637
  this.router.post(
481
638
  `${new this.entityType().getCrudApiPath()?.toString()}/domain`,
@@ -1079,6 +1236,70 @@ export default class DashboardAPI extends BaseAPI<
1079
1236
  );
1080
1237
  }
1081
1238
 
1239
+ /*
1240
+ * Build a compact machine-readable summary of the widgets on a dashboard
1241
+ * for the public overview endpoint. Derived entirely from the stored
1242
+ * dashboardViewConfig (already served to public viewers by /view-config) —
1243
+ * this only lifts out the human-meaningful fields (widget type, title,
1244
+ * text, metric names). The config comes straight from a JSON column, so
1245
+ * every field is read defensively.
1246
+ */
1247
+ private static buildPublicComponentSummaries(
1248
+ dashboardViewConfig: unknown,
1249
+ ): Array<JSONObject> {
1250
+ const summaries: Array<JSONObject> = [];
1251
+
1252
+ const components: unknown =
1253
+ dashboardViewConfig && typeof dashboardViewConfig === "object"
1254
+ ? (dashboardViewConfig as Record<string, unknown>)["components"]
1255
+ : undefined;
1256
+
1257
+ if (!Array.isArray(components)) {
1258
+ return summaries;
1259
+ }
1260
+
1261
+ for (const component of components) {
1262
+ if (!component || typeof component !== "object") {
1263
+ continue;
1264
+ }
1265
+
1266
+ const componentObject: Record<string, unknown> = component as Record<
1267
+ string,
1268
+ unknown
1269
+ >;
1270
+
1271
+ const argumentsObject: Record<string, unknown> =
1272
+ componentObject["arguments"] &&
1273
+ typeof componentObject["arguments"] === "object"
1274
+ ? (componentObject["arguments"] as Record<string, unknown>)
1275
+ : {};
1276
+
1277
+ const getStringArgument: (key: string) => string | null = (
1278
+ key: string,
1279
+ ): string | null => {
1280
+ const value: unknown = argumentsObject[key];
1281
+ return typeof value === "string" && value.trim().length > 0
1282
+ ? value
1283
+ : null;
1284
+ };
1285
+
1286
+ summaries.push({
1287
+ componentType:
1288
+ typeof componentObject["componentType"] === "string"
1289
+ ? componentObject["componentType"]
1290
+ : "",
1291
+ title: getStringArgument("chartTitle") || getStringArgument("title"),
1292
+ description: getStringArgument("chartDescription"),
1293
+ text: getStringArgument("text"),
1294
+ metricNames: Array.from(
1295
+ DashboardAPI.collectDashboardMetricNames(component),
1296
+ ).sort(),
1297
+ });
1298
+ }
1299
+
1300
+ return summaries;
1301
+ }
1302
+
1082
1303
  /*
1083
1304
  * Walk a stored dashboard view config and collect every metric name it
1084
1305
  * references (chart/value/gauge/table widgets all carry their metric in a
@@ -1,9 +1,18 @@
1
1
  import EnterpriseLicense from "../../Models/DatabaseModels/EnterpriseLicense";
2
+ import EnterpriseLicenseInstance from "../../Models/DatabaseModels/EnterpriseLicenseInstance";
2
3
  import BadDataException from "../../Types/Exception/BadDataException";
3
4
  import { JSONObject } from "../../Types/JSON";
5
+ import PartialEntity from "../../Types/Database/PartialEntity";
6
+ import EnterpriseLicenseInstanceSummary from "../../Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary";
7
+ import EnterpriseLicenseUsageUtil from "../../Utils/EnterpriseLicense/EnterpriseLicenseUsage";
8
+ import LIMIT_MAX from "../../Types/Database/LimitMax";
9
+ import ObjectID from "../../Types/ObjectID";
10
+ import PositiveNumber from "../../Types/PositiveNumber";
11
+ import SortOrder from "../../Types/BaseDatabase/SortOrder";
4
12
  import EnterpriseLicenseService, {
5
13
  Service as EnterpriseLicenseServiceType,
6
14
  } from "../Services/EnterpriseLicenseService";
15
+ import EnterpriseLicenseInstanceService from "../Services/EnterpriseLicenseInstanceService";
7
16
  import UserMiddleware from "../Middleware/UserAuthorization";
8
17
  import JSONWebToken from "../Utils/JsonWebToken";
9
18
  import OneUptimeDate from "../../Types/Date";
@@ -16,6 +25,23 @@ import {
16
25
  import BaseAPI from "./BaseAPI";
17
26
  // import { Host } from "../EnvironmentConfig";
18
27
 
28
+ export interface LicenseInstanceUpsert {
29
+ licenseId: ObjectID;
30
+ instanceId: string;
31
+ host: string | undefined;
32
+ // Usage fields are only set on report-user-count, not on validate.
33
+ userCount?: number | undefined;
34
+ userEmailHashes?: Array<string> | undefined;
35
+ lastReportedAt?: Date | undefined;
36
+ }
37
+
38
+ /*
39
+ * Bounds how many instance rows one license key can register. Real customers
40
+ * run a handful of instances; this stops a leaked key from being used to
41
+ * fill the table with junk rows.
42
+ */
43
+ const MAX_INSTANCES_PER_LICENSE: number = 100;
44
+
19
45
  export default class EnterpriseLicenseAPI extends BaseAPI<
20
46
  EnterpriseLicense,
21
47
  EnterpriseLicenseServiceType
@@ -50,6 +76,7 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
50
76
  licenseKey: licenseKey,
51
77
  },
52
78
  select: {
79
+ _id: true,
53
80
  companyName: true,
54
81
  expiresAt: true,
55
82
  licenseKey: true,
@@ -80,6 +107,27 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
80
107
  throw new BadDataException("License key has expired");
81
108
  }
82
109
 
110
+ /*
111
+ * The validating instance sends its instanceId and host so it
112
+ * shows up in the instance list right away (usage is reported
113
+ * later by the daily job on the instance).
114
+ */
115
+ const instanceId: string = this.parseShortText(
116
+ req.body["instanceId"],
117
+ );
118
+ const instanceHost: string = this.parseShortText(req.body["host"]);
119
+
120
+ if (instanceId) {
121
+ await this.upsertLicenseInstance({
122
+ licenseId: license.id!,
123
+ instanceId: instanceId,
124
+ host: instanceHost || undefined,
125
+ });
126
+ }
127
+
128
+ const instances: Array<EnterpriseLicenseInstance> =
129
+ await this.findLicenseInstances(license.id!);
130
+
83
131
  const payload: JSONObject = {
84
132
  companyName: license.companyName || "",
85
133
  expiresAt: license.expiresAt.toISOString(),
@@ -105,6 +153,7 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
105
153
  userCountUpdatedAt: license.userCountUpdatedAt
106
154
  ? license.userCountUpdatedAt.toISOString()
107
155
  : null,
156
+ instances: this.getInstanceSummaries(instances),
108
157
  token,
109
158
  });
110
159
  } catch (err) {
@@ -158,23 +207,67 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
158
207
 
159
208
  const reportedAt: Date = OneUptimeDate.getCurrentDate();
160
209
 
161
- await EnterpriseLicenseService.updateOneById({
162
- id: license.id!,
163
- data: {
164
- currentUserCount: userCount,
165
- userCountUpdatedAt: reportedAt,
166
- },
167
- props: {
168
- isRoot: true,
169
- ignoreHooks: true,
170
- },
171
- });
210
+ const instanceId: string = this.parseShortText(
211
+ req.body["instanceId"],
212
+ );
213
+ const instanceHost: string = this.parseShortText(req.body["host"]);
214
+ const userEmailHashes: Array<string> =
215
+ EnterpriseLicenseUsageUtil.sanitizeUserEmailHashes(
216
+ req.body["userEmailHashes"],
217
+ );
218
+
219
+ if (instanceId) {
220
+ /*
221
+ * Multi-instance report: track usage per instance and count
222
+ * users uniquely across all instances of this license.
223
+ */
224
+ await this.upsertLicenseInstance({
225
+ licenseId: license.id!,
226
+ instanceId: instanceId,
227
+ host: instanceHost || undefined,
228
+ userCount: userCount,
229
+ userEmailHashes: userEmailHashes,
230
+ lastReportedAt: reportedAt,
231
+ });
232
+ }
233
+
234
+ const instances: Array<EnterpriseLicenseInstance> =
235
+ await this.findLicenseInstances(license.id!);
236
+
237
+ let currentUserCount: number = userCount;
238
+
239
+ if (instances.length > 0) {
240
+ currentUserCount = EnterpriseLicenseUsageUtil.getUniqueUserCount(
241
+ instances,
242
+ reportedAt,
243
+ );
244
+ }
245
+
246
+ /*
247
+ * Legacy reports (no instanceId) only drive the license-wide count
248
+ * while no instance has registered — otherwise they would stomp
249
+ * the deduplicated multi-instance count.
250
+ */
251
+ if (instanceId || instances.length === 0) {
252
+ await EnterpriseLicenseService.updateOneById({
253
+ id: license.id!,
254
+ data: {
255
+ currentUserCount: currentUserCount,
256
+ userCountUpdatedAt: reportedAt,
257
+ },
258
+ props: {
259
+ isRoot: true,
260
+ ignoreHooks: true,
261
+ },
262
+ });
263
+ }
172
264
 
173
265
  return Response.sendJsonObjectResponse(req, res, {
174
- currentUserCount: userCount,
266
+ currentUserCount: currentUserCount,
175
267
  userCountUpdatedAt: reportedAt.toISOString(),
176
268
  userLimit:
177
269
  typeof license.userLimit === "number" ? license.userLimit : null,
270
+ instances: this.getInstanceSummaries(instances),
178
271
  });
179
272
  } catch (err) {
180
273
  next(err);
@@ -182,4 +275,178 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
182
275
  },
183
276
  );
184
277
  }
278
+
279
+ private parseShortText(value: unknown): string {
280
+ if (typeof value !== "string") {
281
+ return "";
282
+ }
283
+
284
+ return value.trim().substring(0, 100);
285
+ }
286
+
287
+ private async findLicenseInstances(
288
+ licenseId: ObjectID,
289
+ ): Promise<Array<EnterpriseLicenseInstance>> {
290
+ return EnterpriseLicenseInstanceService.findBy({
291
+ query: {
292
+ enterpriseLicenseId: licenseId,
293
+ },
294
+ select: {
295
+ _id: true,
296
+ instanceId: true,
297
+ host: true,
298
+ userCount: true,
299
+ userEmailHashes: true,
300
+ lastReportedAt: true,
301
+ },
302
+ sort: {
303
+ createdAt: SortOrder.Ascending,
304
+ },
305
+ skip: 0,
306
+ limit: LIMIT_MAX,
307
+ props: {
308
+ isRoot: true,
309
+ },
310
+ });
311
+ }
312
+
313
+ private getInstanceSummaries(
314
+ instances: Array<EnterpriseLicenseInstance>,
315
+ ): Array<EnterpriseLicenseInstanceSummary> {
316
+ return instances.map(
317
+ (
318
+ instance: EnterpriseLicenseInstance,
319
+ ): EnterpriseLicenseInstanceSummary => {
320
+ return {
321
+ instanceId: instance.instanceId || "",
322
+ host: instance.host || null,
323
+ userCount:
324
+ typeof instance.userCount === "number" ? instance.userCount : null,
325
+ lastReportedAt: instance.lastReportedAt
326
+ ? instance.lastReportedAt.toISOString()
327
+ : null,
328
+ };
329
+ },
330
+ );
331
+ }
332
+
333
+ private async upsertLicenseInstance(
334
+ data: LicenseInstanceUpsert,
335
+ ): Promise<void> {
336
+ const updated: boolean = await this.updateLicenseInstanceIfExists(data);
337
+
338
+ if (updated) {
339
+ return;
340
+ }
341
+
342
+ const instanceCount: PositiveNumber =
343
+ await EnterpriseLicenseInstanceService.countBy({
344
+ query: {
345
+ enterpriseLicenseId: data.licenseId,
346
+ },
347
+ props: {
348
+ isRoot: true,
349
+ },
350
+ });
351
+
352
+ if (instanceCount.toNumber() >= MAX_INSTANCES_PER_LICENSE) {
353
+ throw new BadDataException(
354
+ "Too many instances are registered for this license. Please contact support@oneuptime.com.",
355
+ );
356
+ }
357
+
358
+ const newInstance: EnterpriseLicenseInstance =
359
+ new EnterpriseLicenseInstance();
360
+ newInstance.enterpriseLicenseId = data.licenseId;
361
+ newInstance.instanceId = data.instanceId;
362
+
363
+ if (data.host !== undefined) {
364
+ newInstance.host = data.host;
365
+ }
366
+
367
+ if (data.userCount !== undefined) {
368
+ newInstance.userCount = data.userCount;
369
+ }
370
+
371
+ if (data.userEmailHashes !== undefined) {
372
+ newInstance.userEmailHashes = data.userEmailHashes;
373
+ }
374
+
375
+ if (data.lastReportedAt !== undefined) {
376
+ newInstance.lastReportedAt = data.lastReportedAt;
377
+ }
378
+
379
+ try {
380
+ await EnterpriseLicenseInstanceService.create({
381
+ data: newInstance,
382
+ props: {
383
+ isRoot: true,
384
+ },
385
+ });
386
+ } catch (err) {
387
+ /*
388
+ * A concurrent request created the row between our check and this
389
+ * insert — the unique index on (enterpriseLicenseId, instanceId)
390
+ * rejected the duplicate. Apply the report as an update instead.
391
+ */
392
+ const retried: boolean = await this.updateLicenseInstanceIfExists(data);
393
+
394
+ if (!retried) {
395
+ throw err;
396
+ }
397
+ }
398
+ }
399
+
400
+ private async updateLicenseInstanceIfExists(
401
+ data: LicenseInstanceUpsert,
402
+ ): Promise<boolean> {
403
+ const existingInstance: EnterpriseLicenseInstance | null =
404
+ await EnterpriseLicenseInstanceService.findOneBy({
405
+ query: {
406
+ enterpriseLicenseId: data.licenseId,
407
+ instanceId: data.instanceId,
408
+ },
409
+ select: {
410
+ _id: true,
411
+ },
412
+ props: {
413
+ isRoot: true,
414
+ },
415
+ });
416
+
417
+ if (!existingInstance) {
418
+ return false;
419
+ }
420
+
421
+ const updateData: PartialEntity<EnterpriseLicenseInstance> = {};
422
+
423
+ if (data.host !== undefined) {
424
+ updateData.host = data.host;
425
+ }
426
+
427
+ if (data.userCount !== undefined) {
428
+ updateData.userCount = data.userCount;
429
+ }
430
+
431
+ if (data.userEmailHashes !== undefined) {
432
+ updateData.userEmailHashes = data.userEmailHashes;
433
+ }
434
+
435
+ if (data.lastReportedAt !== undefined) {
436
+ updateData.lastReportedAt = data.lastReportedAt;
437
+ }
438
+
439
+ if (Object.keys(updateData).length > 0) {
440
+ await EnterpriseLicenseInstanceService.updateOneById({
441
+ id: existingInstance.id!,
442
+ data: updateData,
443
+ props: {
444
+ isRoot: true,
445
+ ignoreHooks: true,
446
+ },
447
+ });
448
+ }
449
+
450
+ return true;
451
+ }
185
452
  }