@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
@@ -0,0 +1,390 @@
1
+ import MetricMonitorCriteria from "../../../../../Server/Utils/Monitor/Criteria/MetricMonitorCriteria";
2
+ import MonitorCriteriaDataExtractor from "../../../../../Server/Utils/Monitor/MonitorCriteriaDataExtractor";
3
+ import AggregateModel from "../../../../../Types/BaseDatabase/AggregatedModel";
4
+ import AggregatedResult from "../../../../../Types/BaseDatabase/AggregatedResult";
5
+ import {
6
+ CheckOn,
7
+ CriteriaFilter,
8
+ EvaluateOverTimeType,
9
+ FilterType,
10
+ } from "../../../../../Types/Monitor/CriteriaFilter";
11
+ import MetricFormulaConfigData from "../../../../../Types/Metrics/MetricFormulaConfigData";
12
+ import MetricQueryConfigData from "../../../../../Types/Metrics/MetricQueryConfigData";
13
+ import MetricQueryData from "../../../../../Types/Metrics/MetricQueryData";
14
+ import MetricsViewConfig from "../../../../../Types/Metrics/MetricsViewConfig";
15
+ import MetricMonitorResponse from "../../../../../Types/Monitor/MetricMonitor/MetricMonitorResponse";
16
+ import MonitorStep, {
17
+ MonitorStepType,
18
+ } from "../../../../../Types/Monitor/MonitorStep";
19
+ import { MonitorStepCephMonitorUtil } from "../../../../../Types/Monitor/MonitorStepCephMonitor";
20
+ import { MonitorStepDockerMonitorUtil } from "../../../../../Types/Monitor/MonitorStepDockerMonitor";
21
+ import { MonitorStepDockerSwarmMonitorUtil } from "../../../../../Types/Monitor/MonitorStepDockerSwarmMonitor";
22
+ import { MonitorStepHostMonitorUtil } from "../../../../../Types/Monitor/MonitorStepHostMonitor";
23
+ import { MonitorStepKubernetesMonitorUtil } from "../../../../../Types/Monitor/MonitorStepKubernetesMonitor";
24
+ import { MonitorStepPodmanMonitorUtil } from "../../../../../Types/Monitor/MonitorStepPodmanMonitor";
25
+ import { MonitorStepProxmoxMonitorUtil } from "../../../../../Types/Monitor/MonitorStepProxmoxMonitor";
26
+ import RollingTime from "../../../../../Types/RollingTime/RollingTime";
27
+ import ObjectID from "../../../../../Types/ObjectID";
28
+
29
+ /*
30
+ * Criteria evaluation resolves `metricAlias` → result slot via the query
31
+ * configs on the monitor step. Telemetry monitors other than the plain
32
+ * Metric monitor (IoT, Kubernetes, Docker, ...) store their query configs
33
+ * under their own step shape (e.g. `data.iotMonitor.metricViewConfig`), so
34
+ * alias resolution must read from whichever shape the step carries — not
35
+ * just `data.metricMonitor`. These tests pin that behavior: a criteria
36
+ * referencing the second query's alias must evaluate the second result
37
+ * slot, never silently fall back to slot 0.
38
+ */
39
+
40
+ function buildQueryConfig(input: {
41
+ alias: string;
42
+ metricName: string;
43
+ }): MetricQueryConfigData {
44
+ return {
45
+ metricAliasData: {
46
+ metricVariable: input.alias,
47
+ title: input.metricName,
48
+ description: undefined,
49
+ legend: undefined,
50
+ legendUnit: undefined,
51
+ },
52
+ metricQueryData: {
53
+ filterData: {
54
+ metricName: input.metricName,
55
+ },
56
+ } as unknown as MetricQueryData,
57
+ };
58
+ }
59
+
60
+ /*
61
+ * Two queries — "a" (iot_battery_percent) and "b"
62
+ * (iot_signal_strength_dbm) — plus an optional formula "f" = a + b.
63
+ * Result slots: 0 → a, 1 → b, 2 → f.
64
+ */
65
+ function buildViewConfig(input?: { withFormula?: boolean }): MetricsViewConfig {
66
+ const formulaConfigs: Array<MetricFormulaConfigData> = input?.withFormula
67
+ ? [
68
+ {
69
+ metricAliasData: {
70
+ metricVariable: "f",
71
+ title: "Battery + Signal",
72
+ description: undefined,
73
+ legend: undefined,
74
+ legendUnit: undefined,
75
+ },
76
+ metricFormulaData: {
77
+ metricFormula: "a + b",
78
+ },
79
+ },
80
+ ]
81
+ : [];
82
+
83
+ return {
84
+ queryConfigs: [
85
+ buildQueryConfig({ alias: "a", metricName: "iot_battery_percent" }),
86
+ buildQueryConfig({ alias: "b", metricName: "iot_signal_strength_dbm" }),
87
+ ],
88
+ formulaConfigs,
89
+ };
90
+ }
91
+
92
+ /*
93
+ * Bare step whose telemetry shape is attached by `assignShape` — the
94
+ * point of these tests is that `data.metricMonitor` stays undefined.
95
+ */
96
+ function buildStep(
97
+ assignShape: (data: MonitorStepType, viewConfig: MetricsViewConfig) => void,
98
+ viewConfig: MetricsViewConfig,
99
+ ): MonitorStep {
100
+ const monitorStep: MonitorStep = new MonitorStep();
101
+ monitorStep.data = {
102
+ id: ObjectID.generate().toString(),
103
+ monitorCriteria: { data: undefined } as never,
104
+ } as unknown as MonitorStep["data"];
105
+ assignShape(monitorStep.data!, viewConfig);
106
+ return monitorStep;
107
+ }
108
+
109
+ function buildIoTStep(viewConfig: MetricsViewConfig): MonitorStep {
110
+ return buildStep((data: MonitorStepType, v: MetricsViewConfig) => {
111
+ data.iotMonitor = {
112
+ fleetIdentifier: "plant-a-fleet",
113
+ resourceFilters: {},
114
+ metricViewConfig: v,
115
+ rollingTime: RollingTime.Past1Minute,
116
+ };
117
+ }, viewConfig);
118
+ }
119
+
120
+ /*
121
+ * One aggregated result per result slot, in query/formula order.
122
+ */
123
+ function buildResponse(input: {
124
+ viewConfig: MetricsViewConfig;
125
+ resultSlots: Array<Array<number>>;
126
+ }): MetricMonitorResponse {
127
+ const metricResult: Array<AggregatedResult> = input.resultSlots.map(
128
+ (values: Array<number>) => {
129
+ return {
130
+ data: values.map((v: number) => {
131
+ return {
132
+ timestamp: new Date(),
133
+ value: v,
134
+ } as AggregateModel;
135
+ }),
136
+ };
137
+ },
138
+ );
139
+
140
+ return {
141
+ projectId: ObjectID.generate(),
142
+ metricResult,
143
+ metricViewConfig: input.viewConfig,
144
+ monitorId: ObjectID.generate(),
145
+ };
146
+ }
147
+
148
+ function greaterThanFilter(input: {
149
+ alias: string;
150
+ threshold: string;
151
+ }): CriteriaFilter {
152
+ return {
153
+ checkOn: CheckOn.MetricValue,
154
+ filterType: FilterType.GreaterThan,
155
+ value: input.threshold,
156
+ metricMonitorOptions: {
157
+ metricAlias: input.alias,
158
+ metricAggregationType: EvaluateOverTimeType.AnyValue,
159
+ },
160
+ };
161
+ }
162
+
163
+ describe("MetricMonitorCriteria — IoT monitor step alias resolution", () => {
164
+ test("criteria on the second alias evaluates the second series", async () => {
165
+ const viewConfig: MetricsViewConfig = buildViewConfig();
166
+ const criteriaFilter: CriteriaFilter = greaterThanFilter({
167
+ alias: "b",
168
+ threshold: "30",
169
+ });
170
+
171
+ // Slot 0 (alias a) stays under the threshold; slot 1 (alias b) breaches.
172
+ const message: string | null =
173
+ await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
174
+ dataToProcess: buildResponse({
175
+ viewConfig,
176
+ resultSlots: [[10, 12], [55]],
177
+ }),
178
+ criteriaFilter,
179
+ monitorStep: buildIoTStep(viewConfig),
180
+ });
181
+
182
+ expect(message).toBeTruthy();
183
+ expect(message).toContain("55");
184
+ expect(criteriaFilter.metricCriteriaContext?.metricName).toBe(
185
+ "iot_signal_strength_dbm",
186
+ );
187
+ expect(criteriaFilter.metricCriteriaContext?.breachingSample?.value).toBe(
188
+ 55,
189
+ );
190
+ });
191
+
192
+ test("criteria on the second alias ignores a breach in the first series", async () => {
193
+ const viewConfig: MetricsViewConfig = buildViewConfig();
194
+
195
+ // Slot 0 (alias a) breaches, but the criteria targets alias b.
196
+ const message: string | null =
197
+ await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
198
+ dataToProcess: buildResponse({
199
+ viewConfig,
200
+ resultSlots: [[90], [10]],
201
+ }),
202
+ criteriaFilter: greaterThanFilter({ alias: "b", threshold: "30" }),
203
+ monitorStep: buildIoTStep(viewConfig),
204
+ });
205
+
206
+ expect(message).toBeNull();
207
+ });
208
+
209
+ test("formula alias resolves to the formula result slot", async () => {
210
+ const viewConfig: MetricsViewConfig = buildViewConfig({
211
+ withFormula: true,
212
+ });
213
+ const criteriaFilter: CriteriaFilter = greaterThanFilter({
214
+ alias: "f",
215
+ threshold: "100",
216
+ });
217
+
218
+ // Slots: a → 10, b → 20, f (formula) → 150. Only the formula breaches.
219
+ const message: string | null =
220
+ await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
221
+ dataToProcess: buildResponse({
222
+ viewConfig,
223
+ resultSlots: [[10], [20], [150]],
224
+ }),
225
+ criteriaFilter,
226
+ monitorStep: buildIoTStep(viewConfig),
227
+ });
228
+
229
+ expect(message).toBeTruthy();
230
+ expect(message).toContain("150");
231
+ expect(criteriaFilter.metricCriteriaContext?.isFormula).toBe(true);
232
+ });
233
+ });
234
+
235
+ describe("MonitorCriteriaDataExtractor — IoT monitor step alias resolution", () => {
236
+ test("extractMetricValues returns the aliased series, not slot 0", () => {
237
+ const viewConfig: MetricsViewConfig = buildViewConfig();
238
+
239
+ const extracted: { alias: string | null; values: Array<number> } | null =
240
+ MonitorCriteriaDataExtractor.extractMetricValues({
241
+ criteriaFilter: greaterThanFilter({ alias: "b", threshold: "30" }),
242
+ dataToProcess: buildResponse({
243
+ viewConfig,
244
+ resultSlots: [
245
+ [10, 12],
246
+ [55, 60],
247
+ ],
248
+ }),
249
+ monitorStep: buildIoTStep(viewConfig),
250
+ });
251
+
252
+ expect(extracted).not.toBeNull();
253
+ expect(extracted?.alias).toBe("b");
254
+ expect(extracted?.values).toEqual([55, 60]);
255
+ });
256
+ });
257
+
258
+ describe("MetricMonitorCriteria — other telemetry step shapes", () => {
259
+ type ShapeCase = {
260
+ name: string;
261
+ assignShape: (data: MonitorStepType, viewConfig: MetricsViewConfig) => void;
262
+ };
263
+
264
+ const shapeCases: Array<ShapeCase> = [
265
+ {
266
+ name: "kubernetesMonitor",
267
+ assignShape: (data: MonitorStepType, v: MetricsViewConfig) => {
268
+ data.kubernetesMonitor = {
269
+ ...MonitorStepKubernetesMonitorUtil.getDefault(),
270
+ metricViewConfig: v,
271
+ };
272
+ },
273
+ },
274
+ {
275
+ name: "dockerMonitor",
276
+ assignShape: (data: MonitorStepType, v: MetricsViewConfig) => {
277
+ data.dockerMonitor = {
278
+ ...MonitorStepDockerMonitorUtil.getDefault(),
279
+ metricViewConfig: v,
280
+ };
281
+ },
282
+ },
283
+ {
284
+ name: "dockerSwarmMonitor",
285
+ assignShape: (data: MonitorStepType, v: MetricsViewConfig) => {
286
+ data.dockerSwarmMonitor = {
287
+ ...MonitorStepDockerSwarmMonitorUtil.getDefault(),
288
+ metricViewConfig: v,
289
+ };
290
+ },
291
+ },
292
+ {
293
+ name: "hostMonitor",
294
+ assignShape: (data: MonitorStepType, v: MetricsViewConfig) => {
295
+ data.hostMonitor = {
296
+ ...MonitorStepHostMonitorUtil.getDefault(),
297
+ metricViewConfig: v,
298
+ };
299
+ },
300
+ },
301
+ {
302
+ name: "podmanMonitor",
303
+ assignShape: (data: MonitorStepType, v: MetricsViewConfig) => {
304
+ data.podmanMonitor = {
305
+ ...MonitorStepPodmanMonitorUtil.getDefault(),
306
+ metricViewConfig: v,
307
+ };
308
+ },
309
+ },
310
+ {
311
+ name: "proxmoxMonitor",
312
+ assignShape: (data: MonitorStepType, v: MetricsViewConfig) => {
313
+ data.proxmoxMonitor = {
314
+ ...MonitorStepProxmoxMonitorUtil.getDefault(),
315
+ metricViewConfig: v,
316
+ };
317
+ },
318
+ },
319
+ {
320
+ name: "cephMonitor",
321
+ assignShape: (data: MonitorStepType, v: MetricsViewConfig) => {
322
+ data.cephMonitor = {
323
+ ...MonitorStepCephMonitorUtil.getDefault(),
324
+ metricViewConfig: v,
325
+ };
326
+ },
327
+ },
328
+ ];
329
+
330
+ test.each(shapeCases)(
331
+ "$name: criteria on the second alias evaluates the second series",
332
+ async (shapeCase: ShapeCase) => {
333
+ const viewConfig: MetricsViewConfig = buildViewConfig();
334
+ const criteriaFilter: CriteriaFilter = greaterThanFilter({
335
+ alias: "b",
336
+ threshold: "30",
337
+ });
338
+
339
+ const message: string | null =
340
+ await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
341
+ dataToProcess: buildResponse({
342
+ viewConfig,
343
+ resultSlots: [[10, 12], [55]],
344
+ }),
345
+ criteriaFilter,
346
+ monitorStep: buildStep(shapeCase.assignShape, viewConfig),
347
+ });
348
+
349
+ expect(message).toBeTruthy();
350
+ expect(message).toContain("55");
351
+ expect(criteriaFilter.metricCriteriaContext?.metricName).toBe(
352
+ "iot_signal_strength_dbm",
353
+ );
354
+ },
355
+ );
356
+ });
357
+
358
+ describe("MetricMonitorCriteria — metric monitor step (regression)", () => {
359
+ test("alias resolution still reads data.metricMonitor", async () => {
360
+ const viewConfig: MetricsViewConfig = buildViewConfig();
361
+ const criteriaFilter: CriteriaFilter = greaterThanFilter({
362
+ alias: "b",
363
+ threshold: "30",
364
+ });
365
+
366
+ const message: string | null =
367
+ await MetricMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
368
+ dataToProcess: buildResponse({
369
+ viewConfig,
370
+ resultSlots: [[10, 12], [55]],
371
+ }),
372
+ criteriaFilter,
373
+ monitorStep: buildStep(
374
+ (data: MonitorStepType, v: MetricsViewConfig) => {
375
+ data.metricMonitor = {
376
+ metricViewConfig: v,
377
+ rollingTime: RollingTime.Past1Minute,
378
+ };
379
+ },
380
+ viewConfig,
381
+ ),
382
+ });
383
+
384
+ expect(message).toBeTruthy();
385
+ expect(message).toContain("55");
386
+ expect(criteriaFilter.metricCriteriaContext?.metricName).toBe(
387
+ "iot_signal_strength_dbm",
388
+ );
389
+ });
390
+ });
@@ -0,0 +1,202 @@
1
+ import {
2
+ IoTDeviceBufferEntry,
3
+ IoTFleetSnapshotBufferEntry,
4
+ IOT_MAX_FUTURE_CLOCK_SKEW_MINUTES,
5
+ bufferIoTSnapshotMetric,
6
+ clampIoTTimestamp,
7
+ } from "../../../../Server/Utils/Telemetry/IoTSnapshotScan";
8
+ import { JSONObject } from "../../../../Types/JSON";
9
+
10
+ /*
11
+ * Malformed-device hardening for the IoT snapshot scan:
12
+ *
13
+ * - a `device.id` longer than the ShortText column limit (100) is
14
+ * truncated at ingest, so one bad device can never fail the
15
+ * 500-row inventory INSERT chunk downstream — and two ids sharing
16
+ * the truncated prefix fold into ONE entry (a duplicate conflict
17
+ * target inside a single INSERT would abort the statement),
18
+ * - a future-skewed device clock is clamped to ingest time, so the
19
+ * `>=` lastSeenAt dominance guard can't get wedged by one bogus
20
+ * future timestamp and then reject every legitimate update.
21
+ */
22
+
23
+ const FLEET: string = "0a1b2c3d-0000-0000-0000-000000000001";
24
+ const SHORT_TEXT_LIMIT: number = 100;
25
+
26
+ type LabelMap = Record<string, string>;
27
+
28
+ function toNano(ms: number): string {
29
+ return `${ms}000000`;
30
+ }
31
+
32
+ function datapoint(data: {
33
+ value: number;
34
+ atMs?: number;
35
+ labels?: LabelMap;
36
+ asDouble?: boolean;
37
+ }): JSONObject {
38
+ const attributes: Array<JSONObject> = Object.entries(data.labels || {}).map(
39
+ ([key, value]: [string, string]) => {
40
+ return { key, value: { stringValue: value } };
41
+ },
42
+ );
43
+ return {
44
+ ...(data.asDouble ? { asDouble: data.value } : { asInt: data.value }),
45
+ timeUnixNano: toNano(data.atMs ?? Date.now()),
46
+ attributes,
47
+ };
48
+ }
49
+
50
+ interface IoTBuffers {
51
+ resourceBuffer: Map<string, Map<string, IoTDeviceBufferEntry>>;
52
+ fleetBuffer: Map<string, IoTFleetSnapshotBufferEntry>;
53
+ }
54
+
55
+ function iotBuffers(): IoTBuffers {
56
+ return { resourceBuffer: new Map(), fleetBuffer: new Map() };
57
+ }
58
+
59
+ function feed(buffers: IoTBuffers, metricName: string, dp: JSONObject): void {
60
+ bufferIoTSnapshotMetric({
61
+ fleetIdStr: FLEET,
62
+ metricName,
63
+ datapoint: dp,
64
+ resourceBuffer: buffers.resourceBuffer,
65
+ fleetBuffer: buffers.fleetBuffer,
66
+ });
67
+ }
68
+
69
+ function entries(buffers: IoTBuffers): Array<IoTDeviceBufferEntry> {
70
+ return Array.from(buffers.resourceBuffer.get(FLEET)?.values() || []);
71
+ }
72
+
73
+ describe("clampIoTTimestamp", () => {
74
+ const now: Date = new Date("2026-07-02T12:00:00.000Z");
75
+
76
+ test("keeps a past timestamp untouched", () => {
77
+ const past: Date = new Date("2026-07-02T11:00:00.000Z");
78
+ expect(clampIoTTimestamp(past, now)).toEqual(past);
79
+ });
80
+
81
+ test("keeps a future timestamp within the skew tolerance", () => {
82
+ const slightlyAhead: Date = new Date(
83
+ now.getTime() + (IOT_MAX_FUTURE_CLOCK_SKEW_MINUTES - 1) * 60 * 1000,
84
+ );
85
+ expect(clampIoTTimestamp(slightlyAhead, now)).toEqual(slightlyAhead);
86
+ });
87
+
88
+ test("clamps a timestamp beyond the skew tolerance back to now", () => {
89
+ const farAhead: Date = new Date(now.getTime() + 60 * 60 * 1000);
90
+ expect(clampIoTTimestamp(farAhead, now)).toEqual(now);
91
+ });
92
+ });
93
+
94
+ describe("bufferIoTSnapshotMetric — device.id length hardening", () => {
95
+ test("truncates a device.id longer than the 100-char column limit", () => {
96
+ const longId: string = "x".repeat(150);
97
+ const buffers: IoTBuffers = iotBuffers();
98
+
99
+ feed(buffers, "iot_device_up", {
100
+ ...datapoint({ value: 1, labels: { "device.id": longId } }),
101
+ });
102
+
103
+ const buffered: Array<IoTDeviceBufferEntry> = entries(buffers);
104
+ expect(buffered).toHaveLength(1);
105
+ expect(buffered[0]!.externalId).toBe("x".repeat(SHORT_TEXT_LIMIT));
106
+ expect(buffered[0]!.externalId.length).toBe(SHORT_TEXT_LIMIT);
107
+ });
108
+
109
+ test("keeps a device.id exactly at the column limit untouched", () => {
110
+ const exactId: string = "y".repeat(SHORT_TEXT_LIMIT);
111
+ const buffers: IoTBuffers = iotBuffers();
112
+
113
+ feed(buffers, "iot_device_up", {
114
+ ...datapoint({ value: 1, labels: { "device.id": exactId } }),
115
+ });
116
+
117
+ expect(entries(buffers)[0]!.externalId).toBe(exactId);
118
+ });
119
+
120
+ test("two over-long ids sharing the truncated prefix fold into one entry", () => {
121
+ const shared: string = "z".repeat(SHORT_TEXT_LIMIT);
122
+ const buffers: IoTBuffers = iotBuffers();
123
+
124
+ feed(buffers, "iot_device_up", {
125
+ ...datapoint({ value: 1, labels: { "device.id": `${shared}-alpha` } }),
126
+ });
127
+ feed(buffers, "iot_device_up", {
128
+ ...datapoint({ value: 0, labels: { "device.id": `${shared}-bravo` } }),
129
+ });
130
+
131
+ // One folded identity — never two rows hitting one conflict target.
132
+ const buffered: Array<IoTDeviceBufferEntry> = entries(buffers);
133
+ expect(buffered).toHaveLength(1);
134
+ expect(buffered[0]!.externalId).toBe(shared);
135
+ });
136
+ });
137
+
138
+ describe("bufferIoTSnapshotMetric — future clock-skew hardening", () => {
139
+ test("clamps an observedAt beyond the skew tolerance to ingest time", () => {
140
+ const buffers: IoTBuffers = iotBuffers();
141
+ const before: number = Date.now();
142
+
143
+ feed(buffers, "iot_battery_percent", {
144
+ ...datapoint({
145
+ value: 55,
146
+ asDouble: true,
147
+ atMs: Date.now() + 60 * 60 * 1000, // device clock 1h ahead
148
+ labels: { "device.id": "sensor-1" },
149
+ }),
150
+ });
151
+
152
+ const after: number = Date.now();
153
+ const observedAt: Date = entries(buffers)[0]!.observedAt;
154
+ expect(observedAt.getTime()).toBeGreaterThanOrEqual(before);
155
+ expect(observedAt.getTime()).toBeLessThanOrEqual(after);
156
+ });
157
+
158
+ test("keeps an observedAt within the skew tolerance untouched", () => {
159
+ const buffers: IoTBuffers = iotBuffers();
160
+ const slightlyAheadMs: number =
161
+ Date.now() + (IOT_MAX_FUTURE_CLOCK_SKEW_MINUTES - 1) * 60 * 1000;
162
+
163
+ feed(buffers, "iot_battery_percent", {
164
+ ...datapoint({
165
+ value: 55,
166
+ asDouble: true,
167
+ atMs: slightlyAheadMs,
168
+ labels: { "device.id": "sensor-1" },
169
+ }),
170
+ });
171
+
172
+ expect(entries(buffers)[0]!.observedAt.getTime()).toBe(
173
+ Math.trunc(slightlyAheadMs),
174
+ );
175
+ });
176
+
177
+ test("a clamped future point no longer wedges newest-wins folding", () => {
178
+ const buffers: IoTBuffers = iotBuffers();
179
+
180
+ // Skewed device stamps 1h ahead — clamped to ~now at ingest.
181
+ feed(buffers, "iot_battery_percent", {
182
+ ...datapoint({
183
+ value: 50,
184
+ asDouble: true,
185
+ atMs: Date.now() + 60 * 60 * 1000,
186
+ labels: { "device.id": "sensor-1" },
187
+ }),
188
+ });
189
+
190
+ // A legitimate later reading must still win the fold.
191
+ feed(buffers, "iot_battery_percent", {
192
+ ...datapoint({
193
+ value: 80,
194
+ asDouble: true,
195
+ atMs: Date.now() + 1000,
196
+ labels: { "device.id": "sensor-1" },
197
+ }),
198
+ });
199
+
200
+ expect(entries(buffers)[0]!.latestBatteryPercent).toBe(80);
201
+ });
202
+ });