@oneuptime/common 12.0.13 → 12.0.15

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 (561) hide show
  1. package/Models/DatabaseModels/GlobalOidc.ts +20 -0
  2. package/Models/DatabaseModels/GlobalSso.ts +20 -0
  3. package/Models/DatabaseModels/MarketingConversion.ts +19 -9
  4. package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +46 -0
  5. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +14 -0
  6. package/Models/DatabaseModels/StatusPage.ts +7 -4
  7. package/Models/DatabaseModels/UserCall.ts +94 -3
  8. package/Models/DatabaseModels/UserEmail.ts +94 -3
  9. package/Models/DatabaseModels/UserIncomingCallNumber.ts +94 -3
  10. package/Models/DatabaseModels/UserPush.ts +37 -0
  11. package/Models/DatabaseModels/UserSMS.ts +94 -3
  12. package/Models/DatabaseModels/UserWhatsApp.ts +93 -2
  13. package/Server/API/BaseAPI.ts +79 -14
  14. package/Server/API/StatusPageAPI.ts +29 -4
  15. package/Server/API/UserCallAPI.ts +53 -45
  16. package/Server/API/UserEmailAPI.ts +53 -44
  17. package/Server/API/UserIncomingCallNumberAPI.ts +48 -38
  18. package/Server/API/UserNotificationMethodAdminAPI.ts +481 -0
  19. package/Server/API/UserPushAPI.ts +126 -2
  20. package/Server/API/UserSmsAPI.ts +53 -44
  21. package/Server/API/UserWhatsAppAPI.ts +53 -50
  22. package/Server/EnvironmentConfig.ts +7 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.ts +154 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.ts +17 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.ts +45 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.ts +19 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.ts +40 -0
  28. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
  29. package/Server/Middleware/SlackAuthorization.ts +43 -6
  30. package/Server/Middleware/UserAuthorization.ts +236 -26
  31. package/Server/Middleware/VerificationCodeRateLimit.ts +586 -0
  32. package/Server/Middleware/WhatsAppAuthorization.ts +43 -3
  33. package/Server/Services/AnalyticsDatabaseService.ts +169 -0
  34. package/Server/Services/DatabaseService.ts +101 -0
  35. package/Server/Services/GlobalOidcProjectService.ts +126 -1
  36. package/Server/Services/GlobalOidcService.ts +132 -0
  37. package/Server/Services/GlobalSsoProjectService.ts +126 -1
  38. package/Server/Services/GlobalSsoService.ts +132 -0
  39. package/Server/Services/LogAggregationService.ts +5 -0
  40. package/Server/Services/PushNotificationService.ts +127 -12
  41. package/Server/Services/TeamMemberService.ts +53 -13
  42. package/Server/Services/UserCallService.ts +55 -24
  43. package/Server/Services/UserEmailService.ts +83 -17
  44. package/Server/Services/UserIncomingCallNumberService.ts +49 -85
  45. package/Server/Services/UserNotificationMethodAdminService.ts +1527 -0
  46. package/Server/Services/UserNotificationRuleService.ts +26 -0
  47. package/Server/Services/UserPushService.ts +89 -0
  48. package/Server/Services/UserSmsService.ts +50 -17
  49. package/Server/Services/UserWhatsAppService.ts +48 -15
  50. package/Server/Types/Database/QueryHelper.ts +30 -0
  51. package/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.ts +59 -0
  52. package/Server/Utils/ChannelVerification.ts +510 -0
  53. package/Server/Utils/CronTab.ts +73 -0
  54. package/Server/Utils/GlobalSsoAuthorization.ts +175 -0
  55. package/Server/Utils/Marketing/ConversionUploadProvider.ts +35 -5
  56. package/Server/Utils/Marketing/Providers/GoogleAds.ts +1 -1
  57. package/Server/Utils/Marketing/Providers/LinkedIn.ts +1 -1
  58. package/Server/Utils/Marketing/Providers/Meta.ts +1 -1
  59. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +1 -1
  60. package/Server/Utils/Marketing/Providers/Reddit.ts +1 -1
  61. package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +48 -33
  62. package/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.ts +35 -30
  63. package/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts +33 -28
  64. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +475 -47
  65. package/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.ts +35 -30
  66. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +47 -44
  67. package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +33 -28
  68. package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +69 -40
  69. package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +35 -29
  70. package/Server/Utils/Monitor/MonitorAlert.ts +24 -21
  71. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +8 -0
  72. package/Server/Utils/Monitor/MonitorIncident.ts +21 -18
  73. package/Server/Utils/Monitor/MonitorResource.ts +161 -21
  74. package/Server/Utils/Monitor/MonitorResourceContext.ts +107 -0
  75. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +442 -0
  76. package/Server/Utils/Monitor/SeriesResourceLabels.ts +3 -3
  77. package/Server/Utils/Monitor/SeriesResourceLinker.ts +96 -28
  78. package/Server/Utils/StatusPageContentSecurityPolicy.ts +56 -0
  79. package/Server/Utils/StatusPageCustomizationAccess.ts +117 -0
  80. package/Server/Utils/TotpAuth.ts +137 -9
  81. package/Server/Utils/UserRegistrationToken.ts +182 -0
  82. package/Server/Utils/VerificationCode.ts +134 -0
  83. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +173 -94
  84. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +1151 -0
  85. package/Tests/App/Dashboard/AdminUserOnCallPages.test.tsx +905 -0
  86. package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +338 -0
  87. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +1 -1
  88. package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +28 -0
  89. package/Tests/Models/UserPushCriticalAlertColumn.test.ts +172 -0
  90. package/Tests/Server/API/BaseAPIGetListParallel.test.ts +519 -0
  91. package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +543 -0
  92. package/Tests/Server/API/StatusPageCustomizationOrigin.test.ts +348 -0
  93. package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +108 -18
  94. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +963 -0
  95. package/Tests/Server/API/UserPushCriticalAlertsApi.test.ts +114 -0
  96. package/Tests/Server/API/UserSmsApi.test.ts +107 -14
  97. package/Tests/Server/API/UserTotpAuthAPI.test.ts +427 -0
  98. package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +265 -0
  99. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +227 -0
  100. package/Tests/Server/Middleware/SlackAuthorization.test.ts +149 -0
  101. package/Tests/Server/Middleware/UserAuthorization.test.ts +27 -13
  102. package/Tests/Server/Middleware/UserAuthorizationGlobalSsoGovernance.test.ts +1455 -0
  103. package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +91 -2
  104. package/Tests/Server/Middleware/VerificationCodeRateLimit.test.ts +775 -0
  105. package/Tests/Server/Middleware/WhatsAppAuthorization.test.ts +233 -0
  106. package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +10 -62
  107. package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +33 -13
  108. package/Tests/Server/Services/AnalyticsDatabaseSortKeyBoundary.test.ts +658 -0
  109. package/Tests/Server/Services/CriticalOnCallAlertDelivery.test.ts +458 -0
  110. package/Tests/Server/Services/CriticalPushAlertPayload.test.ts +301 -0
  111. package/Tests/Server/Services/DatabaseServiceAtomicIncrement.test.ts +143 -0
  112. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1 -0
  113. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +21 -8
  114. package/Tests/Server/Services/GlobalSsoProviderAuthorization.test.ts +1242 -0
  115. package/Tests/Server/Services/LogAggregationScanMemory.test.ts +421 -0
  116. package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
  117. package/Tests/Server/Services/MonitorResourceProbeAgreementHydration.test.ts +459 -0
  118. package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +10 -0
  119. package/Tests/Server/Services/NotificationChannelCodeIssuance.test.ts +401 -0
  120. package/Tests/Server/Services/PushNotificationDeliveryOptions.test.ts +204 -0
  121. package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +21 -1
  122. package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +290 -0
  123. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +948 -0
  124. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1 -0
  125. package/Tests/Server/Services/UserPushCriticalAlertToggle.test.ts +255 -0
  126. package/Tests/Server/Services/UserTotpAuthEnrolment.test.ts +218 -0
  127. package/Tests/Server/TestingUtils/AuthenticatorApp.ts +248 -0
  128. package/Tests/Server/Types/AnalyticsDatabase/AggregateUtilBucketTimestamp.test.ts +416 -0
  129. package/Tests/Server/Types/Database/Permissions/NotificationChannelVerificationColumns.test.ts +135 -0
  130. package/Tests/Server/Types/Database/Permissions/OnCallScheduleRelationSelect.test.ts +504 -0
  131. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAnyOf.test.ts +91 -0
  132. package/Tests/Server/Types/Database/QueryHelperManyToManyAndEmptyValues.test.ts +551 -0
  133. package/Tests/Server/Types/Database/SelectUtil.test.ts +348 -0
  134. package/Tests/Server/Types/Workflow/Components/JsonToText.test.ts +384 -0
  135. package/Tests/Server/Types/Workflow/Components/MergeJson.test.ts +403 -0
  136. package/Tests/Server/Utils/Browser.test.ts +162 -0
  137. package/Tests/Server/Utils/ChannelVerification.test.ts +689 -0
  138. package/Tests/Server/Utils/CronTab.test.ts +116 -0
  139. package/Tests/Server/Utils/Express.test.ts +371 -0
  140. package/Tests/Server/Utils/GlobalSSOToken.test.ts +581 -0
  141. package/Tests/Server/Utils/GlobalSsoAuthorization.test.ts +1401 -0
  142. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +254 -0
  143. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +1 -1
  144. package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaEvaluateOverTime.test.ts +508 -0
  145. package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaPortTimings.test.ts +30 -9
  146. package/Tests/Server/Utils/Monitor/Criteria/CustomCodeMonitorCriteria.test.ts +707 -0
  147. package/Tests/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.test.ts +698 -0
  148. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +987 -0
  149. package/Tests/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.test.ts +916 -0
  150. package/Tests/Server/Utils/Monitor/Criteria/IncomingEmailBodyCriteria.test.ts +243 -0
  151. package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestBodyCriteria.test.ts +278 -0
  152. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +382 -0
  153. package/Tests/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.test.ts +1219 -0
  154. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +160 -20
  155. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +13 -8
  156. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +162 -18
  157. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +349 -0
  158. package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +86 -0
  159. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +690 -0
  160. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +7 -2
  161. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +92 -18
  162. package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +267 -0
  163. package/Tests/Server/Utils/StatusPageContentSecurityPolicy.test.ts +136 -0
  164. package/Tests/Server/Utils/StatusPageCustomizationAccess.test.ts +223 -0
  165. package/Tests/Server/Utils/Telemetry/OneuptimeLabel.test.ts +439 -0
  166. package/Tests/Server/Utils/TotpAuth.test.ts +719 -0
  167. package/Tests/Server/Utils/UserRegistrationToken.test.ts +322 -0
  168. package/Tests/Server/Utils/VerificationCode.test.ts +296 -0
  169. package/Tests/Types/AutoRemediation/AutoRemediationSuggestionStatus.test.ts +192 -0
  170. package/Tests/Types/Call/CallRequest.test.ts +189 -0
  171. package/Tests/Types/Dashboard/DashboardSize.test.ts +226 -0
  172. package/Tests/Types/Incident/IncidentSlaStatus.test.ts +191 -0
  173. package/Tests/Types/Metrics/MetricDashboardMetricType.test.ts +278 -0
  174. package/Tests/Types/Metrics/MetricPipelineRuleType.test.ts +517 -0
  175. package/Tests/Types/Monitor/IncomingMonitorDefaultCriteria.test.ts +470 -0
  176. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +6 -4
  177. package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +165 -0
  178. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +262 -5
  179. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +1 -1
  180. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +1 -1
  181. package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +905 -0
  182. package/Tests/Types/NetworkDevice/NetworkDeviceLinkRuleScope.test.ts +215 -0
  183. package/Tests/Types/Service/ServiceLanguage.test.ts +255 -0
  184. package/Tests/Types/TextRandomGeneration.test.ts +211 -0
  185. package/Tests/UI/Components/Button.test.tsx +175 -1
  186. package/Tests/UI/Components/CodeBlockLanguages.test.tsx +236 -0
  187. package/Tests/UI/Components/CommandPalette/CommandPalette.test.tsx +568 -0
  188. package/Tests/UI/Components/CommandPalette/CommandPaletteKeyboard.test.tsx +239 -0
  189. package/Tests/UI/Components/CommandPalette/CommandPaletteProviders.test.tsx +400 -0
  190. package/Tests/UI/Components/CommandPalette/PaletteFilter.test.ts +234 -0
  191. package/Tests/UI/Components/CommandPalette/RecentCommands.test.ts +97 -0
  192. package/Tests/UI/Components/CountModelSideMenuItemGuard.test.tsx +198 -0
  193. package/Tests/UI/Components/FeedItemSafeMode.test.tsx +1 -1
  194. package/Tests/UI/Components/Forms/FormSubmitAnalytics.test.tsx +206 -0
  195. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +267 -0
  196. package/Tests/UI/Components/List.test.tsx +9 -2
  197. package/Tests/UI/Components/ListLoadingStates.test.tsx +173 -0
  198. package/Tests/UI/Components/MarkdownLazy.test.tsx +113 -0
  199. package/Tests/UI/Components/MarkdownMermaidRetry.test.tsx +110 -0
  200. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +8 -0
  201. package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +692 -0
  202. package/Tests/UI/Components/MoreMenuMotion.test.tsx +118 -0
  203. package/Tests/UI/Components/NavBarCommandKGating.test.tsx +178 -0
  204. package/Tests/UI/Components/OrderedStatesList.test.tsx +19 -2
  205. package/Tests/UI/Components/SideOverMotion.test.tsx +109 -0
  206. package/Tests/UI/Components/Skeleton.test.tsx +71 -0
  207. package/Tests/UI/Components/TableLoadingStates.test.tsx +279 -0
  208. package/Tests/UI/Components/Tabs.test.tsx +24 -0
  209. package/Tests/UI/Components/Toast.test.tsx +130 -7
  210. package/Tests/UI/Components/ToastStacking.test.tsx +198 -0
  211. package/Tests/UI/Utils/AIChatExport/ConversationMarkdown.test.ts +774 -0
  212. package/Tests/UI/Utils/AIChatExport/MarkdownBlocks.test.ts +791 -0
  213. package/Tests/UI/Utils/AIChatExport/MarkdownSafety.test.ts +544 -0
  214. package/Tests/UI/Utils/Dropdown.test.ts +423 -0
  215. package/Tests/UI/Utils/ModelAPITenantHeader.test.ts +122 -0
  216. package/Tests/UI/Utils/ModelListCache.test.ts +338 -0
  217. package/Tests/UI/Utils/ProjectListCacheInvalidation.test.ts +225 -0
  218. package/Tests/Utils/API.test.ts +16 -2
  219. package/Tests/Utils/Analytics.test.ts +187 -0
  220. package/Tests/Utils/Dashboard/Components/DashboardKubernetesResourceListShared.test.ts +347 -0
  221. package/Tests/Utils/Dashboard/Components/DashboardListSharedArgs.test.ts +259 -0
  222. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +466 -0
  223. package/Tests/Utils/Monitor/MonitorMetricType.test.ts +80 -0
  224. package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +1291 -0
  225. package/Tests/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.test.ts +214 -0
  226. package/Tests/Utils/Rum/UrlScrubberSegments.test.ts +362 -0
  227. package/Types/Email/EmailTemplateType.ts +1 -0
  228. package/Types/Marketing/MarketingConversion.ts +12 -0
  229. package/Types/Monitor/CriteriaFilter.ts +9 -0
  230. package/Types/Monitor/MonitorCriteriaInstance.ts +41 -20
  231. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +131 -8
  232. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +48 -8
  233. package/Types/Monitor/ServiceAlertTemplates.ts +1352 -0
  234. package/Types/NetworkDevice/NetworkDeviceLinkRuleScope.ts +51 -0
  235. package/Types/Permission.ts +35 -0
  236. package/Types/PushNotification/AndroidNotificationChannel.ts +26 -0
  237. package/Types/PushNotification/PushNotificationMessage.ts +15 -0
  238. package/Types/PushNotification/PushNotificationRequest.ts +8 -16
  239. package/Types/Service/ServiceLanguage.ts +144 -0
  240. package/Types/Text.ts +111 -19
  241. package/UI/Components/Button/Button.tsx +27 -14
  242. package/UI/Components/CodeBlock/CodeBlock.tsx +54 -7
  243. package/UI/Components/CodeBlock/LanguageRegistry.ts +103 -0
  244. package/UI/Components/CommandPalette/CommandPalette.tsx +790 -0
  245. package/UI/Components/CommandPalette/PaletteFilter.ts +196 -0
  246. package/UI/Components/CommandPalette/PaletteRow.tsx +173 -0
  247. package/UI/Components/CommandPalette/RecentCommands.ts +57 -0
  248. package/UI/Components/CommandPalette/Types.ts +49 -0
  249. package/UI/Components/CommandPalette/UseProviderSearch.ts +129 -0
  250. package/UI/Components/Dropdown/Dropdown.tsx +6 -1
  251. package/UI/Components/Feed/FeedItem.tsx +1 -1
  252. package/UI/Components/FormModal/BasicFormModal.tsx +12 -0
  253. package/UI/Components/Forms/BasicForm.tsx +13 -1
  254. package/UI/Components/Forms/BasicModelForm.tsx +2 -1
  255. package/UI/Components/Forms/ModelForm.tsx +10 -1
  256. package/UI/Components/Forms/Utils/FormAnalyticsName.ts +39 -0
  257. package/UI/Components/List/List.tsx +37 -5
  258. package/UI/Components/List/ListSkeleton.tsx +54 -0
  259. package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +6 -1
  260. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +71 -26
  261. package/UI/Components/Modal/Modal.tsx +2 -2
  262. package/UI/Components/ModelFormModal/ModelFormModal.tsx +6 -1
  263. package/UI/Components/ModelTable/BaseModelTable.tsx +1 -1
  264. package/UI/Components/ModelTable/useCustomFieldColumns.ts +11 -5
  265. package/UI/Components/MoreMenu/MoreMenu.tsx +39 -1
  266. package/UI/Components/Navbar/NavBar.tsx +14 -1
  267. package/UI/Components/OrderedStatesList/OrderedStatesList.tsx +50 -5
  268. package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +40 -1
  269. package/UI/Components/SideOver/SideOver.tsx +37 -2
  270. package/UI/Components/Skeleton/Skeleton.tsx +53 -0
  271. package/UI/Components/Table/Table.tsx +74 -44
  272. package/UI/Components/Table/TableSkeletonRows.tsx +141 -0
  273. package/UI/Components/Tabs/Tab.tsx +1 -1
  274. package/UI/Components/Toast/Toast.tsx +118 -53
  275. package/UI/Components/Toast/ToastInit.tsx +82 -32
  276. package/UI/Styles/Theme.css +40 -0
  277. package/UI/Utils/ModelAPI/ModelAPI.ts +20 -10
  278. package/UI/Utils/ModelListCache.ts +188 -0
  279. package/UI/Utils/Project.ts +29 -0
  280. package/Utils/API.ts +6 -1
  281. package/Utils/Analytics.ts +55 -2
  282. package/Utils/Monitor/MonitorMetricType.ts +40 -0
  283. package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +587 -37
  284. package/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.ts +115 -0
  285. package/build/dist/Models/DatabaseModels/GlobalOidc.js +21 -0
  286. package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
  287. package/build/dist/Models/DatabaseModels/GlobalSso.js +21 -0
  288. package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
  289. package/build/dist/Models/DatabaseModels/MarketingConversion.js +20 -10
  290. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -1
  291. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +47 -0
  292. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -1
  293. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +14 -0
  294. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  295. package/build/dist/Models/DatabaseModels/StatusPage.js +4 -4
  296. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  297. package/build/dist/Models/DatabaseModels/UserCall.js +101 -3
  298. package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
  299. package/build/dist/Models/DatabaseModels/UserEmail.js +101 -3
  300. package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
  301. package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js +101 -3
  302. package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js.map +1 -1
  303. package/build/dist/Models/DatabaseModels/UserPush.js +38 -0
  304. package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
  305. package/build/dist/Models/DatabaseModels/UserSMS.js +101 -3
  306. package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
  307. package/build/dist/Models/DatabaseModels/UserWhatsApp.js +100 -2
  308. package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
  309. package/build/dist/Server/API/BaseAPI.js +69 -13
  310. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  311. package/build/dist/Server/API/StatusPageAPI.js +25 -4
  312. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  313. package/build/dist/Server/API/UserCallAPI.js +42 -38
  314. package/build/dist/Server/API/UserCallAPI.js.map +1 -1
  315. package/build/dist/Server/API/UserEmailAPI.js +42 -38
  316. package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
  317. package/build/dist/Server/API/UserIncomingCallNumberAPI.js +37 -32
  318. package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
  319. package/build/dist/Server/API/UserNotificationMethodAdminAPI.js +341 -0
  320. package/build/dist/Server/API/UserNotificationMethodAdminAPI.js.map +1 -0
  321. package/build/dist/Server/API/UserPushAPI.js +95 -2
  322. package/build/dist/Server/API/UserPushAPI.js.map +1 -1
  323. package/build/dist/Server/API/UserSmsAPI.js +42 -38
  324. package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
  325. package/build/dist/Server/API/UserWhatsAppAPI.js +42 -40
  326. package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
  327. package/build/dist/Server/EnvironmentConfig.js +6 -0
  328. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  329. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js +89 -0
  330. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js.map +1 -0
  331. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js +12 -0
  332. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js.map +1 -0
  333. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js +40 -0
  334. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js.map +1 -0
  335. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js +12 -0
  336. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js.map +1 -0
  337. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js +29 -0
  338. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js.map +1 -0
  339. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
  340. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  341. package/build/dist/Server/Middleware/SlackAuthorization.js +28 -4
  342. package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
  343. package/build/dist/Server/Middleware/UserAuthorization.js +197 -22
  344. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  345. package/build/dist/Server/Middleware/VerificationCodeRateLimit.js +393 -0
  346. package/build/dist/Server/Middleware/VerificationCodeRateLimit.js.map +1 -0
  347. package/build/dist/Server/Middleware/WhatsAppAuthorization.js +32 -2
  348. package/build/dist/Server/Middleware/WhatsAppAuthorization.js.map +1 -1
  349. package/build/dist/Server/Services/AnalyticsDatabaseService.js +136 -0
  350. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  351. package/build/dist/Server/Services/DatabaseService.js +74 -0
  352. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  353. package/build/dist/Server/Services/GlobalOidcProjectService.js +105 -0
  354. package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -1
  355. package/build/dist/Server/Services/GlobalOidcService.js +127 -0
  356. package/build/dist/Server/Services/GlobalOidcService.js.map +1 -1
  357. package/build/dist/Server/Services/GlobalSsoProjectService.js +105 -0
  358. package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -1
  359. package/build/dist/Server/Services/GlobalSsoService.js +127 -0
  360. package/build/dist/Server/Services/GlobalSsoService.js.map +1 -1
  361. package/build/dist/Server/Services/LogAggregationService.js +5 -0
  362. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  363. package/build/dist/Server/Services/PushNotificationService.js +69 -30
  364. package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
  365. package/build/dist/Server/Services/TeamMemberService.js +43 -7
  366. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  367. package/build/dist/Server/Services/UserCallService.js +54 -22
  368. package/build/dist/Server/Services/UserCallService.js.map +1 -1
  369. package/build/dist/Server/Services/UserEmailService.js +78 -17
  370. package/build/dist/Server/Services/UserEmailService.js.map +1 -1
  371. package/build/dist/Server/Services/UserIncomingCallNumberService.js +45 -74
  372. package/build/dist/Server/Services/UserIncomingCallNumberService.js.map +1 -1
  373. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1117 -0
  374. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -0
  375. package/build/dist/Server/Services/UserNotificationRuleService.js +20 -0
  376. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  377. package/build/dist/Server/Services/UserPushService.js +77 -0
  378. package/build/dist/Server/Services/UserPushService.js.map +1 -1
  379. package/build/dist/Server/Services/UserSmsService.js +50 -17
  380. package/build/dist/Server/Services/UserSmsService.js.map +1 -1
  381. package/build/dist/Server/Services/UserWhatsAppService.js +48 -15
  382. package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
  383. package/build/dist/Server/Types/Database/QueryHelper.js +27 -0
  384. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  385. package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js +46 -0
  386. package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js.map +1 -1
  387. package/build/dist/Server/Utils/ChannelVerification.js +348 -0
  388. package/build/dist/Server/Utils/ChannelVerification.js.map +1 -0
  389. package/build/dist/Server/Utils/CronTab.js +66 -0
  390. package/build/dist/Server/Utils/CronTab.js.map +1 -1
  391. package/build/dist/Server/Utils/GlobalSsoAuthorization.js +112 -0
  392. package/build/dist/Server/Utils/GlobalSsoAuthorization.js.map +1 -0
  393. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +17 -1
  394. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -1
  395. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +1 -1
  396. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -1
  397. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +1 -1
  398. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -1
  399. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +1 -1
  400. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -1
  401. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +1 -1
  402. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -1
  403. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +1 -1
  404. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -1
  405. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +37 -35
  406. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
  407. package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js +21 -25
  408. package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js.map +1 -1
  409. package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js +20 -22
  410. package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js.map +1 -1
  411. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +370 -42
  412. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  413. package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js +21 -25
  414. package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js.map +1 -1
  415. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +34 -37
  416. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  417. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +21 -25
  418. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
  419. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +64 -52
  420. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
  421. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +25 -29
  422. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
  423. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +20 -17
  424. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  425. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +8 -0
  426. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  427. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +17 -14
  428. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  429. package/build/dist/Server/Utils/Monitor/MonitorResource.js +123 -23
  430. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  431. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +95 -0
  432. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -0
  433. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +313 -0
  434. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -0
  435. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +3 -3
  436. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +67 -26
  437. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  438. package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js +39 -0
  439. package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js.map +1 -0
  440. package/build/dist/Server/Utils/StatusPageCustomizationAccess.js +85 -0
  441. package/build/dist/Server/Utils/StatusPageCustomizationAccess.js.map +1 -0
  442. package/build/dist/Server/Utils/TotpAuth.js +127 -8
  443. package/build/dist/Server/Utils/TotpAuth.js.map +1 -1
  444. package/build/dist/Server/Utils/UserRegistrationToken.js +170 -0
  445. package/build/dist/Server/Utils/UserRegistrationToken.js.map +1 -0
  446. package/build/dist/Server/Utils/VerificationCode.js +120 -0
  447. package/build/dist/Server/Utils/VerificationCode.js.map +1 -0
  448. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  449. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  450. package/build/dist/Types/Marketing/MarketingConversion.js +10 -0
  451. package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -1
  452. package/build/dist/Types/Monitor/CriteriaFilter.js +1 -0
  453. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  454. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +42 -21
  455. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  456. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +77 -5
  457. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  458. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +18 -6
  459. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  460. package/build/dist/Types/Monitor/ServiceAlertTemplates.js +1028 -0
  461. package/build/dist/Types/Monitor/ServiceAlertTemplates.js.map +1 -0
  462. package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js +46 -0
  463. package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js.map +1 -0
  464. package/build/dist/Types/Permission.js +33 -0
  465. package/build/dist/Types/Permission.js.map +1 -1
  466. package/build/dist/Types/PushNotification/AndroidNotificationChannel.js +27 -0
  467. package/build/dist/Types/PushNotification/AndroidNotificationChannel.js.map +1 -0
  468. package/build/dist/Types/Service/ServiceLanguage.js +96 -0
  469. package/build/dist/Types/Service/ServiceLanguage.js.map +1 -0
  470. package/build/dist/Types/Text.js +82 -17
  471. package/build/dist/Types/Text.js.map +1 -1
  472. package/build/dist/UI/Components/Button/Button.js +22 -14
  473. package/build/dist/UI/Components/Button/Button.js.map +1 -1
  474. package/build/dist/UI/Components/CodeBlock/CodeBlock.js +35 -3
  475. package/build/dist/UI/Components/CodeBlock/CodeBlock.js.map +1 -1
  476. package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js +57 -0
  477. package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js.map +1 -0
  478. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +439 -0
  479. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -0
  480. package/build/dist/UI/Components/CommandPalette/PaletteFilter.js +135 -0
  481. package/build/dist/UI/Components/CommandPalette/PaletteFilter.js.map +1 -0
  482. package/build/dist/UI/Components/CommandPalette/PaletteRow.js +83 -0
  483. package/build/dist/UI/Components/CommandPalette/PaletteRow.js.map +1 -0
  484. package/build/dist/UI/Components/CommandPalette/RecentCommands.js +47 -0
  485. package/build/dist/UI/Components/CommandPalette/RecentCommands.js.map +1 -0
  486. package/build/dist/UI/Components/CommandPalette/Types.js +2 -0
  487. package/build/dist/UI/Components/CommandPalette/Types.js.map +1 -0
  488. package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js +88 -0
  489. package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js.map +1 -0
  490. package/build/dist/UI/Components/Dropdown/Dropdown.js +6 -1
  491. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  492. package/build/dist/UI/Components/Feed/FeedItem.js +1 -1
  493. package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
  494. package/build/dist/UI/Components/FormModal/BasicFormModal.js +2 -1
  495. package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
  496. package/build/dist/UI/Components/Forms/BasicForm.js +10 -1
  497. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  498. package/build/dist/UI/Components/Forms/BasicModelForm.js +2 -1
  499. package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
  500. package/build/dist/UI/Components/Forms/ModelForm.js +4 -1
  501. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  502. package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js +33 -0
  503. package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js.map +1 -0
  504. package/build/dist/UI/Components/List/List.js +19 -5
  505. package/build/dist/UI/Components/List/List.js.map +1 -1
  506. package/build/dist/UI/Components/List/ListSkeleton.js +21 -0
  507. package/build/dist/UI/Components/List/ListSkeleton.js.map +1 -0
  508. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
  509. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +52 -25
  510. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  511. package/build/dist/UI/Components/Modal/Modal.js +2 -2
  512. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js +2 -1
  513. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  514. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +1 -1
  515. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  516. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +9 -2
  517. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  518. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +32 -1
  519. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  520. package/build/dist/UI/Components/Navbar/NavBar.js +5 -1
  521. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  522. package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js +29 -5
  523. package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js.map +1 -1
  524. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +23 -1
  525. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
  526. package/build/dist/UI/Components/SideOver/SideOver.js +23 -2
  527. package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
  528. package/build/dist/UI/Components/Skeleton/Skeleton.js +23 -0
  529. package/build/dist/UI/Components/Skeleton/Skeleton.js.map +1 -0
  530. package/build/dist/UI/Components/Table/Table.js +35 -24
  531. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  532. package/build/dist/UI/Components/Table/TableSkeletonRows.js +52 -0
  533. package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -0
  534. package/build/dist/UI/Components/Tabs/Tab.js +1 -1
  535. package/build/dist/UI/Components/Tabs/Tab.js.map +1 -1
  536. package/build/dist/UI/Components/Toast/Toast.js +77 -23
  537. package/build/dist/UI/Components/Toast/Toast.js.map +1 -1
  538. package/build/dist/UI/Components/Toast/ToastInit.js +37 -15
  539. package/build/dist/UI/Components/Toast/ToastInit.js.map +1 -1
  540. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +18 -9
  541. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  542. package/build/dist/UI/Utils/ModelListCache.js +126 -0
  543. package/build/dist/UI/Utils/ModelListCache.js.map +1 -0
  544. package/build/dist/UI/Utils/Project.js +24 -1
  545. package/build/dist/UI/Utils/Project.js.map +1 -1
  546. package/build/dist/Utils/API.js +6 -1
  547. package/build/dist/Utils/API.js.map +1 -1
  548. package/build/dist/Utils/Analytics.js +50 -2
  549. package/build/dist/Utils/Analytics.js.map +1 -1
  550. package/build/dist/Utils/Monitor/MonitorMetricType.js +37 -0
  551. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  552. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +318 -37
  553. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -1
  554. package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js +87 -0
  555. package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js.map +1 -0
  556. package/jest.config.json +1 -1
  557. package/package.json +2 -3
  558. package/Server/Utils/Monitor/MonitorClusterContext.ts +0 -182
  559. package/Tests/__mocks__/otpauth.js +0 -30
  560. package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js +0 -141
  561. package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js.map +0 -1
