@oneuptime/common 12.0.23 → 12.0.24

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 (294) hide show
  1. package/Models/DatabaseModels/DetectionRule.ts +55 -0
  2. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +27 -7
  3. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +67 -0
  4. package/Models/DatabaseModels/Project.ts +68 -0
  5. package/Models/DatabaseModels/User.ts +68 -0
  6. package/Server/API/TelemetryAPI.ts +29 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.ts +28 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.ts +49 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.ts +33 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  11. package/Server/Middleware/GzipRequestBody.ts +311 -0
  12. package/Server/Middleware/MultipartFormData.ts +87 -3
  13. package/Server/Services/DetectionRuleService.ts +73 -6
  14. package/Server/Services/Index.ts +205 -0
  15. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +59 -0
  16. package/Server/Services/NetworkDeviceService.ts +148 -0
  17. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  18. package/Server/Services/OnCallDutyPolicyFeedService.ts +1 -1
  19. package/Server/Services/ProjectService.ts +29 -19
  20. package/Server/Services/SecurityEventService.ts +90 -13
  21. package/Server/Services/StatusPageResourceService.ts +223 -0
  22. package/Server/Services/TelemetryAttributeService.ts +17 -0
  23. package/Server/Services/UserService.ts +4 -6
  24. package/Server/Types/Markdown.ts +161 -67
  25. package/Server/Utils/AI/Toolbox/OnCallTools.ts +12 -4
  26. package/Server/Utils/AnalyticsDatabase/Statement.ts +13 -1
  27. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +297 -6
  28. package/Server/Utils/Attribution.ts +4 -0
  29. package/Server/Utils/Marketing/MarketingEventUtil.ts +45 -7
  30. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +55 -3
  31. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +21 -0
  32. package/Server/Utils/StartServer.ts +12 -32
  33. package/Server/Utils/Telemetry/LlmCostBudgetEvaluator.ts +101 -4
  34. package/Server/Utils/Telemetry/LlmMetricSpend.ts +130 -0
  35. package/Server/Views/Partials/AnalyticsConsent.ejs +159 -102
  36. package/Server/Views/Partials/Head.ejs +2 -1
  37. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +776 -38
  38. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +359 -0
  39. package/Tests/App/Dashboard/CorrelateGraph.test.tsx +865 -0
  40. package/Tests/App/Dashboard/CorrelationGraph.test.ts +346 -0
  41. package/Tests/App/Dashboard/CriteriaFilterConditionDropdown.test.tsx +256 -0
  42. package/Tests/App/Dashboard/CriteriaFilterDefaults.test.ts +738 -0
  43. package/Tests/App/Dashboard/CriteriaFilterMonitorTypeChange.test.ts +1122 -0
  44. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +283 -0
  45. package/Tests/App/Dashboard/ExternalStatusPageCriteriaFilter.test.ts +407 -0
  46. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +5 -14
  47. package/Tests/App/Dashboard/MonitorCriteriaActionToggles.test.tsx +426 -0
  48. package/Tests/App/Dashboard/MonitorStepsCriteriaAlignmentWiring.test.tsx +352 -0
  49. package/Tests/App/Dashboard/OnCallLayerCardOverrides.test.tsx +278 -0
  50. package/Tests/App/Dashboard/OnCallLayerShiftPreviewOverrides.test.ts +237 -0
  51. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +2 -2
  52. package/Tests/App/Dashboard/OnCallSchedulePreviewOverrides.test.tsx +460 -0
  53. package/Tests/App/Dashboard/SecurityEventAttributeUtil.test.ts +171 -0
  54. package/Tests/App/Dashboard/SecurityEventCorrelation.test.ts +857 -0
  55. package/Tests/App/Dashboard/SecurityEventDetailObservables.test.tsx +194 -0
  56. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +124 -0
  57. package/Tests/App/Dashboard/SecurityEventsMonitorStepForm.test.tsx +79 -19
  58. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +382 -0
  59. package/Tests/App/StatusPage/BulkAddStatusPageMonitors.test.ts +216 -0
  60. package/Tests/App/StatusPage/BulkAddStatusPageMonitorsModal.test.tsx +590 -11
  61. package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +757 -0
  62. package/Tests/Models/DiscoveryScanNameColumn.test.ts +293 -0
  63. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +564 -0
  64. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +120 -4
  65. package/Tests/Server/Middleware/GzipRequestBody.test.ts +725 -0
  66. package/Tests/Server/Middleware/MultipartFormData.test.ts +298 -0
  67. package/Tests/Server/Services/AddDetectionRuleDistinctCountColumnsMigration.test.ts +311 -0
  68. package/Tests/Server/Services/DetectionRuleService.test.ts +134 -2
  69. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +7 -1
  70. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +131 -0
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanService.test.ts +194 -0
  72. package/Tests/Server/Services/NetworkDeviceMonitorStatusStamp.test.ts +800 -0
  73. package/Tests/Server/Services/OnCallScheduleAttachRosterRefresh.test.ts +244 -0
  74. package/Tests/Server/Services/SecurityEventService.test.ts +376 -0
  75. package/Tests/Server/Services/StatusPageResourceDuplicates.test.ts +703 -0
  76. package/Tests/Server/Services/TelemetryAttributeServiceSecurityEvent.test.ts +256 -0
  77. package/Tests/Server/Types/Workflow/Components/ComponentRegistryParity.test.ts +417 -0
  78. package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorWhereOperators.test.ts +677 -0
  79. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +43 -0
  80. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +15 -11
  81. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +369 -8
  82. package/Tests/Server/Utils/SecurityEvent/SigmaClickhouseCompiler.test.ts +35 -0
  83. package/Tests/Server/Utils/Telemetry/LlmCostBudgetEvaluator.test.ts +368 -4
  84. package/Tests/Server/Utils/Telemetry/LlmMetricSpend.test.ts +245 -0
  85. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +48 -5
  86. package/Tests/Types/Code/YamlSyntax.test.ts +396 -0
  87. package/Tests/Types/Monitor/MetricAndProfileDefaultCriteria.test.ts +957 -0
  88. package/Tests/Types/Monitor/MonitorCriteriaActionValidation.test.ts +598 -0
  89. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +49 -0
  90. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +6 -6
  91. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.test.ts +4 -4
  92. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +4 -3
  93. package/Tests/Types/Telemetry/LlmMetricConventions.test.ts +193 -0
  94. package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +0 -12
  95. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +1 -17
  96. package/Tests/UI/Components/CodeEditor.test.tsx +474 -10
  97. package/Tests/UI/Components/Detail/YamlField.test.tsx +105 -0
  98. package/Tests/UI/Components/EntityDropdownLabelsBulkAdd.test.tsx +319 -0
  99. package/Tests/UI/Components/Forms/FormFieldYaml.test.tsx +231 -0
  100. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +1 -0
  101. package/Tests/UI/Components/Forms/ValidationYAML.test.ts +234 -0
  102. package/Tests/UI/Components/ModelTable/AttributeColumns.test.tsx +573 -0
  103. package/Tests/UI/Components/ModelTable/BaseModelTableAttributeColumns.test.tsx +824 -0
  104. package/Tests/UI/Components/ModelTable/ColumnCustomizationModalAddableColumns.test.tsx +707 -0
  105. package/Tests/UI/Components/SideOver.test.tsx +1 -1
  106. package/Tests/UI/Components/StatusPage/ResourceGridSelection.test.tsx +294 -0
  107. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +436 -0
  108. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +6 -2
  109. package/Tests/UI/Components/Toggle.test.tsx +25 -0
  110. package/Tests/UI/Components/YamlEditor.test.tsx +847 -0
  111. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +252 -0
  112. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +221 -0
  113. package/Tests/Utils/NetworkDiscovery/ScanNameUtil.test.ts +312 -0
  114. package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +468 -0
  115. package/Tests/Utils/Telemetry/LlmMetricQuery.test.ts +472 -0
  116. package/Types/Code/YamlSyntax.ts +396 -0
  117. package/Types/Exception/ExceptionCode.ts +1 -0
  118. package/Types/Exception/PayloadTooLargeException.ts +8 -0
  119. package/Types/Marketing/Attribution.ts +10 -0
  120. package/Types/Marketing/MarketingEvent.ts +4 -0
  121. package/Types/Monitor/MonitorCriteriaInstance.ts +116 -56
  122. package/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.ts +2 -1
  123. package/Types/SecurityEvent/DetectionFindingConstants.ts +11 -0
  124. package/Types/Telemetry/LlmMetricConventions.ts +124 -0
  125. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +3 -0
  126. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +10 -1
  127. package/UI/Components/CodeEditor/CodeEditor.tsx +241 -79
  128. package/UI/Components/CodeEditor/YamlEditor.tsx +369 -0
  129. package/UI/Components/Detail/Detail.tsx +10 -0
  130. package/UI/Components/EntityDropdown/EntityDropdown.tsx +33 -0
  131. package/UI/Components/Forms/Fields/FormField.tsx +31 -0
  132. package/UI/Components/Forms/Types/Field.ts +8 -0
  133. package/UI/Components/Forms/Types/FormFieldSchemaType.ts +1 -0
  134. package/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.ts +2 -0
  135. package/UI/Components/Forms/Validation.ts +46 -0
  136. package/UI/Components/Input/Input.tsx +3 -0
  137. package/UI/Components/ModelTable/AttributeColumns.tsx +321 -0
  138. package/UI/Components/ModelTable/BaseModelTable.tsx +263 -14
  139. package/UI/Components/ModelTable/Column.ts +8 -0
  140. package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +310 -0
  141. package/UI/Components/ModelTable/ColumnPreference.ts +13 -1
  142. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +2 -2
  143. package/UI/Components/SideOver/SideOver.tsx +5 -5
  144. package/UI/Components/StatusPage/ResourceGrid.tsx +82 -2
  145. package/UI/Components/StatusPage/ResourceList.tsx +197 -38
  146. package/UI/Components/Toggle/Toggle.tsx +9 -1
  147. package/UI/Components/Types/FieldType.ts +1 -0
  148. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +73 -1
  149. package/Utils/NetworkDiscovery/ScanNameUtil.ts +181 -0
  150. package/Utils/StatusPage/ResourceExplorer.ts +258 -0
  151. package/Utils/Telemetry/LlmMetricQuery.ts +204 -0
  152. package/build/dist/Models/DatabaseModels/DetectionRule.js +57 -0
  153. package/build/dist/Models/DatabaseModels/DetectionRule.js.map +1 -1
  154. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +27 -7
  155. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -1
  156. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +69 -0
  157. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  158. package/build/dist/Models/DatabaseModels/Project.js +76 -0
  159. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  160. package/build/dist/Models/DatabaseModels/User.js +76 -0
  161. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  162. package/build/dist/Server/API/TelemetryAPI.js +17 -0
  163. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  164. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js +21 -0
  165. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789400000000-AddNameToNetworkDeviceDiscoveryScan.js.map +1 -0
  166. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js +42 -0
  167. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789500000000-AddCampaignUtmFields.js.map +1 -0
  168. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js +22 -0
  169. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789512000000-AddDetectionRuleDistinctCountColumns.js.map +1 -0
  170. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  171. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  172. package/build/dist/Server/Middleware/GzipRequestBody.js +224 -0
  173. package/build/dist/Server/Middleware/GzipRequestBody.js.map +1 -0
  174. package/build/dist/Server/Middleware/MultipartFormData.js +71 -2
  175. package/build/dist/Server/Middleware/MultipartFormData.js.map +1 -1
  176. package/build/dist/Server/Services/DetectionRuleService.js +49 -4
  177. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  178. package/build/dist/Server/Services/Index.js +204 -0
  179. package/build/dist/Server/Services/Index.js.map +1 -1
  180. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +50 -0
  181. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  182. package/build/dist/Server/Services/NetworkDeviceService.js +124 -0
  183. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  184. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +68 -13
  185. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  186. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js +1 -1
  187. package/build/dist/Server/Services/OnCallDutyPolicyFeedService.js.map +1 -1
  188. package/build/dist/Server/Services/ProjectService.js +25 -45
  189. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  190. package/build/dist/Server/Services/SecurityEventService.js +57 -5
  191. package/build/dist/Server/Services/SecurityEventService.js.map +1 -1
  192. package/build/dist/Server/Services/StatusPageResourceService.js +140 -0
  193. package/build/dist/Server/Services/StatusPageResourceService.js.map +1 -1
  194. package/build/dist/Server/Services/TelemetryAttributeService.js +17 -0
  195. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  196. package/build/dist/Server/Services/UserService.js +2 -12
  197. package/build/dist/Server/Services/UserService.js.map +1 -1
  198. package/build/dist/Server/Types/Markdown.js +147 -61
  199. package/build/dist/Server/Types/Markdown.js.map +1 -1
  200. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +12 -4
  201. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -1
  202. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +12 -1
  203. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  204. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +246 -9
  205. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  206. package/build/dist/Server/Utils/Attribution.js.map +1 -1
  207. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +27 -7
  208. package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +42 -7
  210. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  211. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +18 -0
  212. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  213. package/build/dist/Server/Utils/StartServer.js +12 -21
  214. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  215. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js +76 -5
  216. package/build/dist/Server/Utils/Telemetry/LlmCostBudgetEvaluator.js.map +1 -1
  217. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js +102 -0
  218. package/build/dist/Server/Utils/Telemetry/LlmMetricSpend.js.map +1 -0
  219. package/build/dist/Types/Code/YamlSyntax.js +212 -0
  220. package/build/dist/Types/Code/YamlSyntax.js.map +1 -0
  221. package/build/dist/Types/Exception/ExceptionCode.js +1 -0
  222. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  223. package/build/dist/Types/Exception/PayloadTooLargeException.js +8 -0
  224. package/build/dist/Types/Exception/PayloadTooLargeException.js.map +1 -0
  225. package/build/dist/Types/Marketing/Attribution.js +10 -0
  226. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  227. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +112 -51
  228. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  229. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js +2 -1
  230. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationSeverityMapper.js.map +1 -1
  231. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +9 -0
  232. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  233. package/build/dist/Types/Telemetry/LlmMetricConventions.js +98 -0
  234. package/build/dist/Types/Telemetry/LlmMetricConventions.js.map +1 -0
  235. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  236. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  237. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +5 -2
  238. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  239. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +206 -76
  240. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  241. package/build/dist/UI/Components/CodeEditor/YamlEditor.js +201 -0
  242. package/build/dist/UI/Components/CodeEditor/YamlEditor.js.map +1 -0
  243. package/build/dist/UI/Components/Detail/Detail.js +9 -0
  244. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  245. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js +16 -1
  246. package/build/dist/UI/Components/EntityDropdown/EntityDropdown.js.map +1 -1
  247. package/build/dist/UI/Components/Forms/Fields/FormField.js +14 -1
  248. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  249. package/build/dist/UI/Components/Forms/Types/Field.js.map +1 -1
  250. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js +1 -0
  251. package/build/dist/UI/Components/Forms/Types/FormFieldSchemaType.js.map +1 -1
  252. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js +2 -0
  253. package/build/dist/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.js.map +1 -1
  254. package/build/dist/UI/Components/Forms/Validation.js +27 -0
  255. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  256. package/build/dist/UI/Components/Input/Input.js +1 -1
  257. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  258. package/build/dist/UI/Components/ModelTable/AttributeColumns.js +159 -0
  259. package/build/dist/UI/Components/ModelTable/AttributeColumns.js.map +1 -0
  260. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +179 -17
  261. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  262. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +133 -2
  263. package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -1
  264. package/build/dist/UI/Components/ModelTable/ColumnPreference.js +7 -1
  265. package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -1
  266. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +2 -2
  267. package/build/dist/UI/Components/SideOver/SideOver.js +5 -5
  268. package/build/dist/UI/Components/StatusPage/ResourceGrid.js +28 -2
  269. package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -1
  270. package/build/dist/UI/Components/StatusPage/ResourceList.js +87 -20
  271. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  272. package/build/dist/UI/Components/Toggle/Toggle.js +9 -1
  273. package/build/dist/UI/Components/Toggle/Toggle.js.map +1 -1
  274. package/build/dist/UI/Components/Types/FieldType.js +1 -0
  275. package/build/dist/UI/Components/Types/FieldType.js.map +1 -1
  276. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +37 -1
  277. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -1
  278. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js +120 -0
  279. package/build/dist/Utils/NetworkDiscovery/ScanNameUtil.js.map +1 -0
  280. package/build/dist/Utils/StatusPage/ResourceExplorer.js +171 -0
  281. package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -1
  282. package/build/dist/Utils/Telemetry/LlmMetricQuery.js +119 -0
  283. package/build/dist/Utils/Telemetry/LlmMetricQuery.js.map +1 -0
  284. package/jest.config.json +2 -0
  285. package/package.json +1 -1
  286. package/tsconfig.json +2 -1
  287. package/Tests/UI/Components/Toast.test.tsx +0 -197
  288. package/Tests/UI/Components/ToastStacking.test.tsx +0 -198
  289. package/UI/Components/Toast/Toast.tsx +0 -189
  290. package/UI/Components/Toast/ToastInit.tsx +0 -110
  291. package/build/dist/UI/Components/Toast/Toast.js +0 -123
  292. package/build/dist/UI/Components/Toast/Toast.js.map +0 -1
  293. package/build/dist/UI/Components/Toast/ToastInit.js +0 -53
  294. package/build/dist/UI/Components/Toast/ToastInit.js.map +0 -1
@@ -0,0 +1,800 @@
1
+ import MonitorService from "../../../Server/Services/MonitorService";
2
+ import NetworkDeviceLabelRuleEngineService from "../../../Server/Services/NetworkDeviceLabelRuleEngineService";
3
+ import NetworkDeviceOwnerRuleEngineService from "../../../Server/Services/NetworkDeviceOwnerRuleEngineService";
4
+ import NetworkDeviceService from "../../../Server/Services/NetworkDeviceService";
5
+ import NetworkSiteService from "../../../Server/Services/NetworkSiteService";
6
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
7
+ import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
8
+ import NetworkDeviceMonitoringMethod from "../../../Types/NetworkDevice/NetworkDeviceMonitoringMethod";
9
+ import ObjectID from "../../../Types/ObjectID";
10
+ import UpdateBy from "../../../Server/Types/Database/UpdateBy";
11
+ import { OnUpdate } from "../../../Server/Types/Database/Hooks";
12
+ import { afterEach, describe, expect, it } from "@jest/globals";
13
+
14
+ /*
15
+ * Contract under test — OneUptime/oneuptime#3392, "a correctly bound
16
+ * ping-only device is stuck on Pending".
17
+ *
18
+ * A monitor-backed NetworkDevice is never polled: no probe, no
19
+ * credentials, no walk. Its ONLY source of health is the Monitor bound to
20
+ * it, stamped onto `currentMonitorStatusId`, which the device list pill,
21
+ * the site rollup and the topology node all read.
22
+ *
23
+ * Before this fix the only writer of that column was
24
+ * `NetworkSiteService.onMonitorStatusChanged`, which runs when a monitor's
25
+ * status CHANGES. So the documented workflow — create a Ping monitor, set
26
+ * Monitoring Method to Monitor, bind the monitor — produced a device with
27
+ * an empty stamp, and it stayed empty until the monitor happened to go
28
+ * down. On a healthy device that is never, which is why the reporter's
29
+ * phone stayed on "Pending" while it answered every ping.
30
+ *
31
+ * What is pinned here:
32
+ *
33
+ * - binding stamps the monitor's CURRENT status immediately, on create
34
+ * and on update, rather than waiting for its next change,
35
+ * - the site chain is recomputed when the stamp moves, and only then,
36
+ * - unbinding (or switching the device back to SNMP) clears the stamp,
37
+ * so a ping monitor's verdict cannot outlive the binding,
38
+ * - an SNMP device's stamp — which comes from the Network Device monitor
39
+ * watching its walk, not from this column — is never touched,
40
+ * - the monitor lookup is scoped to the device's own project,
41
+ * - and a failure in any of it never escapes into the device update.
42
+ */
43
+
44
+ const PROJECT_ID: ObjectID = new ObjectID(
45
+ "22222222-2222-4222-8222-222222222222",
46
+ );
47
+ const OTHER_PROJECT_ID: ObjectID = new ObjectID(
48
+ "99999999-9999-4999-8999-999999999999",
49
+ );
50
+ const DEVICE_ID: ObjectID = new ObjectID(
51
+ "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
52
+ );
53
+ const SECOND_DEVICE_ID: ObjectID = new ObjectID(
54
+ "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
55
+ );
56
+ const SITE_ID: ObjectID = new ObjectID("11111111-1111-4111-8111-111111111111");
57
+ const MONITOR_ID: ObjectID = new ObjectID(
58
+ "44444444-4444-4444-8444-444444444444",
59
+ );
60
+ const OPERATIONAL_STATUS_ID: ObjectID = new ObjectID(
61
+ "55555555-5555-4555-8555-555555555555",
62
+ );
63
+ const OFFLINE_STATUS_ID: ObjectID = new ObjectID(
64
+ "66666666-6666-4666-8666-666666666666",
65
+ );
66
+
67
+ function fakeDevice(overrides: Record<string, unknown>): NetworkDevice {
68
+ return {
69
+ id: DEVICE_ID,
70
+ _id: DEVICE_ID.toString(),
71
+ projectId: PROJECT_ID,
72
+ ...overrides,
73
+ } as unknown as NetworkDevice;
74
+ }
75
+
76
+ function fakeMonitor(currentMonitorStatusId: ObjectID | undefined): Monitor {
77
+ return {
78
+ id: MONITOR_ID,
79
+ _id: MONITOR_ID.toString(),
80
+ currentMonitorStatusId: currentMonitorStatusId,
81
+ } as unknown as Monitor;
82
+ }
83
+
84
+ /*
85
+ * The whole write path this method owns, mocked at the three seams it
86
+ * touches: read the device, read its monitor, write the stamp (plus the
87
+ * rollup it triggers).
88
+ */
89
+ function mockDeviceAndMonitor(data: {
90
+ device: NetworkDevice | null;
91
+ monitor?: Monitor | null;
92
+ }): {
93
+ update: jest.SpyInstance;
94
+ rollup: jest.SpyInstance;
95
+ findMonitor: jest.SpyInstance;
96
+ } {
97
+ jest
98
+ .spyOn(NetworkDeviceService, "findOneById")
99
+ .mockResolvedValue(data.device);
100
+
101
+ const findMonitor: jest.SpyInstance = jest
102
+ .spyOn(MonitorService, "findOneBy")
103
+ .mockResolvedValue(data.monitor === undefined ? null : data.monitor);
104
+
105
+ const update: jest.SpyInstance = jest
106
+ .spyOn(NetworkDeviceService, "updateColumnsByIdWithoutHooks")
107
+ .mockResolvedValue(undefined as never);
108
+
109
+ const rollup: jest.SpyInstance = jest
110
+ .spyOn(NetworkSiteService, "recomputeRollupForSiteAndAncestors")
111
+ .mockResolvedValue(undefined as never);
112
+
113
+ return { update, rollup, findMonitor };
114
+ }
115
+
116
+ describe("NetworkDeviceService.refreshStampedMonitorStatus", () => {
117
+ afterEach(() => {
118
+ jest.restoreAllMocks();
119
+ });
120
+
121
+ /*
122
+ * The fix, stated once: the device in the issue, bound to a Ping monitor
123
+ * that is Operational and has been for weeks.
124
+ */
125
+ it("stamps the bound monitor's current status onto a monitor-backed device", async () => {
126
+ const { update } = mockDeviceAndMonitor({
127
+ device: fakeDevice({
128
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
129
+ monitorId: MONITOR_ID,
130
+ currentMonitorStatusId: undefined,
131
+ }),
132
+ monitor: fakeMonitor(OPERATIONAL_STATUS_ID),
133
+ });
134
+
135
+ await NetworkDeviceService.refreshStampedMonitorStatus({
136
+ deviceId: DEVICE_ID,
137
+ clearWhenNotMonitorBacked: false,
138
+ });
139
+
140
+ expect(update).toHaveBeenCalledTimes(1);
141
+ const args: any = update.mock.calls[0]![0];
142
+ expect(args.id.toString()).toBe(DEVICE_ID.toString());
143
+ expect(args.data.currentMonitorStatusId.toString()).toBe(
144
+ OPERATIONAL_STATUS_ID.toString(),
145
+ );
146
+ });
147
+
148
+ it("stamps an offline monitor just the same, so the device reads Down", async () => {
149
+ const { update } = mockDeviceAndMonitor({
150
+ device: fakeDevice({
151
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
152
+ monitorId: MONITOR_ID,
153
+ currentMonitorStatusId: OPERATIONAL_STATUS_ID,
154
+ }),
155
+ monitor: fakeMonitor(OFFLINE_STATUS_ID),
156
+ });
157
+
158
+ await NetworkDeviceService.refreshStampedMonitorStatus({
159
+ deviceId: DEVICE_ID,
160
+ clearWhenNotMonitorBacked: false,
161
+ });
162
+
163
+ expect(
164
+ update.mock.calls[0]![0].data.currentMonitorStatusId.toString(),
165
+ ).toBe(OFFLINE_STATUS_ID.toString());
166
+ });
167
+
168
+ /*
169
+ * The site card above the device shows a worst-of rollup over its
170
+ * devices' stamps, so a stamp that moves without recomputing it leaves
171
+ * the site and the device contradicting each other on screen.
172
+ */
173
+ it("refreshes the device's site chain when the stamp moves", async () => {
174
+ const { rollup } = mockDeviceAndMonitor({
175
+ device: fakeDevice({
176
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
177
+ monitorId: MONITOR_ID,
178
+ siteId: SITE_ID,
179
+ }),
180
+ monitor: fakeMonitor(OPERATIONAL_STATUS_ID),
181
+ });
182
+
183
+ await NetworkDeviceService.refreshStampedMonitorStatus({
184
+ deviceId: DEVICE_ID,
185
+ clearWhenNotMonitorBacked: false,
186
+ });
187
+
188
+ expect(rollup).toHaveBeenCalledTimes(1);
189
+ expect(rollup.mock.calls[0]![0].toString()).toBe(SITE_ID.toString());
190
+ });
191
+
192
+ it("skips the rollup for a device that belongs to no site", async () => {
193
+ const { update, rollup } = mockDeviceAndMonitor({
194
+ device: fakeDevice({
195
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
196
+ monitorId: MONITOR_ID,
197
+ }),
198
+ monitor: fakeMonitor(OPERATIONAL_STATUS_ID),
199
+ });
200
+
201
+ await NetworkDeviceService.refreshStampedMonitorStatus({
202
+ deviceId: DEVICE_ID,
203
+ clearWhenNotMonitorBacked: false,
204
+ });
205
+
206
+ expect(update).toHaveBeenCalledTimes(1);
207
+ expect(rollup).not.toHaveBeenCalled();
208
+ });
209
+
210
+ /*
211
+ * Idempotence is what makes this safe to call from every save and from
212
+ * the upgrade backfill: re-deriving a stamp that already agrees must
213
+ * cost nothing and, more importantly, must not churn the site rollups of
214
+ * a whole estate on every unrelated device edit.
215
+ */
216
+ it("writes nothing when the stamp already agrees", async () => {
217
+ const { update, rollup } = mockDeviceAndMonitor({
218
+ device: fakeDevice({
219
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
220
+ monitorId: MONITOR_ID,
221
+ siteId: SITE_ID,
222
+ currentMonitorStatusId: OPERATIONAL_STATUS_ID,
223
+ }),
224
+ monitor: fakeMonitor(OPERATIONAL_STATUS_ID),
225
+ });
226
+
227
+ await NetworkDeviceService.refreshStampedMonitorStatus({
228
+ deviceId: DEVICE_ID,
229
+ clearWhenNotMonitorBacked: false,
230
+ });
231
+
232
+ expect(update).not.toHaveBeenCalled();
233
+ expect(rollup).not.toHaveBeenCalled();
234
+ });
235
+
236
+ /*
237
+ * Discovery import creates ping-only hosts with no monitor on purpose —
238
+ * a subnet sweep has nothing to bind them to yet. "Pending" is the true
239
+ * answer for those, so nothing is invented for them.
240
+ */
241
+ it("leaves an already-empty stamp alone when no monitor is bound", async () => {
242
+ const { update, findMonitor } = mockDeviceAndMonitor({
243
+ device: fakeDevice({
244
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
245
+ monitorId: undefined,
246
+ }),
247
+ });
248
+
249
+ await NetworkDeviceService.refreshStampedMonitorStatus({
250
+ deviceId: DEVICE_ID,
251
+ clearWhenNotMonitorBacked: false,
252
+ });
253
+
254
+ expect(findMonitor).not.toHaveBeenCalled();
255
+ expect(update).not.toHaveBeenCalled();
256
+ });
257
+
258
+ /*
259
+ * ...and the same device once its monitor is unbound. The old verdict
260
+ * must not survive the binding that produced it.
261
+ */
262
+ it("clears the stamp when the monitor is unbound", async () => {
263
+ const { update } = mockDeviceAndMonitor({
264
+ device: fakeDevice({
265
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
266
+ monitorId: undefined,
267
+ currentMonitorStatusId: OPERATIONAL_STATUS_ID,
268
+ }),
269
+ });
270
+
271
+ await NetworkDeviceService.refreshStampedMonitorStatus({
272
+ deviceId: DEVICE_ID,
273
+ clearWhenNotMonitorBacked: false,
274
+ });
275
+
276
+ expect(update).toHaveBeenCalledTimes(1);
277
+ expect(update.mock.calls[0]![0].data.currentMonitorStatusId).toBeNull();
278
+ });
279
+
280
+ /*
281
+ * A bound monitor that has somehow never had a status is the same case:
282
+ * there is nothing to adopt, and a stale stamp is worse than none.
283
+ */
284
+ it("clears the stamp when the bound monitor has no status of its own", async () => {
285
+ const { update } = mockDeviceAndMonitor({
286
+ device: fakeDevice({
287
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
288
+ monitorId: MONITOR_ID,
289
+ currentMonitorStatusId: OPERATIONAL_STATUS_ID,
290
+ }),
291
+ monitor: fakeMonitor(undefined),
292
+ });
293
+
294
+ await NetworkDeviceService.refreshStampedMonitorStatus({
295
+ deviceId: DEVICE_ID,
296
+ clearWhenNotMonitorBacked: false,
297
+ });
298
+
299
+ expect(update.mock.calls[0]![0].data.currentMonitorStatusId).toBeNull();
300
+ });
301
+
302
+ it("clears the stamp when the bound monitor is gone", async () => {
303
+ const { update } = mockDeviceAndMonitor({
304
+ device: fakeDevice({
305
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
306
+ monitorId: MONITOR_ID,
307
+ currentMonitorStatusId: OPERATIONAL_STATUS_ID,
308
+ }),
309
+ monitor: null,
310
+ });
311
+
312
+ await NetworkDeviceService.refreshStampedMonitorStatus({
313
+ deviceId: DEVICE_ID,
314
+ clearWhenNotMonitorBacked: false,
315
+ });
316
+
317
+ expect(update.mock.calls[0]![0].data.currentMonitorStatusId).toBeNull();
318
+ });
319
+
320
+ /*
321
+ * The monitorId FK only requires the Monitor row to exist, not that it
322
+ * belongs to the device's project — the same hole the create/update
323
+ * guards close. Reading it back through a project-scoped query is what
324
+ * stops another tenant's status being stamped here.
325
+ */
326
+ it("looks the monitor up inside the device's own project", async () => {
327
+ const { findMonitor } = mockDeviceAndMonitor({
328
+ device: fakeDevice({
329
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
330
+ monitorId: MONITOR_ID,
331
+ }),
332
+ monitor: fakeMonitor(OPERATIONAL_STATUS_ID),
333
+ });
334
+
335
+ await NetworkDeviceService.refreshStampedMonitorStatus({
336
+ deviceId: DEVICE_ID,
337
+ clearWhenNotMonitorBacked: false,
338
+ });
339
+
340
+ const query: any = findMonitor.mock.calls[0]![0].query;
341
+ expect(query._id.toString()).toBe(MONITOR_ID.toString());
342
+ expect(query.projectId.toString()).toBe(PROJECT_ID.toString());
343
+ expect(query.projectId.toString()).not.toBe(OTHER_PROJECT_ID.toString());
344
+ });
345
+
346
+ /*
347
+ * An SNMP device's stamp comes from the Network Device monitor watching
348
+ * its walk, and that binding lives in the monitor's step data rather
349
+ * than in this column. Re-deriving it from `monitorId` would wipe a
350
+ * legitimate status the walk pipeline put there.
351
+ */
352
+ it("does not touch an SNMP device by default", async () => {
353
+ const { update, findMonitor } = mockDeviceAndMonitor({
354
+ device: fakeDevice({
355
+ monitoringMethod: NetworkDeviceMonitoringMethod.Snmp,
356
+ monitorId: MONITOR_ID,
357
+ currentMonitorStatusId: OPERATIONAL_STATUS_ID,
358
+ }),
359
+ monitor: fakeMonitor(OFFLINE_STATUS_ID),
360
+ });
361
+
362
+ await NetworkDeviceService.refreshStampedMonitorStatus({
363
+ deviceId: DEVICE_ID,
364
+ clearWhenNotMonitorBacked: false,
365
+ });
366
+
367
+ expect(findMonitor).not.toHaveBeenCalled();
368
+ expect(update).not.toHaveBeenCalled();
369
+ });
370
+
371
+ /*
372
+ * A device created before the column existed holds NULL, which reads as
373
+ * SNMP — and must behave as one.
374
+ */
375
+ it("treats a device with no monitoring method as SNMP", async () => {
376
+ const { update } = mockDeviceAndMonitor({
377
+ device: fakeDevice({
378
+ monitoringMethod: undefined,
379
+ monitorId: MONITOR_ID,
380
+ currentMonitorStatusId: OPERATIONAL_STATUS_ID,
381
+ }),
382
+ monitor: fakeMonitor(OFFLINE_STATUS_ID),
383
+ });
384
+
385
+ await NetworkDeviceService.refreshStampedMonitorStatus({
386
+ deviceId: DEVICE_ID,
387
+ clearWhenNotMonitorBacked: false,
388
+ });
389
+
390
+ expect(update).not.toHaveBeenCalled();
391
+ });
392
+
393
+ /*
394
+ * The one write that DOES clear an SNMP device: the one that just moved
395
+ * it off monitor-backed. The ping monitor's verdict outliving the
396
+ * binding matters because a stamped status beats reachability in
397
+ * DeviceHealthStateUtil — it would keep deciding the site rollup of a
398
+ * device that is now being walked.
399
+ */
400
+ it("clears the stamp when a device is switched back to SNMP", async () => {
401
+ const { update, rollup } = mockDeviceAndMonitor({
402
+ device: fakeDevice({
403
+ monitoringMethod: NetworkDeviceMonitoringMethod.Snmp,
404
+ monitorId: MONITOR_ID,
405
+ siteId: SITE_ID,
406
+ currentMonitorStatusId: OPERATIONAL_STATUS_ID,
407
+ }),
408
+ monitor: fakeMonitor(OPERATIONAL_STATUS_ID),
409
+ });
410
+
411
+ await NetworkDeviceService.refreshStampedMonitorStatus({
412
+ deviceId: DEVICE_ID,
413
+ clearWhenNotMonitorBacked: true,
414
+ });
415
+
416
+ expect(update).toHaveBeenCalledTimes(1);
417
+ expect(update.mock.calls[0]![0].data.currentMonitorStatusId).toBeNull();
418
+ expect(rollup).toHaveBeenCalledTimes(1);
419
+ });
420
+
421
+ it("does nothing for a device the update never actually matched", async () => {
422
+ const { update } = mockDeviceAndMonitor({ device: null });
423
+
424
+ await NetworkDeviceService.refreshStampedMonitorStatus({
425
+ deviceId: DEVICE_ID,
426
+ clearWhenNotMonitorBacked: true,
427
+ });
428
+
429
+ expect(update).not.toHaveBeenCalled();
430
+ });
431
+
432
+ it("does nothing for a device row carrying no project", async () => {
433
+ const { update } = mockDeviceAndMonitor({
434
+ device: {
435
+ id: DEVICE_ID,
436
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
437
+ monitorId: MONITOR_ID,
438
+ } as unknown as NetworkDevice,
439
+ });
440
+
441
+ await NetworkDeviceService.refreshStampedMonitorStatus({
442
+ deviceId: DEVICE_ID,
443
+ clearWhenNotMonitorBacked: true,
444
+ });
445
+
446
+ expect(update).not.toHaveBeenCalled();
447
+ });
448
+
449
+ /*
450
+ * The columns the method reads. A select that drops one of them makes
451
+ * this silently wrong rather than loudly broken — a missing
452
+ * `monitoringMethod` reads as SNMP and skips every monitor-backed
453
+ * device, which is the bug all over again.
454
+ */
455
+ it("selects every column its decision depends on", async () => {
456
+ const findDevice: jest.SpyInstance = jest
457
+ .spyOn(NetworkDeviceService, "findOneById")
458
+ .mockResolvedValue(null);
459
+
460
+ await NetworkDeviceService.refreshStampedMonitorStatus({
461
+ deviceId: DEVICE_ID,
462
+ clearWhenNotMonitorBacked: false,
463
+ });
464
+
465
+ const select: any = findDevice.mock.calls[0]![0].select;
466
+ expect(select.monitoringMethod).toBe(true);
467
+ expect(select.monitorId).toBe(true);
468
+ expect(select.currentMonitorStatusId).toBe(true);
469
+ expect(select.projectId).toBe(true);
470
+ expect(select.siteId).toBe(true);
471
+ });
472
+ });
473
+
474
+ /*
475
+ * The hook that turns "the operator pressed Save" into a re-stamp. The
476
+ * dashboard's Device Details card posts `monitoringMethod` and the
477
+ * `monitor` RELATION together; server-side callers write the `monitorId`
478
+ * COLUMN. Both spellings have to fire, or the fix reaches only half the
479
+ * writes (OneUptime/oneuptime#2940 is the same lesson for sites).
480
+ */
481
+ describe("NetworkDeviceService.onUpdateSuccess re-stamps a changed binding", () => {
482
+ afterEach(() => {
483
+ jest.restoreAllMocks();
484
+ });
485
+
486
+ function runOnUpdateSuccess(
487
+ data: Record<string, unknown>,
488
+ updatedItemIds: Array<ObjectID> = [DEVICE_ID],
489
+ ): Promise<OnUpdate<NetworkDevice>> {
490
+ const onUpdate: OnUpdate<NetworkDevice> = {
491
+ updateBy: {
492
+ query: {},
493
+ data: data,
494
+ props: { isRoot: true },
495
+ } as unknown as UpdateBy<NetworkDevice>,
496
+ carryForward: null,
497
+ };
498
+
499
+ return (NetworkDeviceService as any).onUpdateSuccess(
500
+ onUpdate,
501
+ updatedItemIds,
502
+ );
503
+ }
504
+
505
+ it("re-stamps when the monitoring method is written", async () => {
506
+ const refresh: jest.SpyInstance = jest
507
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
508
+ .mockResolvedValue(undefined as never);
509
+
510
+ await runOnUpdateSuccess({
511
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
512
+ });
513
+
514
+ expect(refresh).toHaveBeenCalledTimes(1);
515
+ expect(refresh.mock.calls[0]![0].deviceId.toString()).toBe(
516
+ DEVICE_ID.toString(),
517
+ );
518
+ });
519
+
520
+ it("re-stamps when the monitor RELATION is written, as the dashboard posts it", async () => {
521
+ const refresh: jest.SpyInstance = jest
522
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
523
+ .mockResolvedValue(undefined as never);
524
+
525
+ await runOnUpdateSuccess({ monitor: { _id: MONITOR_ID.toString() } });
526
+
527
+ expect(refresh).toHaveBeenCalledTimes(1);
528
+ });
529
+
530
+ it("re-stamps when the monitorId COLUMN is written", async () => {
531
+ const refresh: jest.SpyInstance = jest
532
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
533
+ .mockResolvedValue(undefined as never);
534
+
535
+ await runOnUpdateSuccess({ monitorId: MONITOR_ID });
536
+
537
+ expect(refresh).toHaveBeenCalledTimes(1);
538
+ });
539
+
540
+ /*
541
+ * The exact save from the issue: Monitoring Method and Monitor set in one
542
+ * submit, which is how the Device Details card posts them.
543
+ */
544
+ it("re-stamps the reported save, which writes both at once", async () => {
545
+ const refresh: jest.SpyInstance = jest
546
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
547
+ .mockResolvedValue(undefined as never);
548
+
549
+ await runOnUpdateSuccess({
550
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
551
+ monitor: { _id: MONITOR_ID.toString() },
552
+ deviceRole: "IP phone",
553
+ });
554
+
555
+ expect(refresh).toHaveBeenCalledTimes(1);
556
+ expect(refresh.mock.calls[0]![0].clearWhenNotMonitorBacked).toBe(true);
557
+ });
558
+
559
+ /*
560
+ * A method write is the only thing allowed to clear an SNMP device's
561
+ * stamp, because it is the only write that can have just moved the
562
+ * device off monitor-backed. Binding alone must not.
563
+ */
564
+ it("only asks to clear on a method write", async () => {
565
+ const refresh: jest.SpyInstance = jest
566
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
567
+ .mockResolvedValue(undefined as never);
568
+
569
+ await runOnUpdateSuccess({ monitorId: MONITOR_ID });
570
+
571
+ expect(refresh.mock.calls[0]![0].clearWhenNotMonitorBacked).toBe(false);
572
+ });
573
+
574
+ it("re-stamps every device a bulk update touched", async () => {
575
+ const refresh: jest.SpyInstance = jest
576
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
577
+ .mockResolvedValue(undefined as never);
578
+
579
+ await runOnUpdateSuccess(
580
+ { monitoringMethod: NetworkDeviceMonitoringMethod.Monitor },
581
+ [DEVICE_ID, SECOND_DEVICE_ID],
582
+ );
583
+
584
+ expect(refresh).toHaveBeenCalledTimes(2);
585
+ expect(
586
+ refresh.mock.calls.map((call: Array<any>) => {
587
+ return call[0].deviceId.toString();
588
+ }),
589
+ ).toEqual([DEVICE_ID.toString(), SECOND_DEVICE_ID.toString()]);
590
+ });
591
+
592
+ /*
593
+ * onUpdateSuccess runs even when the permission-scoped UPDATE matched
594
+ * nothing, and a re-stamp of a device the caller could not touch would
595
+ * be a write they were not allowed to make.
596
+ */
597
+ it("re-stamps nothing when the update matched no rows", async () => {
598
+ const refresh: jest.SpyInstance = jest
599
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
600
+ .mockResolvedValue(undefined as never);
601
+
602
+ await runOnUpdateSuccess(
603
+ { monitoringMethod: NetworkDeviceMonitoringMethod.Monitor },
604
+ [],
605
+ );
606
+
607
+ expect(refresh).not.toHaveBeenCalled();
608
+ });
609
+
610
+ /*
611
+ * The SNMP walk rewrites sysName, lastSeenAt and the interface counts on
612
+ * every single poll of every single device. Re-deriving a stamp on those
613
+ * would put a device read and a monitor read into the hot ingest path
614
+ * for an entire fleet.
615
+ */
616
+ it("does not re-stamp on a write that leaves the binding alone", async () => {
617
+ const refresh: jest.SpyInstance = jest
618
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
619
+ .mockResolvedValue(undefined as never);
620
+
621
+ await runOnUpdateSuccess({ sysName: "un0661voipcp01", interfacesDown: 2 });
622
+
623
+ expect(refresh).not.toHaveBeenCalled();
624
+ });
625
+
626
+ /*
627
+ * Status maintenance is best-effort, exactly like the site maintenance
628
+ * beside it: the device update has already committed, and failing the
629
+ * caller's request over a stamp would turn a cosmetic problem into a
630
+ * failed save.
631
+ */
632
+ it("never lets a re-stamp failure escape into the update", async () => {
633
+ jest
634
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
635
+ .mockRejectedValue(new Error("monitor lookup exploded") as never);
636
+
637
+ await expect(
638
+ runOnUpdateSuccess({
639
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
640
+ }),
641
+ ).resolves.toBeDefined();
642
+ });
643
+
644
+ /*
645
+ * ...and the two halves are independent. A rollup failure in the site
646
+ * maintenance below must not cost the device its stamp, which is why
647
+ * they sit in separate try blocks.
648
+ */
649
+ it("still re-stamps when the site maintenance beside it fails", async () => {
650
+ const refresh: jest.SpyInstance = jest
651
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
652
+ .mockResolvedValue(undefined as never);
653
+
654
+ jest
655
+ .spyOn(NetworkSiteService, "recomputeRollupForSiteAndAncestors")
656
+ .mockRejectedValue(new Error("rollup exploded") as never);
657
+
658
+ await expect(
659
+ runOnUpdateSuccess({
660
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
661
+ siteId: SITE_ID,
662
+ }),
663
+ ).resolves.toBeDefined();
664
+
665
+ expect(refresh).toHaveBeenCalledTimes(1);
666
+ });
667
+ });
668
+
669
+ /*
670
+ * The create side. A device can arrive monitor-backed and already bound —
671
+ * the "Add Device" form asks for the monitor in the same submit, and the
672
+ * discovery review screen imports ping-only hosts in bulk — so waiting for
673
+ * a later edit to stamp it would leave a freshly created device on
674
+ * "Pending" for exactly the same reason the reported one was.
675
+ *
676
+ * The chain is detached from the create response on purpose (a rule
677
+ * failure must never fail the device write), so these drain the microtask
678
+ * queue the way the rule-chain suite does.
679
+ */
680
+ describe("NetworkDeviceService.onCreateSuccess stamps a device created bound", () => {
681
+ afterEach(() => {
682
+ jest.restoreAllMocks();
683
+ });
684
+
685
+ function mockRuleChain(): void {
686
+ jest
687
+ .spyOn(NetworkDeviceLabelRuleEngineService, "applyRulesToNetworkDevice")
688
+ .mockResolvedValue(undefined as never);
689
+ jest
690
+ .spyOn(NetworkDeviceOwnerRuleEngineService, "applyRulesToNetworkDevice")
691
+ .mockResolvedValue(undefined as never);
692
+ jest
693
+ .spyOn(NetworkDeviceService, "applySiteAssignmentRulesToDevice")
694
+ .mockResolvedValue(undefined as never);
695
+ jest
696
+ .spyOn(NetworkSiteService, "recomputeRollupForSiteAndAncestors")
697
+ .mockResolvedValue(undefined as never);
698
+ }
699
+
700
+ async function runOnCreateSuccess(createdItem: NetworkDevice): Promise<void> {
701
+ await (NetworkDeviceService as any).onCreateSuccess(
702
+ { createBy: { data: {}, props: {} }, carryForward: null },
703
+ createdItem,
704
+ );
705
+
706
+ await new Promise((resolve: (value: unknown) => void) => {
707
+ setTimeout(resolve, 0);
708
+ });
709
+ }
710
+
711
+ it("stamps a device created monitor-backed", async () => {
712
+ mockRuleChain();
713
+ const refresh: jest.SpyInstance = jest
714
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
715
+ .mockResolvedValue(undefined as never);
716
+
717
+ await runOnCreateSuccess(
718
+ fakeDevice({
719
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
720
+ monitorId: MONITOR_ID,
721
+ }),
722
+ );
723
+
724
+ expect(refresh).toHaveBeenCalledTimes(1);
725
+ expect(refresh.mock.calls[0]![0].deviceId.toString()).toBe(
726
+ DEVICE_ID.toString(),
727
+ );
728
+ // Nothing to clear on a row that has only just been written.
729
+ expect(refresh.mock.calls[0]![0].clearWhenNotMonitorBacked).toBe(false);
730
+ });
731
+
732
+ /*
733
+ * Discovery import creates ping-only hosts with no monitor bound. Those
734
+ * still go through, and the method itself decides there is nothing to
735
+ * stamp — which keeps the "did we call it" decision in one place.
736
+ */
737
+ it("stamps a monitor-backed device created with nothing bound", async () => {
738
+ mockRuleChain();
739
+ const refresh: jest.SpyInstance = jest
740
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
741
+ .mockResolvedValue(undefined as never);
742
+
743
+ await runOnCreateSuccess(
744
+ fakeDevice({ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor }),
745
+ );
746
+
747
+ expect(refresh).toHaveBeenCalledTimes(1);
748
+ });
749
+
750
+ /*
751
+ * Every SNMP device ever created goes through this hook, including a
752
+ * whole subnet at a time on discovery import. None of them has anything
753
+ * to stamp, so none of them pays for a device read and a monitor read.
754
+ */
755
+ it("does not touch an SNMP device", async () => {
756
+ mockRuleChain();
757
+ const refresh: jest.SpyInstance = jest
758
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
759
+ .mockResolvedValue(undefined as never);
760
+
761
+ await runOnCreateSuccess(
762
+ fakeDevice({ monitoringMethod: NetworkDeviceMonitoringMethod.Snmp }),
763
+ );
764
+
765
+ expect(refresh).not.toHaveBeenCalled();
766
+ });
767
+
768
+ it("does not touch a device created with no monitoring method", async () => {
769
+ mockRuleChain();
770
+ const refresh: jest.SpyInstance = jest
771
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
772
+ .mockResolvedValue(undefined as never);
773
+
774
+ await runOnCreateSuccess(fakeDevice({}));
775
+
776
+ expect(refresh).not.toHaveBeenCalled();
777
+ });
778
+
779
+ /*
780
+ * Detached, and it has to stay that way: the device row is already
781
+ * committed and its id already returned to the caller by the time this
782
+ * runs, so a monitor lookup that throws must not surface as a failed
783
+ * create.
784
+ */
785
+ it("never lets a stamp failure escape the create", async () => {
786
+ mockRuleChain();
787
+ jest
788
+ .spyOn(NetworkDeviceService, "refreshStampedMonitorStatus")
789
+ .mockRejectedValue(new Error("monitor lookup exploded") as never);
790
+
791
+ await expect(
792
+ runOnCreateSuccess(
793
+ fakeDevice({
794
+ monitoringMethod: NetworkDeviceMonitoringMethod.Monitor,
795
+ monitorId: MONITOR_ID,
796
+ }),
797
+ ),
798
+ ).resolves.toBeUndefined();
799
+ });
800
+ });