@@ -196,3 +196,1294 @@ describe("NetworkDeviceLinkRuleUtil.resolveRule", () => {
196
196
  ]);
197
197
  });
198
198
  });
199
+
200
+ /*
201
+ * ---------------------------------------------------------------------------
202
+ * Site scope — GitHub issue #3260.
203
+ *
204
+ * The suite above is the project-scope contract and has to keep passing
205
+ * exactly as written: site scope is opt-in, so every rule saved before the
206
+ * column existed still means "one parent for the whole project".
207
+ *
208
+ * The fixtures below speak the reporter's vocabulary — routers and switches
209
+ * in numbered units — because the bug is only legible at that shape: fourteen
210
+ * units, fourteen routers all carrying the same label, and a global map that
211
+ * answered "which one is THE router?" with silence in all fourteen.
212
+ * ---------------------------------------------------------------------------
213
+ */
214
+
215
+ const SWITCH_LABEL: string = "label-subcategory-switch";
216
+ const ROUTER_LABEL: string = "label-subcategory-router";
217
+ const MANAGED_LABEL: string = "label-managed";
218
+
219
+ /*
220
+ * The `device` helper from the suite above, extended rather than replaced: id
221
+ * and labels first, the two site fields optional, so an unsited device is
222
+ * still written `deviceAt("sw1", [...])` and reads the way it always did.
223
+ */
224
+ const deviceAt: (
225
+ id: string,
226
+ labelIds: Array<string>,
227
+ siteId?: string | null | undefined,
228
+ siteName?: string | undefined,
229
+ ) => LinkRuleDeviceInput = (
230
+ id: string,
231
+ labelIds: Array<string>,
232
+ siteId?: string | null | undefined,
233
+ siteName?: string | undefined,
234
+ ): LinkRuleDeviceInput => {
235
+ return { id, labelIds, siteId, siteName };
236
+ };
237
+
238
+ const routerAt: (
239
+ id: string,
240
+ siteId?: string | null | undefined,
241
+ siteName?: string | undefined,
242
+ ) => LinkRuleDeviceInput = (
243
+ id: string,
244
+ siteId?: string | null | undefined,
245
+ siteName?: string | undefined,
246
+ ): LinkRuleDeviceInput => {
247
+ return deviceAt(id, [ROUTER_LABEL, MANAGED_LABEL], siteId, siteName);
248
+ };
249
+
250
+ const switchAt: (
251
+ id: string,
252
+ siteId?: string | null | undefined,
253
+ siteName?: string | undefined,
254
+ ) => LinkRuleDeviceInput = (
255
+ id: string,
256
+ siteId?: string | null | undefined,
257
+ siteName?: string | undefined,
258
+ ): LinkRuleDeviceInput => {
259
+ return deviceAt(id, [SWITCH_LABEL, MANAGED_LABEL], siteId, siteName);
260
+ };
261
+
262
+ // Site-scoped by default; `scopedRule({ scope: "Project" })` for the other one.
263
+ const scopedRule: (overrides?: Partial<LinkRuleInput>) => LinkRuleInput = (
264
+ overrides?: Partial<LinkRuleInput>,
265
+ ): LinkRuleInput => {
266
+ return {
267
+ id: "rule-uplink",
268
+ name: "Switch uplinks",
269
+ isEnabled: true,
270
+ childLabelIds: [SWITCH_LABEL, MANAGED_LABEL],
271
+ parentLabelIds: [ROUTER_LABEL, MANAGED_LABEL],
272
+ scope: "Site",
273
+ ...overrides,
274
+ };
275
+ };
276
+
277
+ /*
278
+ * Site names sort into the warning text, so they are zero padded: "Unit 9"
279
+ * would sort after "Unit 14" and make an assertion about which three sites got
280
+ * named depend on how many units the test happened to build.
281
+ */
282
+ const twoDigit: (value: number) => string = (value: number): string => {
283
+ return value < 10 ? `0${value}` : `${value}`;
284
+ };
285
+
286
+ /** One unit's devices, in the order a device query would hand them over. */
287
+ const unit: (
288
+ index: number,
289
+ routerCount: number,
290
+ switchCount: number,
291
+ ) => Array<LinkRuleDeviceInput> = (
292
+ index: number,
293
+ routerCount: number,
294
+ switchCount: number,
295
+ ): Array<LinkRuleDeviceInput> => {
296
+ const siteId: string = `site-${twoDigit(index)}`;
297
+ const siteName: string = `Unit ${twoDigit(index)}`;
298
+ const devices: Array<LinkRuleDeviceInput> = [];
299
+ for (let router: number = 1; router <= routerCount; router++) {
300
+ devices.push(
301
+ routerAt(`router-${twoDigit(index)}-${router}`, siteId, siteName),
302
+ );
303
+ }
304
+ for (let leaf: number = 1; leaf <= switchCount; leaf++) {
305
+ devices.push(
306
+ switchAt(`switch-${twoDigit(index)}-${leaf}`, siteId, siteName),
307
+ );
308
+ }
309
+ return devices;
310
+ };
311
+
312
+ /** `unit()` over a run of consecutive units, flattened. */
313
+ const units: (
314
+ from: number,
315
+ to: number,
316
+ routerCount: number,
317
+ switchCount: number,
318
+ ) => Array<LinkRuleDeviceInput> = (
319
+ from: number,
320
+ to: number,
321
+ routerCount: number,
322
+ switchCount: number,
323
+ ): Array<LinkRuleDeviceInput> => {
324
+ const devices: Array<LinkRuleDeviceInput> = [];
325
+ for (let index: number = from; index <= to; index++) {
326
+ devices.push(...unit(index, routerCount, switchCount));
327
+ }
328
+ return devices;
329
+ };
330
+
331
+ /*
332
+ * Device id -> normalised site key, so a test can ask which site a link's two
333
+ * ends live in without hard-coding the id-to-site mapping a second time.
334
+ */
335
+ const siteIndexOf: (
336
+ devices: Array<LinkRuleDeviceInput>,
337
+ ) => Map<string, string> = (
338
+ devices: Array<LinkRuleDeviceInput>,
339
+ ): Map<string, string> => {
340
+ const index: Map<string, string> = new Map<string, string>();
341
+ for (const entry of devices) {
342
+ index.set(entry.id, (entry.siteId || "").toString().trim());
343
+ }
344
+ return index;
345
+ };
346
+
347
+ /*
348
+ * The util's warning and group-failure types, reached through its own
349
+ * signatures. A second `import` from the same module would trip
350
+ * `no-duplicate-imports`, and this file is only ever meant to grow downwards.
351
+ */
352
+ type ResolvedWarning = NonNullable<
353
+ ReturnType<typeof NetworkDeviceLinkRuleUtil.getWarning>
354
+ >;
355
+
356
+ type ResolvedGroupFailure = NonNullable<
357
+ LinkRuleOutcome["groupFailures"]
358
+ >[number];
359
+
360
+ const warningOf: (outcome: LinkRuleOutcome) => ResolvedWarning = (
361
+ outcome: LinkRuleOutcome,
362
+ ): ResolvedWarning => {
363
+ const warning: ResolvedWarning | null =
364
+ NetworkDeviceLinkRuleUtil.getWarning(outcome);
365
+ expect(warning).not.toBeNull();
366
+ return warning!;
367
+ };
368
+
369
+ const failuresOf: (outcome: LinkRuleOutcome) => Array<ResolvedGroupFailure> = (
370
+ outcome: LinkRuleOutcome,
371
+ ): Array<ResolvedGroupFailure> => {
372
+ return outcome.groupFailures || [];
373
+ };
374
+
375
+ interface ScopeScenario {
376
+ label: string;
377
+ rule: LinkRuleInput;
378
+ devices: Array<LinkRuleDeviceInput>;
379
+ }
380
+
381
+ /*
382
+ * One table, swept by the invariant tests below. The point of sweeping rather
383
+ * than asserting case by case is that the two invariants — matchedChildCount
384
+ * tracks links, and "empty" and "skipped" are the same fact — are what every
385
+ * caller reads the outcome through, so they must hold on the paths nobody
386
+ * thought about as much as on the ones they did.
387
+ */
388
+ const SCOPE_SCENARIOS: Array<ScopeScenario> = [
389
+ {
390
+ label: "project scope, one router and two switches",
391
+ rule: scopedRule({ scope: "Project" }),
392
+ devices: unit(1, 1, 2),
393
+ },
394
+ {
395
+ label: "project scope, a router in each of two units",
396
+ rule: scopedRule({ scope: "Project" }),
397
+ devices: units(1, 2, 1, 2),
398
+ },
399
+ {
400
+ label: "project scope, no router anywhere",
401
+ rule: scopedRule({ scope: "Project" }),
402
+ devices: unit(1, 0, 2),
403
+ },
404
+ {
405
+ label: "project scope, a router and nothing to place",
406
+ rule: scopedRule({ scope: "Project" }),
407
+ devices: unit(1, 1, 0),
408
+ },
409
+ {
410
+ label: "project scope, disabled",
411
+ rule: scopedRule({ scope: "Project", isEnabled: false }),
412
+ devices: unit(1, 1, 2),
413
+ },
414
+ {
415
+ label: "project scope, no child labels",
416
+ rule: scopedRule({ scope: "Project", childLabelIds: [] }),
417
+ devices: unit(1, 1, 2),
418
+ },
419
+ {
420
+ label: "project scope, no parent labels",
421
+ rule: scopedRule({ scope: "Project", parentLabelIds: [] }),
422
+ devices: unit(1, 1, 2),
423
+ },
424
+ {
425
+ label: "site scope, fourteen healthy units",
426
+ rule: scopedRule(),
427
+ devices: units(1, 14, 1, 3),
428
+ },
429
+ {
430
+ label: "site scope, thirteen healthy units and one with two routers",
431
+ rule: scopedRule(),
432
+ devices: [...units(1, 13, 1, 3), ...unit(14, 2, 3)],
433
+ },
434
+ {
435
+ label: "site scope, every unit parentless",
436
+ rule: scopedRule(),
437
+ devices: units(1, 3, 0, 2),
438
+ },
439
+ {
440
+ label: "site scope, nothing is assigned to a site",
441
+ rule: scopedRule(),
442
+ devices: [
443
+ switchAt("floater-1"),
444
+ switchAt("floater-2"),
445
+ routerAt("floater-3"),
446
+ ],
447
+ },
448
+ {
449
+ label: "site scope, no devices at all",
450
+ rule: scopedRule(),
451
+ devices: [],
452
+ },
453
+ {
454
+ label: "site scope, the only child match is the router itself",
455
+ rule: scopedRule(),
456
+ devices: [
457
+ deviceAt(
458
+ "core-01",
459
+ [ROUTER_LABEL, SWITCH_LABEL, MANAGED_LABEL],
460
+ "site-01",
461
+ "Unit 01",
462
+ ),
463
+ ],
464
+ },
465
+ {
466
+ label: "site scope, nothing carries the child labels",
467
+ rule: scopedRule(),
468
+ devices: units(1, 3, 1, 0),
469
+ },
470
+ {
471
+ label: "site scope, disabled",
472
+ rule: scopedRule({ isEnabled: false }),
473
+ devices: units(1, 3, 1, 2),
474
+ },
475
+ ];
476
+
477
+ describe("NetworkDeviceLinkRuleUtil.resolveRule — site scope", () => {
478
+ it("leaves the project-scope outcome byte for byte what it always was", () => {
479
+ /*
480
+ * The migration adds a nullable column, so every rule in every existing
481
+ * project arrives here with scope undefined. If that path grew even one
482
+ * new key, a rule nobody touched would start reporting site coverage for
483
+ * a project that has never been divided into sites — which is why the
484
+ * absence of the keys is asserted with `in`, not with `toBeUndefined`.
485
+ */
486
+ const devices: Array<LinkRuleDeviceInput> = unit(1, 1, 2);
487
+
488
+ const implicit: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
489
+ scopedRule({ scope: undefined }),
490
+ devices,
491
+ );
492
+ const explicit: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
493
+ scopedRule({ scope: "Project" }),
494
+ devices,
495
+ );
496
+ // An unrecognised column value is a project-scoped rule too, never a guess.
497
+ const garbage: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
498
+ scopedRule({ scope: "Region" }),
499
+ devices,
500
+ );
501
+
502
+ expect(implicit).toEqual(explicit);
503
+ expect(garbage).toEqual(explicit);
504
+
505
+ expect(Object.keys(implicit).sort()).toEqual([
506
+ "links",
507
+ "matchedChildCount",
508
+ "matchedParentCount",
509
+ "ruleId",
510
+ "ruleName",
511
+ ]);
512
+ expect("groupFailures" in implicit).toBe(false);
513
+ expect("applicableSiteCount" in implicit).toBe(false);
514
+ expect("unsitedChildDeviceCount" in implicit).toBe(false);
515
+
516
+ // The skipping path carries exactly one more key, and still no site keys.
517
+ const skipped: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
518
+ scopedRule({ scope: "Project" }),
519
+ unit(1, 0, 2),
520
+ );
521
+
522
+ expect(Object.keys(skipped).sort()).toEqual([
523
+ "links",
524
+ "matchedChildCount",
525
+ "matchedParentCount",
526
+ "ruleId",
527
+ "ruleName",
528
+ "skipReason",
529
+ ]);
530
+ expect("groupFailures" in skipped).toBe(false);
531
+ expect("applicableSiteCount" in skipped).toBe(false);
532
+ expect("unsitedChildDeviceCount" in skipped).toBe(false);
533
+ });
534
+
535
+ it("asks the parent question once per site, so two units draw four uplinks", () => {
536
+ /*
537
+ * Under project scope this exact device set is `ambiguousParent` — two
538
+ * routers carry the parent labels — and draws nothing. Partitioning by
539
+ * site turns the same labels into the star each unit was meant to have.
540
+ */
541
+ const devices: Array<LinkRuleDeviceInput> = [
542
+ ...unit(1, 1, 2),
543
+ ...unit(2, 1, 2),
544
+ ];
545
+
546
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
547
+ scopedRule(),
548
+ devices,
549
+ );
550
+
551
+ expect(outcome.links).toHaveLength(4);
552
+ expect(outcome.skipReason).toBeUndefined();
553
+ expect(outcome.groupFailures).toBeUndefined();
554
+ expect(outcome.applicableSiteCount).toBe(2);
555
+
556
+ // No uplink may cross a site boundary: that would be a cable nobody has.
557
+ const siteOf: Map<string, string> = siteIndexOf(devices);
558
+ for (const link of outcome.links) {
559
+ expect(siteOf.get(link.fromDeviceId)).toBe(siteOf.get(link.toDeviceId));
560
+ }
561
+ });
562
+
563
+ it("draws all fourteen units instead of reporting fourteen candidate routers", () => {
564
+ /*
565
+ * The reporter's exact scenario from issue #3260: fourteen units, each
566
+ * with one router carrying `SubCategory:Router` and three switches. On
567
+ * the global map the old resolver saw fourteen parent candidates, called
568
+ * the rule ambiguous, and drew NOTHING — not even the thirteen units that
569
+ * were never ambiguous in the first place.
570
+ */
571
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
572
+ scopedRule(),
573
+ units(1, 14, 1, 3),
574
+ );
575
+
576
+ expect(outcome.links).toHaveLength(42);
577
+ expect(outcome.skipReason).toBeUndefined();
578
+ expect(outcome.groupFailures).toBeUndefined();
579
+ expect(outcome.applicableSiteCount).toBe(14);
580
+ expect(outcome.matchedParentCount).toBe(14);
581
+ // A rule doing its job owes the operator no explanation.
582
+ expect(NetworkDeviceLinkRuleUtil.getWarning(outcome)).toBeNull();
583
+ });
584
+
585
+ it("puts a null site and a missing site in the same excluded bucket", () => {
586
+ /*
587
+ * `null` and `undefined` are the same fact — "nobody has assigned this
588
+ * device to a site" — and the resolver keys its groups on a string, so if
589
+ * they normalised differently they would become two sites, each with one
590
+ * router, each drawing uplinks between devices whose only relationship is
591
+ * that neither has been filed yet.
592
+ */
593
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
594
+ scopedRule(),
595
+ [
596
+ routerAt("router-null", null),
597
+ switchAt("switch-null", null),
598
+ routerAt("router-undefined"),
599
+ switchAt("switch-undefined"),
600
+ switchAt("switch-empty", ""),
601
+ switchAt("switch-blank", " "),
602
+ ],
603
+ );
604
+
605
+ expect(outcome.links).toEqual([]);
606
+ expect(outcome.applicableSiteCount).toBe(0);
607
+ expect(outcome.groupFailures).toBeUndefined();
608
+ expect(outcome.unsitedChildDeviceCount).toBe(4);
609
+ });
610
+
611
+ it("trims and stringifies the site key, so ' s1 ' and 's1' are one site", () => {
612
+ /*
613
+ * The caller stringifies an ObjectID into this field, and a stray space in
614
+ * an imported site id would otherwise split one building into two — each
615
+ * half then reporting that it has no router.
616
+ */
617
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
618
+ scopedRule(),
619
+ [
620
+ routerAt("router-1", " s1 ", "Unit 01"),
621
+ switchAt("switch-1", "s1", "Unit 01"),
622
+ switchAt("switch-2", "s1 ", "Unit 01"),
623
+ ],
624
+ );
625
+
626
+ expect(outcome.links).toEqual([
627
+ { fromDeviceId: "switch-1", toDeviceId: "router-1" },
628
+ { fromDeviceId: "switch-2", toDeviceId: "router-1" },
629
+ ]);
630
+ expect(outcome.applicableSiteCount).toBe(1);
631
+ expect(outcome.groupFailures).toBeUndefined();
632
+ });
633
+
634
+ it("counts unsited children rather than linking them to each other", () => {
635
+ /*
636
+ * Two switches and a router with no site is not a site with three devices
637
+ * in it. Linking them would assert a cabling fact from the absence of
638
+ * data, so they are walked past — and counted, because "the rule skipped
639
+ * four of your devices" is the whole reason the operator can act on it.
640
+ */
641
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
642
+ scopedRule(),
643
+ [switchAt("switch-1"), switchAt("switch-2"), routerAt("router-1")],
644
+ );
645
+
646
+ expect(outcome.links).toEqual([]);
647
+ expect(outcome.unsitedChildDeviceCount).toBe(2);
648
+ expect(outcome.applicableSiteCount).toBe(0);
649
+ expect(warningOf(outcome).message).toContain("not assigned to a site");
650
+ });
651
+
652
+ it("does not count an unsited device that only carries the parent labels", () => {
653
+ /*
654
+ * The count is a damage assessment — "these are the nodes floating on your
655
+ * map" — and an unsited router is not a node the rule wanted to place. It
656
+ * would inflate the number the operator is being asked to act on.
657
+ */
658
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
659
+ scopedRule(),
660
+ [routerAt("router-unsited"), ...unit(1, 1, 1)],
661
+ );
662
+
663
+ expect(outcome.unsitedChildDeviceCount).toBeUndefined();
664
+ expect(outcome.links).toEqual([
665
+ { fromDeviceId: "switch-01-1", toDeviceId: "router-01-1" },
666
+ ]);
667
+ });
668
+
669
+ it("says nothing about a site holding neither a parent nor a child", () => {
670
+ /*
671
+ * The rule simply does not apply in that building. Reporting it would
672
+ * trade one loud failure for one quiet one per site in the project, and a
673
+ * banner that lists two hundred irrelevant sites is a banner nobody reads.
674
+ */
675
+ const devices: Array<LinkRuleDeviceInput> = [
676
+ ...unit(1, 1, 2),
677
+ deviceAt("camera-1", ["label-camera"], "site-02", "Unit 02"),
678
+ deviceAt("camera-2", ["label-camera"], "site-02", "Unit 02"),
679
+ ];
680
+
681
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
682
+ scopedRule(),
683
+ devices,
684
+ );
685
+
686
+ expect(outcome.links).toHaveLength(2);
687
+ expect(outcome.groupFailures).toBeUndefined();
688
+ expect(outcome.applicableSiteCount).toBe(1);
689
+ });
690
+
691
+ it("says nothing about a site with a router and nothing to place", () => {
692
+ /*
693
+ * A parent label like `SubCategory:Router` is present in nearly every
694
+ * site by construction, so a site that has one and no switches is the
695
+ * normal case, not a misconfiguration.
696
+ */
697
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
698
+ scopedRule(),
699
+ [...unit(1, 1, 2), ...unit(2, 1, 0), ...unit(3, 3, 0)],
700
+ );
701
+
702
+ expect(outcome.links).toHaveLength(2);
703
+ expect(outcome.groupFailures).toBeUndefined();
704
+ expect(outcome.applicableSiteCount).toBe(1);
705
+ });
706
+
707
+ it("says nothing about a site whose only child match is its own router", () => {
708
+ /*
709
+ * A collapsed core carrying both label sets is one box: after the
710
+ * self-link exclusion the site has nothing left to place, which is the
711
+ * same silence as having no switches at all rather than a failure to
712
+ * report. The pre-exclusion child count is 1 here, so this pins that the
713
+ * gate looks at what is left AFTER the parent is removed.
714
+ */
715
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
716
+ scopedRule(),
717
+ [
718
+ ...unit(1, 1, 2),
719
+ deviceAt(
720
+ "core-02",
721
+ [ROUTER_LABEL, SWITCH_LABEL, MANAGED_LABEL],
722
+ "site-02",
723
+ "Unit 02",
724
+ ),
725
+ ],
726
+ );
727
+
728
+ expect(outcome.links).toHaveLength(2);
729
+ expect(outcome.groupFailures).toBeUndefined();
730
+ expect(outcome.applicableSiteCount).toBe(1);
731
+ });
732
+
733
+ it("reports a site with switches and no router once, with its own damage count", () => {
734
+ const devices: Array<LinkRuleDeviceInput> = [
735
+ ...unit(1, 1, 2),
736
+ ...unit(2, 0, 3),
737
+ ];
738
+
739
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
740
+ scopedRule(),
741
+ devices,
742
+ );
743
+
744
+ expect(outcome.links).toHaveLength(2);
745
+ expect(failuresOf(outcome)).toEqual([
746
+ {
747
+ siteId: "site-02",
748
+ siteName: "Unit 02",
749
+ reason: "noParentMatched",
750
+ matchedParentCount: 0,
751
+ // The three switches stranded in Unit 02, not the five in the project.
752
+ matchedChildCount: 3,
753
+ },
754
+ ]);
755
+ expect(outcome.applicableSiteCount).toBe(2);
756
+ });
757
+
758
+ it("reports an ambiguous site's own parent count, never the project-wide one", () => {
759
+ /*
760
+ * The old failure mode in one number. Project-wide there are four routers
761
+ * here; the operator's actual problem is the two sitting in Unit 03, and
762
+ * telling them "4 devices carry the parent labels" would send them looking
763
+ * at the three units that are fine.
764
+ */
765
+ const devices: Array<LinkRuleDeviceInput> = [
766
+ ...unit(1, 1, 2),
767
+ ...unit(2, 1, 2),
768
+ ...unit(3, 2, 2),
769
+ ];
770
+
771
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
772
+ scopedRule(),
773
+ devices,
774
+ );
775
+
776
+ expect(outcome.links).toHaveLength(4);
777
+ expect(failuresOf(outcome)).toHaveLength(1);
778
+ expect(failuresOf(outcome)[0]!.reason).toBe("ambiguousParent");
779
+ expect(failuresOf(outcome)[0]!.matchedParentCount).toBe(2);
780
+ expect(failuresOf(outcome)[0]!.siteId).toBe("site-03");
781
+ expect(failuresOf(outcome)[0]!.matchedChildCount).toBe(2);
782
+ });
783
+
784
+ it("keeps matchedChildCount equal to links.length in every scope and outcome", () => {
785
+ /*
786
+ * The rule list renders "Draws N uplinks" from matchedChildCount while the
787
+ * map renders links, so the moment those two disagree the product tells
788
+ * the operator two different stories about the same rule.
789
+ */
790
+ for (const scenario of SCOPE_SCENARIOS) {
791
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
792
+ scenario.rule,
793
+ scenario.devices,
794
+ );
795
+
796
+ expect({
797
+ label: scenario.label,
798
+ matchedChildCount: outcome.matchedChildCount,
799
+ }).toEqual({
800
+ label: scenario.label,
801
+ matchedChildCount: outcome.links.length,
802
+ });
803
+ }
804
+ });
805
+
806
+ it("treats an empty links array and a skipReason as the same fact, both scopes", () => {
807
+ /*
808
+ * describeOutcome switches on skipReason and falls through to "Draws N
809
+ * uplinks", so a gap in either direction produces a rule that either
810
+ * claims to draw nothing while drawing, or claims to draw nothing while
811
+ * offering no reason at all.
812
+ */
813
+ for (const scenario of SCOPE_SCENARIOS) {
814
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
815
+ scenario.rule,
816
+ scenario.devices,
817
+ );
818
+
819
+ expect({
820
+ label: scenario.label,
821
+ empty: outcome.links.length === 0,
822
+ }).toEqual({
823
+ label: scenario.label,
824
+ empty: outcome.skipReason !== undefined,
825
+ });
826
+ }
827
+ });
828
+
829
+ it("never claims to draw anything for an outcome that drew nothing", () => {
830
+ for (const scenario of SCOPE_SCENARIOS) {
831
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
832
+ scenario.rule,
833
+ scenario.devices,
834
+ );
835
+ const sentence: string =
836
+ NetworkDeviceLinkRuleUtil.describeOutcome(outcome);
837
+
838
+ expect({
839
+ label: scenario.label,
840
+ draws: sentence.startsWith("Draws"),
841
+ }).toEqual({
842
+ label: scenario.label,
843
+ draws: outcome.links.length > 0,
844
+ });
845
+ }
846
+ });
847
+
848
+ it("reports an empty device list as nothing to place, not as a resolved rule", () => {
849
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
850
+ scopedRule(),
851
+ [],
852
+ );
853
+
854
+ expect(outcome.links).toEqual([]);
855
+ expect(outcome.skipReason).toBe("noChildrenMatched");
856
+ expect(outcome.groupFailures).toBeUndefined();
857
+ expect(outcome.unsitedChildDeviceCount).toBeUndefined();
858
+ expect(outcome.applicableSiteCount).toBe(0);
859
+ });
860
+
861
+ it("never links a device to itself, and a dual-labelled core is its site's parent", () => {
862
+ /*
863
+ * A collapsed core carrying both label sets is still one box. In Unit 01
864
+ * it is the only parent candidate, so it becomes the parent and is
865
+ * excluded from its own children; Unit 02 resolves its own star in
866
+ * parallel and the two never touch.
867
+ */
868
+ const dualIsTheOnlyParent: LinkRuleOutcome =
869
+ NetworkDeviceLinkRuleUtil.resolveRule(scopedRule(), [
870
+ deviceAt(
871
+ "core-01",
872
+ [ROUTER_LABEL, SWITCH_LABEL, MANAGED_LABEL],
873
+ "site-01",
874
+ "Unit 01",
875
+ ),
876
+ switchAt("switch-01-1", "site-01", "Unit 01"),
877
+ routerAt("router-02-1", "site-02", "Unit 02"),
878
+ switchAt("switch-02-1", "site-02", "Unit 02"),
879
+ ]);
880
+
881
+ expect(dualIsTheOnlyParent.links).toEqual([
882
+ { fromDeviceId: "switch-01-1", toDeviceId: "core-01" },
883
+ { fromDeviceId: "switch-02-1", toDeviceId: "router-02-1" },
884
+ ]);
885
+ for (const link of dualIsTheOnlyParent.links) {
886
+ expect(link.fromDeviceId).not.toBe(link.toDeviceId);
887
+ }
888
+
889
+ /*
890
+ * And the other half of the same rule: a device carrying the parent labels
891
+ * is always a parent CANDIDATE, so it is never quietly demoted to child
892
+ * just because its site already has a router. Unit 02 below has two boxes
893
+ * answering to `SubCategory:Router` and the honest answer is that nobody
894
+ * said which one is the uplink — so Unit 02 draws nothing and says why,
895
+ * while Unit 01 is untouched by its neighbour's problem.
896
+ */
897
+ const dualCompetesWithARouter: LinkRuleOutcome =
898
+ NetworkDeviceLinkRuleUtil.resolveRule(scopedRule(), [
899
+ deviceAt(
900
+ "core-01",
901
+ [ROUTER_LABEL, SWITCH_LABEL, MANAGED_LABEL],
902
+ "site-01",
903
+ "Unit 01",
904
+ ),
905
+ switchAt("switch-01-1", "site-01", "Unit 01"),
906
+ deviceAt(
907
+ "core-02",
908
+ [ROUTER_LABEL, SWITCH_LABEL, MANAGED_LABEL],
909
+ "site-02",
910
+ "Unit 02",
911
+ ),
912
+ routerAt("router-02-1", "site-02", "Unit 02"),
913
+ switchAt("switch-02-1", "site-02", "Unit 02"),
914
+ ]);
915
+
916
+ expect(dualCompetesWithARouter.links).toEqual([
917
+ { fromDeviceId: "switch-01-1", toDeviceId: "core-01" },
918
+ ]);
919
+ expect(failuresOf(dualCompetesWithARouter)).toEqual([
920
+ {
921
+ siteId: "site-02",
922
+ siteName: "Unit 02",
923
+ reason: "ambiguousParent",
924
+ matchedParentCount: 2,
925
+ matchedChildCount: 2,
926
+ },
927
+ ]);
928
+ });
929
+
930
+ it("never draws the same pair twice under site scope", () => {
931
+ /*
932
+ * Sites are disjoint buckets and the builder merges by pair, so a repeated
933
+ * pair would silently become one edge on the map while inflating every
934
+ * count the rule list shows.
935
+ */
936
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
937
+ scopedRule(),
938
+ units(1, 4, 1, 3),
939
+ );
940
+
941
+ const pairs: Set<string> = new Set<string>(
942
+ outcome.links.map(
943
+ (link: { fromDeviceId: string; toDeviceId: string }) => {
944
+ return `${link.fromDeviceId}->${link.toDeviceId}`;
945
+ },
946
+ ),
947
+ );
948
+
949
+ expect(outcome.links).toHaveLength(12);
950
+ expect(pairs.size).toBe(outcome.links.length);
951
+ });
952
+
953
+ it("is exactly project scope when every device is in one site", () => {
954
+ /*
955
+ * One site is one universe, so the partition is the identity and the two
956
+ * scopes cannot disagree. This is what makes the column safe to flip on a
957
+ * single-site project: the map does not move.
958
+ */
959
+ const devices: Array<LinkRuleDeviceInput> = unit(1, 1, 3);
960
+
961
+ const siteScoped: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
962
+ scopedRule(),
963
+ devices,
964
+ );
965
+ const projectScoped: LinkRuleOutcome =
966
+ NetworkDeviceLinkRuleUtil.resolveRule(
967
+ scopedRule({ scope: "Project" }),
968
+ devices,
969
+ );
970
+
971
+ expect(siteScoped.links).toEqual(projectScoped.links);
972
+ expect(siteScoped.matchedChildCount).toBe(projectScoped.matchedChildCount);
973
+ expect(siteScoped.matchedParentCount).toBe(
974
+ projectScoped.matchedParentCount,
975
+ );
976
+ expect(siteScoped.skipReason).toBe(projectScoped.skipReason);
977
+ });
978
+
979
+ it("reports noSiteResolved rather than borrowing one site's reason", () => {
980
+ /*
981
+ * Unit 01 has no router and Unit 02 has two. Reporting either reason for
982
+ * the rule as a whole would be a false statement about the other site, and
983
+ * the mixture is exactly why this reason exists.
984
+ */
985
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
986
+ scopedRule(),
987
+ [...unit(1, 0, 2), ...unit(2, 2, 1)],
988
+ );
989
+
990
+ expect(outcome.links).toEqual([]);
991
+ expect(outcome.skipReason).toBe("noSiteResolved");
992
+ expect(NetworkDeviceLinkRuleUtil.describeOutcome(outcome)).toBe(
993
+ "No site resolved this rule.",
994
+ );
995
+ expect(failuresOf(outcome)).toHaveLength(2);
996
+ expect(outcome.applicableSiteCount).toBe(2);
997
+ });
998
+
999
+ it("counts every site that drew in the coverage fraction's numerator", () => {
1000
+ /*
1001
+ * "Drawing in X of Y sites" is subtraction, so it is only true if the
1002
+ * sites that failed and the sites that drew partition the applicable set
1003
+ * with nothing double-counted and nothing missing. Checked against the
1004
+ * links themselves rather than against the counters.
1005
+ */
1006
+ const devices: Array<LinkRuleDeviceInput> = [
1007
+ ...unit(1, 1, 2),
1008
+ ...unit(2, 1, 1),
1009
+ ...unit(3, 0, 2),
1010
+ ...unit(4, 2, 1),
1011
+ ...unit(5, 1, 0),
1012
+ deviceAt("camera-1", ["label-camera"], "site-06", "Unit 06"),
1013
+ ];
1014
+
1015
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1016
+ scopedRule(),
1017
+ devices,
1018
+ );
1019
+
1020
+ const siteOf: Map<string, string> = siteIndexOf(devices);
1021
+ const sitesThatDrew: Set<string> = new Set<string>();
1022
+ for (const link of outcome.links) {
1023
+ sitesThatDrew.add(siteOf.get(link.fromDeviceId)!);
1024
+ sitesThatDrew.add(siteOf.get(link.toDeviceId)!);
1025
+ }
1026
+
1027
+ expect(outcome.applicableSiteCount).toBe(4);
1028
+ expect(failuresOf(outcome)).toHaveLength(2);
1029
+ expect(sitesThatDrew.size).toBe(
1030
+ (outcome.applicableSiteCount || 0) - failuresOf(outcome).length,
1031
+ );
1032
+ });
1033
+
1034
+ it("does not touch the device array it was handed", () => {
1035
+ /*
1036
+ * resolveRules hands the SAME array instance to every rule in turn, so a
1037
+ * resolver that sorted or bucketed in place would make rule two see a
1038
+ * different project from rule one — and the bug would only show up in
1039
+ * projects with more than one rule.
1040
+ */
1041
+ const devices: Array<LinkRuleDeviceInput> = [
1042
+ ...units(1, 3, 1, 2),
1043
+ switchAt("floater-1"),
1044
+ ];
1045
+ const before: string = JSON.stringify(devices);
1046
+
1047
+ const alone: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1048
+ scopedRule(),
1049
+ devices,
1050
+ );
1051
+
1052
+ const outcomes: Array<LinkRuleOutcome> =
1053
+ NetworkDeviceLinkRuleUtil.resolveRules(
1054
+ [
1055
+ scopedRule({ id: "rule-first", scope: "Project" }),
1056
+ scopedRule({ id: "rule-second", childLabelIds: ["label-camera"] }),
1057
+ scopedRule(),
1058
+ ],
1059
+ devices,
1060
+ );
1061
+
1062
+ expect(JSON.stringify(devices)).toBe(before);
1063
+ // Third in a queue, and still the outcome it produces on its own.
1064
+ expect(outcomes[2]).toEqual(alone);
1065
+ });
1066
+
1067
+ it("does not reach across the site tree from an ancestor site", () => {
1068
+ /*
1069
+ * DELIBERATE, not a gap: sites are matched exactly, so a router filed
1070
+ * under the campus does not parent the switches filed under each building
1071
+ * inside it. Resolving up the tree would mean the map silently depends on
1072
+ * a hierarchy the operator can restructure at any time, and every building
1073
+ * would suddenly share one uplink. The rule refuses and names the
1074
+ * buildings instead, which is a fix the operator can actually make.
1075
+ */
1076
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1077
+ scopedRule(),
1078
+ [
1079
+ routerAt("campus-core", "site-campus", "Campus"),
1080
+ switchAt("switch-a-1", "site-bldg-a", "Building A"),
1081
+ switchAt("switch-a-2", "site-bldg-a", "Building A"),
1082
+ switchAt("switch-b-1", "site-bldg-b", "Building B"),
1083
+ ],
1084
+ );
1085
+
1086
+ expect(outcome.links).toEqual([]);
1087
+ expect(outcome.skipReason).toBe("noSiteResolved");
1088
+ expect(failuresOf(outcome)).toEqual([
1089
+ {
1090
+ siteId: "site-bldg-a",
1091
+ siteName: "Building A",
1092
+ reason: "noParentMatched",
1093
+ matchedParentCount: 0,
1094
+ matchedChildCount: 2,
1095
+ },
1096
+ {
1097
+ siteId: "site-bldg-b",
1098
+ siteName: "Building B",
1099
+ reason: "noParentMatched",
1100
+ matchedParentCount: 0,
1101
+ matchedChildCount: 1,
1102
+ },
1103
+ ]);
1104
+ // The campus itself had nothing to place, so it is not counted against.
1105
+ expect(outcome.applicableSiteCount).toBe(2);
1106
+ });
1107
+ });
1108
+
1109
+ describe("NetworkDeviceLinkRuleUtil.getWarning", () => {
1110
+ it("stays silent for a rule that is doing its job, in either scope", () => {
1111
+ const projectScoped: LinkRuleOutcome =
1112
+ NetworkDeviceLinkRuleUtil.resolveRule(
1113
+ scopedRule({ scope: "Project" }),
1114
+ unit(1, 1, 3),
1115
+ );
1116
+ const siteScoped: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1117
+ scopedRule(),
1118
+ units(1, 6, 1, 2),
1119
+ );
1120
+
1121
+ expect(NetworkDeviceLinkRuleUtil.getWarning(projectScoped)).toBeNull();
1122
+ expect(NetworkDeviceLinkRuleUtil.getWarning(siteScoped)).toBeNull();
1123
+ });
1124
+
1125
+ it("echoes describeOutcome verbatim for every project-scoped skip", () => {
1126
+ /*
1127
+ * The project path is what the topology API used to compute inline, and
1128
+ * the banner and the rule list render the same rule side by side. Asserted
1129
+ * against describeOutcome's OWN return value rather than against copies of
1130
+ * its sentences, so the two can never be edited apart.
1131
+ */
1132
+ const skips: Array<ScopeScenario> = [
1133
+ {
1134
+ label: "disabled",
1135
+ rule: scopedRule({ scope: "Project", isEnabled: false }),
1136
+ devices: unit(1, 1, 2),
1137
+ },
1138
+ {
1139
+ label: "noChildLabels",
1140
+ rule: scopedRule({ scope: "Project", childLabelIds: [] }),
1141
+ devices: unit(1, 1, 2),
1142
+ },
1143
+ {
1144
+ label: "noParentLabels",
1145
+ rule: scopedRule({ scope: "Project", parentLabelIds: [] }),
1146
+ devices: unit(1, 1, 2),
1147
+ },
1148
+ {
1149
+ label: "noParentMatched",
1150
+ rule: scopedRule({ scope: "Project" }),
1151
+ devices: unit(1, 0, 2),
1152
+ },
1153
+ {
1154
+ label: "ambiguousParent",
1155
+ rule: scopedRule({ scope: "Project" }),
1156
+ devices: units(1, 2, 1, 2),
1157
+ },
1158
+ {
1159
+ label: "noChildrenMatched",
1160
+ rule: scopedRule({ scope: "Project" }),
1161
+ devices: unit(1, 1, 0),
1162
+ },
1163
+ ];
1164
+
1165
+ for (const scenario of skips) {
1166
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1167
+ scenario.rule,
1168
+ scenario.devices,
1169
+ );
1170
+ const warning: ResolvedWarning = warningOf(outcome);
1171
+
1172
+ expect({ label: scenario.label, reason: warning.reason }).toEqual({
1173
+ label: scenario.label,
1174
+ reason: outcome.skipReason,
1175
+ });
1176
+ expect({ label: scenario.label, message: warning.message }).toEqual({
1177
+ label: scenario.label,
1178
+ message: NetworkDeviceLinkRuleUtil.describeOutcome(outcome),
1179
+ });
1180
+ expect(warning.ruleId).toBe("rule-uplink");
1181
+ expect(warning.ruleName).toBe("Switch uplinks");
1182
+ }
1183
+ });
1184
+
1185
+ it("warns about the fourteenth unit while the other thirteen keep drawing", () => {
1186
+ /*
1187
+ * The regression this whole change is for. The warning predicate used to
1188
+ * be `links.length === 0`, which this outcome passes — 39 links — while
1189
+ * the operator's fourteenth building is genuinely broken. Silence here
1190
+ * means a site that has been quietly missing its uplinks since somebody
1191
+ * racked a second router in it.
1192
+ */
1193
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1194
+ scopedRule(),
1195
+ [...units(1, 13, 1, 3), ...unit(14, 2, 3)],
1196
+ );
1197
+
1198
+ const warning: ResolvedWarning = warningOf(outcome);
1199
+
1200
+ expect(outcome.links.length).toBeGreaterThan(0);
1201
+ expect(outcome.links).toHaveLength(39);
1202
+ expect(warning.reason).toBe("ambiguousParent");
1203
+ expect(warning.message).toContain("Drawing in 13 of 14 sites.");
1204
+ expect(warning.message).toContain("Unit 14");
1205
+ expect(warning.message).toContain("2 devices carry the parent labels");
1206
+ });
1207
+
1208
+ it("folds mixed failures into one bullet, ambiguity before missing parents", () => {
1209
+ /*
1210
+ * One bullet per rule, never one per site: the banner's length has to be
1211
+ * bounded by the number of rules the operator wrote, not by the number of
1212
+ * sites they own. Ambiguity leads because it is the more specific
1213
+ * complaint — "you have two of these" is a five-second fix, "you have
1214
+ * none" may be a truck roll.
1215
+ */
1216
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1217
+ scopedRule(),
1218
+ [...unit(1, 2, 2), ...unit(2, 0, 1), ...unit(3, 0, 2), ...unit(4, 1, 1)],
1219
+ );
1220
+
1221
+ const warning: ResolvedWarning = warningOf(outcome);
1222
+
1223
+ expect(failuresOf(outcome)).toHaveLength(3);
1224
+ expect(warning.reason).toBe("ambiguousParent");
1225
+
1226
+ const ambiguity: number = warning.message.indexOf(
1227
+ "2 devices carry the parent labels in Unit 01.",
1228
+ );
1229
+ const missing: number = warning.message.indexOf(
1230
+ "No device carries the parent labels in Unit 02 and Unit 03,",
1231
+ );
1232
+
1233
+ expect(ambiguity).toBeGreaterThan(-1);
1234
+ expect(missing).toBeGreaterThan(-1);
1235
+ expect(ambiguity).toBeLessThan(missing);
1236
+ expect(warning.message).toContain("3 devices there have no uplink.");
1237
+ });
1238
+
1239
+ it("uses the project's own sentence when nothing carries the child labels", () => {
1240
+ /*
1241
+ * No site failed and nothing was stranded, so there is nothing site-shaped
1242
+ * to say — and inventing a site-scoped phrasing here would give the
1243
+ * operator two different sentences for the same situation depending on a
1244
+ * column they may not know they set.
1245
+ */
1246
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1247
+ scopedRule(),
1248
+ units(1, 3, 1, 0),
1249
+ );
1250
+
1251
+ expect(outcome.skipReason).toBe("noChildrenMatched");
1252
+ expect(warningOf(outcome).message).toBe(
1253
+ "No device carries the child labels yet.",
1254
+ );
1255
+ });
1256
+
1257
+ it("opens with the headline that matches whether anything drew at all", () => {
1258
+ const nothingDrew: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1259
+ scopedRule(),
1260
+ units(1, 2, 0, 1),
1261
+ );
1262
+ const somethingDrew: LinkRuleOutcome =
1263
+ NetworkDeviceLinkRuleUtil.resolveRule(scopedRule(), [
1264
+ ...unit(1, 1, 2),
1265
+ ...unit(2, 0, 1),
1266
+ ]);
1267
+
1268
+ expect(
1269
+ warningOf(nothingDrew).message.startsWith("No site resolved this rule."),
1270
+ ).toBe(true);
1271
+ expect(warningOf(somethingDrew).message.startsWith("Drawing in")).toBe(
1272
+ true,
1273
+ );
1274
+ expect(warningOf(somethingDrew).message).toContain(
1275
+ "Drawing in 1 of 2 sites.",
1276
+ );
1277
+ });
1278
+
1279
+ it("omits the coverage fraction when only one site applies", () => {
1280
+ /*
1281
+ * "Drawing in 1 of 1 sites" is a sentence that costs the reader time and
1282
+ * tells them nothing. The unsited clause still has to be emitted, so this
1283
+ * pins that the two are independent rather than one gating the other.
1284
+ */
1285
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1286
+ scopedRule(),
1287
+ [...unit(1, 1, 2), switchAt("floater-1"), switchAt("floater-2")],
1288
+ );
1289
+
1290
+ const warning: ResolvedWarning = warningOf(outcome);
1291
+
1292
+ expect(outcome.applicableSiteCount).toBe(1);
1293
+ expect(outcome.links).toHaveLength(2);
1294
+ expect(warning.reason).toBe("devicesWithoutSite");
1295
+ expect(warning.message).toBe(
1296
+ "2 devices carrying the child labels are not assigned to a site, so this site-scoped rule skips them.",
1297
+ );
1298
+ expect(warning.message).not.toContain("Drawing in");
1299
+ expect(warning.message).not.toContain("of 1 sites");
1300
+ });
1301
+
1302
+ it("names three sites and counts the rest exactly", () => {
1303
+ /*
1304
+ * Three names is enough to recognise a pattern; fifteen is a wall of text
1305
+ * in a banner. The COUNT stays exact even though the names are elided, so
1306
+ * the operator still knows the true blast radius.
1307
+ */
1308
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1309
+ scopedRule(),
1310
+ units(1, 15, 0, 1),
1311
+ );
1312
+
1313
+ const warning: ResolvedWarning = warningOf(outcome);
1314
+ const allNames: Array<string> = [];
1315
+ for (let index: number = 1; index <= 15; index++) {
1316
+ allNames.push(`Unit ${twoDigit(index)}`);
1317
+ }
1318
+ const named: Array<string> = allNames.filter((name: string): boolean => {
1319
+ return warning.message.includes(name);
1320
+ });
1321
+
1322
+ expect(failuresOf(outcome)).toHaveLength(15);
1323
+ expect(named).toEqual(["Unit 01", "Unit 02", "Unit 03"]);
1324
+ expect(warning.message).toContain("and 12 more sites");
1325
+ expect(warning.message).toContain("15 devices there have no uplink.");
1326
+ });
1327
+
1328
+ it("says 'and 1 more site' rather than 'and 1 more sites'", () => {
1329
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1330
+ scopedRule(),
1331
+ units(1, 4, 0, 1),
1332
+ );
1333
+
1334
+ const warning: ResolvedWarning = warningOf(outcome);
1335
+
1336
+ expect(warning.message).toContain(
1337
+ "Unit 01, Unit 02, Unit 03 and 1 more site,",
1338
+ );
1339
+ expect(warning.message).not.toContain("more sites");
1340
+ });
1341
+
1342
+ it("describes a site with no name instead of printing its id", () => {
1343
+ /*
1344
+ * The id is a database key: it means nothing to the reader and, printed in
1345
+ * a banner, it is the one part of the sentence they might paste into a
1346
+ * ticket. A site with no name is missing a name, which is a different
1347
+ * problem from the one this warning is about.
1348
+ */
1349
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1350
+ scopedRule(),
1351
+ [switchAt("switch-x", "site-9f3c-never-printed")],
1352
+ );
1353
+
1354
+ const warning: ResolvedWarning = warningOf(outcome);
1355
+
1356
+ expect(warning.message).toBe(
1357
+ "No site resolved this rule. No device carries the parent labels in an unnamed site, so 1 device there has no uplink.",
1358
+ );
1359
+ expect(warning.message).not.toContain("site-9f3c-never-printed");
1360
+ });
1361
+
1362
+ it("truncates a site name past 40 characters to 39 and an ellipsis", () => {
1363
+ /*
1364
+ * Site names are ShortText and run to 100 characters. Three of them
1365
+ * unabridged would be a 300-character site list inside a one-line banner.
1366
+ */
1367
+ const longName: string = "Manufacturing Plant Seven East Annex Riser Room";
1368
+
1369
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1370
+ scopedRule(),
1371
+ [switchAt("switch-x", "site-long", longName)],
1372
+ );
1373
+
1374
+ const warning: ResolvedWarning = warningOf(outcome);
1375
+
1376
+ expect(longName.length).toBeGreaterThan(40);
1377
+ expect(warning.message).toContain(
1378
+ "Manufacturing Plant Seven East Annex Ri…",
1379
+ );
1380
+ expect(warning.message).not.toContain(longName);
1381
+ });
1382
+
1383
+ it("produces a byte-identical message however the devices are ordered", () => {
1384
+ /*
1385
+ * The live view refetches every sixty seconds and the device query has no
1386
+ * ORDER BY, so an unsorted site list would reshuffle the banner on every
1387
+ * poll and read as a change that did not happen. Links keep following
1388
+ * first appearance in the array — that is the map's stable draw order, not
1389
+ * the banner's.
1390
+ */
1391
+ const inOrder: Array<LinkRuleDeviceInput> = [
1392
+ ...unit(5, 1, 1),
1393
+ ...unit(3, 0, 1),
1394
+ ...unit(1, 1, 1),
1395
+ ...unit(4, 2, 1),
1396
+ ...unit(2, 0, 1),
1397
+ ];
1398
+ const shuffled: Array<LinkRuleDeviceInput> = [
1399
+ ...unit(1, 1, 1),
1400
+ ...unit(2, 0, 1),
1401
+ ...unit(4, 2, 1),
1402
+ ...unit(5, 1, 1),
1403
+ ...unit(3, 0, 1),
1404
+ ];
1405
+
1406
+ const first: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1407
+ scopedRule(),
1408
+ inOrder,
1409
+ );
1410
+ const second: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1411
+ scopedRule(),
1412
+ shuffled,
1413
+ );
1414
+
1415
+ expect(warningOf(first).message).toBe(warningOf(second).message);
1416
+ expect(warningOf(first).message).toContain(
1417
+ "Unit 02 and Unit 03, so 2 devices there have no uplink.",
1418
+ );
1419
+
1420
+ expect(first.links).toEqual([
1421
+ { fromDeviceId: "switch-05-1", toDeviceId: "router-05-1" },
1422
+ { fromDeviceId: "switch-01-1", toDeviceId: "router-01-1" },
1423
+ ]);
1424
+ expect(second.links).toEqual([
1425
+ { fromDeviceId: "switch-01-1", toDeviceId: "router-01-1" },
1426
+ { fromDeviceId: "switch-05-1", toDeviceId: "router-05-1" },
1427
+ ]);
1428
+ });
1429
+
1430
+ it("reads 'has' for one stranded device and 'have' for several", () => {
1431
+ const one: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1432
+ scopedRule(),
1433
+ [...unit(1, 1, 1), ...unit(2, 0, 1)],
1434
+ );
1435
+ const several: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1436
+ scopedRule(),
1437
+ [...unit(1, 1, 1), ...unit(2, 0, 4)],
1438
+ );
1439
+
1440
+ expect(warningOf(one).message).toContain(
1441
+ "so 1 device there has no uplink.",
1442
+ );
1443
+ expect(warningOf(several).message).toContain(
1444
+ "so 4 devices there have no uplink.",
1445
+ );
1446
+ });
1447
+
1448
+ it("keeps even the worst case short enough to read", () => {
1449
+ /*
1450
+ * The banner is one line per rule. Thirty failing sites of both kinds,
1451
+ * hundred-character names and stranded unsited devices is the loudest a
1452
+ * single rule can be, and it still has to fit somewhere a human will read
1453
+ * it rather than scroll past.
1454
+ */
1455
+ const hundredCharName: (index: number) => string = (
1456
+ index: number,
1457
+ ): string => {
1458
+ const base: string = `Unit ${twoDigit(index)} Distribution Annex `;
1459
+ return (base + "0123456789".repeat(10)).slice(0, 100);
1460
+ };
1461
+
1462
+ const devices: Array<LinkRuleDeviceInput> = [];
1463
+ for (let index: number = 1; index <= 15; index++) {
1464
+ const siteId: string = `ambiguous-${twoDigit(index)}`;
1465
+ const siteName: string = hundredCharName(index);
1466
+ devices.push(routerAt(`amb-router-a-${index}`, siteId, siteName));
1467
+ devices.push(routerAt(`amb-router-b-${index}`, siteId, siteName));
1468
+ devices.push(switchAt(`amb-switch-${index}`, siteId, siteName));
1469
+ }
1470
+ for (let index: number = 16; index <= 30; index++) {
1471
+ const siteId: string = `parentless-${twoDigit(index)}`;
1472
+ const siteName: string = hundredCharName(index);
1473
+ devices.push(switchAt(`orphan-switch-${index}`, siteId, siteName));
1474
+ }
1475
+ devices.push(switchAt("floater-1"));
1476
+ devices.push(switchAt("floater-2"));
1477
+
1478
+ const outcome: LinkRuleOutcome = NetworkDeviceLinkRuleUtil.resolveRule(
1479
+ scopedRule(),
1480
+ devices,
1481
+ );
1482
+ const warning: ResolvedWarning = warningOf(outcome);
1483
+
1484
+ expect(hundredCharName(1)).toHaveLength(100);
1485
+ expect(failuresOf(outcome)).toHaveLength(30);
1486
+ expect(outcome.unsitedChildDeviceCount).toBe(2);
1487
+ expect(warning.message.length).toBeLessThanOrEqual(700);
1488
+ });
1489
+ });