@oneuptime/common 12.0.34 → 13.0.0

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 (259) hide show
  1. package/Models/DatabaseModels/GlobalConfig.ts +12 -12
  2. package/Models/DatabaseModels/InventoryItem.ts +44 -1
  3. package/Models/DatabaseModels/NetworkDevice.ts +126 -0
  4. package/Server/EnvironmentConfig.ts +47 -16
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Infrastructure/Redis.ts +8 -8
  8. package/Server/Infrastructure/Status.ts +10 -4
  9. package/Server/Services/DatabaseService.ts +65 -13
  10. package/Server/Services/InventoryItemService.ts +31 -1
  11. package/Server/Services/MonitorTemplateService.ts +140 -19
  12. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
  13. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
  14. package/Server/Services/NetworkDeviceService.ts +69 -0
  15. package/Server/Services/UserNotificationRuleService.ts +76 -25
  16. package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
  17. package/Server/Types/Database/QueryUtil.ts +56 -0
  18. package/Server/Types/Markdown.ts +93 -1
  19. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
  20. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
  21. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
  25. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
  26. package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
  27. package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
  28. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
  29. package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
  30. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
  31. package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
  32. package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
  33. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
  34. package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
  35. package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
  36. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
  37. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
  38. package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
  39. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
  40. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
  41. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
  42. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
  43. package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
  44. package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
  45. package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
  46. package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
  47. package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
  48. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
  49. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
  50. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
  51. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
  52. package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
  53. package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
  54. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
  55. package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
  56. package/Tests/Models/InventoryItemStatus.test.ts +147 -0
  57. package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
  58. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
  59. package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
  60. package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
  61. package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
  62. package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
  63. package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
  64. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
  65. package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
  66. package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
  67. package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
  68. package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
  69. package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
  70. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
  72. package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
  73. package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
  74. package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
  75. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
  76. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
  77. package/Tests/Server/Types/Markdown.test.ts +194 -1
  78. package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
  79. package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
  80. package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
  81. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
  82. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
  83. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
  84. package/Tests/Server/Utils/EventLoop.test.ts +359 -0
  85. package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
  86. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
  87. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
  88. package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
  90. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
  91. package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
  92. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
  93. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
  94. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
  95. package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
  96. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
  97. package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
  98. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
  99. package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
  100. package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
  101. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
  102. package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
  103. package/Tests/Types/JSONFunctions.test.ts +40 -0
  104. package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
  105. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
  106. package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
  107. package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
  108. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
  109. package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
  110. package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
  111. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
  112. package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
  113. package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
  114. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  115. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
  116. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
  117. package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
  118. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
  119. package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
  120. package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
  121. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
  122. package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
  123. package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
  124. package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
  125. package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
  126. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  127. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
  128. package/Types/Dashboard/DashboardVariable.ts +8 -0
  129. package/Types/JSON.ts +4 -0
  130. package/Types/JSONFunctions.ts +16 -0
  131. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  132. package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
  133. package/Types/SerializableObjectDictionary.ts +4 -0
  134. package/Types/Telemetry/InventoryLiveness.ts +56 -0
  135. package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
  136. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
  137. package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
  138. package/UI/esbuild-config.js +6 -0
  139. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
  140. package/Utils/Dashboard/LabelVariable.ts +152 -0
  141. package/Utils/Dashboard/VariableUrlState.ts +27 -5
  142. package/Utils/MetricUnitUtil.ts +34 -0
  143. package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
  144. package/Utils/Monitor/MetricValueFormatter.ts +342 -0
  145. package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
  146. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
  147. package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
  148. package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
  149. package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
  150. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
  152. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
  154. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  155. package/build/dist/Server/EnvironmentConfig.js +44 -12
  156. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  157. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
  158. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
  159. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  160. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  161. package/build/dist/Server/Infrastructure/Redis.js +8 -8
  162. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  163. package/build/dist/Server/Infrastructure/Status.js +10 -4
  164. package/build/dist/Server/Infrastructure/Status.js.map +1 -1
  165. package/build/dist/Server/Services/DatabaseService.js +58 -13
  166. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  167. package/build/dist/Server/Services/InventoryItemService.js +28 -1
  168. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  169. package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
  170. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  171. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
  172. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  173. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
  174. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  175. package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
  176. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  177. package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
  178. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  179. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
  180. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
  181. package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
  182. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  183. package/build/dist/Server/Types/Markdown.js +75 -0
  184. package/build/dist/Server/Types/Markdown.js.map +1 -1
  185. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
  186. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  187. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
  188. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  189. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
  190. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
  192. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  193. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
  194. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  195. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
  196. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  197. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
  198. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
  199. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
  200. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
  201. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
  202. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
  203. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
  204. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  205. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
  206. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
  207. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
  208. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
  210. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  211. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
  212. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  213. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
  214. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
  215. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  216. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  217. package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
  218. package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
  219. package/build/dist/Types/JSON.js +1 -0
  220. package/build/dist/Types/JSON.js.map +1 -1
  221. package/build/dist/Types/JSONFunctions.js +13 -0
  222. package/build/dist/Types/JSONFunctions.js.map +1 -1
  223. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
  224. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  225. package/build/dist/Types/SerializableObjectDictionary.js +4 -0
  226. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  227. package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
  228. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
  229. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
  230. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
  231. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
  232. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  233. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
  234. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
  235. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
  236. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
  237. package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
  238. package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
  239. package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
  240. package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
  241. package/build/dist/Utils/MetricUnitUtil.js +26 -0
  242. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  243. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
  244. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
  245. package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
  246. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
  247. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
  248. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
  249. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
  250. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  251. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
  252. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  253. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
  254. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
  255. package/jest.config.json +2 -0
  256. package/package.json +1 -1
  257. package/test-setup.sh +9 -9
  258. package/tsconfig.json +51 -11
  259. package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
@@ -57,6 +57,17 @@ function isIpv4Address(value) {
57
57
  }
58
58
  // Endpoint nodes rendered per graph; the slice is deterministic (by MAC).
59
59
  const ENDPOINT_RENDER_CAP = 2000;
60
+ /*
61
+ * The built-in roles that sit at the top of a network, for the parent rule
62
+ * in drawAdoptedEndpoint when the project has not stamped its own answer.
63
+ * Mirrors the isCoreLayer flags in DEFAULT_NETWORK_DEVICE_ROLES, which is
64
+ * the seed every project's role table starts from.
65
+ */
66
+ const BUILT_IN_CORE_LAYER_ROLES = new Set([
67
+ "router",
68
+ "firewall",
69
+ "loadBalancer",
70
+ ]);
60
71
  /*
61
72
  * How long an ARP/FDB-learned endpoint keeps its "up" colour after the last
62
73
  * walk that saw it. Taken from the shared device rule at its default
@@ -128,6 +139,12 @@ export default class NetworkTopologyUtil {
128
139
  }
129
140
  // Kept aside so the diagnostics pass below can read it back per node.
130
141
  const isNeighborDiscoveryEnabledById = new Map();
142
+ /*
143
+ * Managed nodes by id, for the endpoint adoption pass: a device found
144
+ * in a switch's forwarding table is stamped with what the table knew
145
+ * about it (MAC, address, VLAN), and the parent rule reads its role.
146
+ */
147
+ const deviceNodeById = new Map();
131
148
  for (const device of devices) {
132
149
  isNeighborDiscoveryEnabledById.set(device.id, device.isNeighborDiscoveryEnabled);
133
150
  const deviceRole = resolveDeviceRole(device.deviceRole, {
@@ -150,6 +167,13 @@ export default class NetworkTopologyUtil {
150
167
  sysName: device.sysName,
151
168
  vendor: device.vendor,
152
169
  deviceModel: device.deviceModel,
170
+ /*
171
+ * The declared or learned MAC, whether or not a forwarding table
172
+ * has found it yet: the drawer shows it, and an operator checking
173
+ * why a device floats wants to see what the map was looking for.
174
+ * Normalised so it reads the same as the endpoint rows beside it.
175
+ */
176
+ macAddress: normalizeMac(device.macAddress),
153
177
  };
154
178
  /*
155
179
  * The configured role, when the project has one for this key. The
@@ -159,6 +183,7 @@ export default class NetworkTopologyUtil {
159
183
  */
160
184
  applyRoleStamp(deviceNode, stampForRoleKey(roleKeyForNode(device.deviceRole, deviceRole), roleIndex));
161
185
  nodes.push(deviceNode);
186
+ deviceNodeById.set(device.id, deviceNode);
162
187
  }
163
188
  const edgeByKey = new Map();
164
189
  /*
@@ -442,6 +467,19 @@ export default class NetworkTopologyUtil {
442
467
  let droppedEndpointCount = 0;
443
468
  let renderedEndpointCount = 0;
444
469
  let endpointsTruncated = false;
470
+ let adoptedEndpointCount = 0;
471
+ /*
472
+ * --- Endpoints that ARE managed devices ---
473
+ *
474
+ * A ping-only register, handset or kiosk is a NetworkDevice with a node
475
+ * of its own, and it is ALSO a MAC some switch learned on an access
476
+ * port - which until now was drawn as a second, anonymous node hanging
477
+ * off that switch while the device's own node floated with no link at
478
+ * all (issue #3489). The index below is what lets the two be recognised
479
+ * as one box, so the forwarding-table evidence draws the device's cable
480
+ * instead of a stranger's.
481
+ */
482
+ const adoption = NetworkTopologyUtil.planEndpointAdoption(devices, sortedEndpoints, deviceNodeIds, now);
445
483
  for (const endpoint of sortedEndpoints) {
446
484
  if (!endpoint.attachedNetworkDeviceId ||
447
485
  !deviceNodeIds.has(endpoint.attachedNetworkDeviceId)) {
@@ -449,6 +487,25 @@ export default class NetworkTopologyUtil {
449
487
  droppedEndpointCount++;
450
488
  continue;
451
489
  }
490
+ const adoptedDevice = adoption.deviceByEndpointId.get(endpoint.id);
491
+ if (adoptedDevice) {
492
+ /*
493
+ * Counted before the render cap, and never against it: an adopted
494
+ * endpoint mints no node, so it cannot crowd out one that does.
495
+ */
496
+ adoptedEndpointCount++;
497
+ if (adoption.drawnEndpointIds.has(endpoint.id)) {
498
+ NetworkTopologyUtil.drawAdoptedEndpoint({
499
+ endpoint: endpoint,
500
+ device: adoptedDevice,
501
+ deviceNode: deviceNodeById.get(adoptedDevice.id),
502
+ edgeByKey: edgeByKey,
503
+ edges: edges,
504
+ interfaceByDeviceAndIndex: interfaceByDeviceAndIndex,
505
+ });
506
+ }
507
+ continue;
508
+ }
452
509
  if (renderedEndpointCount >= ENDPOINT_RENDER_CAP) {
453
510
  endpointsTruncated = true;
454
511
  continue;
@@ -500,6 +557,7 @@ export default class NetworkTopologyUtil {
500
557
  toNodeId: nodeId,
501
558
  fromPort: fromPort,
502
559
  protocols: ["fdb"],
560
+ learnedByNodeId: endpoint.attachedNetworkDeviceId,
503
561
  fromInterface: switchInterface
504
562
  ? {
505
563
  interfaceIndex: switchInterface.interfaceIndex,
@@ -529,6 +587,7 @@ export default class NetworkTopologyUtil {
529
587
  edges,
530
588
  droppedEndpointCount,
531
589
  endpointsTruncated,
590
+ adoptedEndpointCount,
532
591
  suppressedNodeCount: 0,
533
592
  };
534
593
  }
@@ -546,9 +605,425 @@ export default class NetworkTopologyUtil {
546
605
  }),
547
606
  droppedEndpointCount,
548
607
  endpointsTruncated,
608
+ adoptedEndpointCount,
549
609
  suppressedNodeCount: nodes.length - visibleNodes.length,
550
610
  };
551
611
  }
612
+ /*
613
+ * Which endpoints are managed devices, and which ONE of them draws the
614
+ * device's cable.
615
+ *
616
+ * A device can match more than one row. The common way is hardware that
617
+ * was swapped keeping its address: the old unit's MAC is still a row in
618
+ * the endpoint table (nothing prunes it), stored on the device from the
619
+ * ARP pass, and the new unit's MAC is a fresher row that matches the
620
+ * device by address. Drawing both would put the register on two ports
621
+ * at once, one of them a port nothing has been plugged into for a
622
+ * month. So every matching row is ADOPTED - none of them is minted as a
623
+ * stranger - and the most recently seen one draws the link. A MAC match
624
+ * outranks an address match at equal freshness, and the MAC order the
625
+ * caller already sorted by settles the rest, so the choice never depends
626
+ * on query order.
627
+ *
628
+ * The price is that a genuinely dual-homed device draws one cable. That
629
+ * is rare among the devices this exists for, and it is the honest
630
+ * trade: one current cable beats one current and one stale.
631
+ */
632
+ static planEndpointAdoption(devices, sortedEndpoints, deviceNodeIds, now) {
633
+ const index = NetworkTopologyUtil.buildEndpointAdoptionIndex(devices);
634
+ const deviceByEndpointId = new Map();
635
+ const drawnEndpointIds = new Set();
636
+ if (index.size === 0) {
637
+ return { deviceByEndpointId, drawnEndpointIds };
638
+ }
639
+ const candidatesByDeviceId = new Map();
640
+ for (const endpoint of sortedEndpoints) {
641
+ if (!endpoint.attachedNetworkDeviceId ||
642
+ !deviceNodeIds.has(endpoint.attachedNetworkDeviceId)) {
643
+ continue;
644
+ }
645
+ const match = NetworkTopologyUtil.adoptionMatchForEndpoint(endpoint, index);
646
+ if (!match) {
647
+ continue;
648
+ }
649
+ /*
650
+ * An address is only evidence while it is current. A MAC names a
651
+ * box for good, but an address is re-leased: a row the ARP pass
652
+ * bound months ago may name a laptop that held the register's
653
+ * address before the register was plugged in. Such a row is left to
654
+ * be drawn as the stale (down-coloured) endpoint it is, rather than
655
+ * cabling the device to a stranger's port with a live node's
656
+ * confidence. The window is the one endpoint nodes age out on.
657
+ */
658
+ if (!match.matchedByMac &&
659
+ !NetworkTopologyUtil.isFreshSighting(endpoint, now)) {
660
+ continue;
661
+ }
662
+ deviceByEndpointId.set(endpoint.id, match.device);
663
+ const bucket = candidatesByDeviceId.get(match.device.id);
664
+ const candidate = {
665
+ endpoint: endpoint,
666
+ matchedByMac: match.matchedByMac,
667
+ };
668
+ if (bucket) {
669
+ bucket.push(candidate);
670
+ }
671
+ else {
672
+ candidatesByDeviceId.set(match.device.id, [candidate]);
673
+ }
674
+ }
675
+ for (const candidates of candidatesByDeviceId.values()) {
676
+ let chosen = candidates[0];
677
+ for (const candidate of candidates.slice(1)) {
678
+ if (NetworkTopologyUtil.isFresherSighting(candidate, chosen)) {
679
+ chosen = candidate;
680
+ }
681
+ }
682
+ drawnEndpointIds.add(chosen.endpoint.id);
683
+ }
684
+ return { deviceByEndpointId, drawnEndpointIds };
685
+ }
686
+ static isFreshSighting(endpoint, now) {
687
+ if (!endpoint.lastSeenAt) {
688
+ return false;
689
+ }
690
+ const ageMs = now.getTime() - new Date(endpoint.lastSeenAt).getTime();
691
+ return ageMs <= ENDPOINT_FRESH_WINDOW_MS;
692
+ }
693
+ /*
694
+ * Strictly fresher: later lastSeenAt, then a MAC match over an address
695
+ * match. Equal on both keeps the incumbent, which is the earlier entry
696
+ * in MAC order.
697
+ */
698
+ static isFresherSighting(candidate, incumbent) {
699
+ const candidateSeen = candidate.endpoint.lastSeenAt
700
+ ? new Date(candidate.endpoint.lastSeenAt).getTime()
701
+ : Number.NEGATIVE_INFINITY;
702
+ const incumbentSeen = incumbent.endpoint.lastSeenAt
703
+ ? new Date(incumbent.endpoint.lastSeenAt).getTime()
704
+ : Number.NEGATIVE_INFINITY;
705
+ if (candidateSeen !== incumbentSeen) {
706
+ return candidateSeen > incumbentSeen;
707
+ }
708
+ return candidate.matchedByMac && !incumbent.matchedByMac;
709
+ }
710
+ /*
711
+ * Every key an endpoint could be recognised as a managed device by.
712
+ *
713
+ * Two kinds, strongest first:
714
+ *
715
+ * "mac" - the device's declared or ARP-learned MAC against the MAC the
716
+ * switch learned. A MAC is unique per project among endpoints
717
+ * (the endpoint table enforces it), so this needs no scoping.
718
+ * "ip" - the device's hostname, when it is an IPv4 literal, against
719
+ * the address a router's ARP table bound the MAC to. Scoped to
720
+ * the SITE: every branch has a 10.0.0.5, and an unscoped match
721
+ * would cable one register in every store to whichever switch
722
+ * happened to learn a MAC that answers at that address. No site
723
+ * on either side is a site of its own, so a project that never
724
+ * set sites up still matches - project-wide, as it has to.
725
+ *
726
+ * The same ambiguity guard as the neighbour matcher: a key two devices
727
+ * both claim is deleted rather than resolved, because a cable drawn to
728
+ * the wrong box is worse than one not drawn.
729
+ *
730
+ * Interface MACs (TopologyDeviceInput.macAddresses) are deliberately NOT
731
+ * keys here. A walked switch's interface MACs turn up in its uplink
732
+ * neighbours' forwarding tables as transit traffic, and adopting those
733
+ * would redraw every switch-to-switch cable LLDP already reports from a
734
+ * weaker source. The declared MAC is different in kind: an operator (or
735
+ * the ARP pass) put it there to say "this is the box".
736
+ */
737
+ static buildEndpointAdoptionIndex(devices) {
738
+ const index = new Map();
739
+ const ambiguous = new Set();
740
+ for (const device of devices) {
741
+ for (const key of NetworkTopologyUtil.adoptionKeysForDevice(device)) {
742
+ const existing = index.get(key);
743
+ if (existing && existing.id !== device.id) {
744
+ ambiguous.add(key);
745
+ continue;
746
+ }
747
+ index.set(key, device);
748
+ }
749
+ }
750
+ /*
751
+ * A walked device's interface MACs are not adoption keys (see above),
752
+ * but they ARE evidence about who owns a MAC. A declared MAC that some
753
+ * walked device reports as one of its own interfaces is a contradiction
754
+ * - a register registered at a switch's SVI address and stamped with
755
+ * the switch's MAC, say - and the neighbour matcher already refuses
756
+ * that key. Refusing it here too keeps the two from disagreeing about
757
+ * which box a MAC is.
758
+ */
759
+ for (const device of devices) {
760
+ for (const rawMac of device.macAddresses || []) {
761
+ const mac = normalizeMac(rawMac);
762
+ if (!mac) {
763
+ continue;
764
+ }
765
+ const key = NetworkTopologyUtil.adoptionMacKey(mac);
766
+ const claimant = index.get(key);
767
+ if (claimant && claimant.id !== device.id) {
768
+ ambiguous.add(key);
769
+ }
770
+ }
771
+ }
772
+ for (const key of ambiguous) {
773
+ index.delete(key);
774
+ }
775
+ return index;
776
+ }
777
+ static adoptionKeysForDevice(device) {
778
+ const keys = [];
779
+ const mac = normalizeMac(device.macAddress);
780
+ if (mac) {
781
+ keys.push(NetworkTopologyUtil.adoptionMacKey(mac));
782
+ }
783
+ /*
784
+ * hostname only, never `name`: a name is an operator's label, and one
785
+ * that happens to be an IP literal is not a claim that the device
786
+ * answers there - the same line matchKeysForDevice draws.
787
+ */
788
+ const hostname = NetworkTopologyUtil.normalizeKey(device.hostname);
789
+ if (isIpv4Address(hostname)) {
790
+ keys.push(NetworkTopologyUtil.adoptionIpKey(device.siteId, hostname));
791
+ }
792
+ return keys;
793
+ }
794
+ /*
795
+ * The device this endpoint is, if it is one, and by which evidence. MAC
796
+ * first: it is the stronger claim, and the only one that survives an
797
+ * address change.
798
+ */
799
+ static adoptionMatchForEndpoint(endpoint, index) {
800
+ const mac = normalizeMac(endpoint.macAddress);
801
+ if (mac) {
802
+ const byMac = index.get(NetworkTopologyUtil.adoptionMacKey(mac));
803
+ if (byMac) {
804
+ return { device: byMac, matchedByMac: true };
805
+ }
806
+ }
807
+ const ip = NetworkTopologyUtil.normalizeKey(endpoint.ipAddress);
808
+ if (isIpv4Address(ip)) {
809
+ const byIp = index.get(NetworkTopologyUtil.adoptionIpKey(endpoint.siteId, ip));
810
+ if (byIp) {
811
+ return { device: byIp, matchedByMac: false };
812
+ }
813
+ }
814
+ return undefined;
815
+ }
816
+ static adoptionMacKey(normalizedMac) {
817
+ return `mac\u0000${normalizedMac}`;
818
+ }
819
+ static adoptionIpKey(siteId, normalizedIp) {
820
+ /*
821
+ * null, undefined, "" and whitespace all normalise to the same "no
822
+ * site" key - the convention NetworkDeviceLinkRuleUtil already uses.
823
+ */
824
+ const siteKey = (siteId || "").trim();
825
+ return `ip\u0000${siteKey}\u0000${normalizedIp}`;
826
+ }
827
+ /*
828
+ * Draws one adopted endpoint: an "fdb" edge from the switch that learned
829
+ * the MAC to the device's own node, merged into whatever edge already
830
+ * joins that pair, plus the identity the tables knew stamped on the node.
831
+ *
832
+ * A device found in its OWN tables is a self-reference (its address in
833
+ * its own ARP cache, say) and draws nothing - an edge from a node to
834
+ * itself is not a cable. It still counts as adopted: the row is not an
835
+ * endpoint, and it must not be minted as one.
836
+ */
837
+ static drawAdoptedEndpoint(data) {
838
+ var _a;
839
+ const switchId = data.endpoint.attachedNetworkDeviceId;
840
+ const deviceId = data.device.id;
841
+ if (data.deviceNode) {
842
+ NetworkTopologyUtil.stampAdoptedIdentity(data.deviceNode, data.endpoint);
843
+ }
844
+ if (switchId === deviceId) {
845
+ return;
846
+ }
847
+ // Port label + interface state on the switch end, when identifiable.
848
+ let switchInterface = undefined;
849
+ if (data.endpoint.attachedInterfaceIndex !== undefined) {
850
+ switchInterface = data.interfaceByDeviceAndIndex.get(`${switchId}::${data.endpoint.attachedInterfaceIndex}`);
851
+ }
852
+ let switchPort = data.endpoint.attachedPortName || (switchInterface === null || switchInterface === void 0 ? void 0 : switchInterface.name);
853
+ if (!switchPort && data.endpoint.attachedInterfaceIndex !== undefined) {
854
+ switchPort = `if${data.endpoint.attachedInterfaceIndex}`;
855
+ }
856
+ const switchEndpoint = switchInterface
857
+ ? {
858
+ interfaceIndex: switchInterface.interfaceIndex,
859
+ interfaceName: switchInterface.name,
860
+ isOperationallyUp: switchInterface.isOperationallyUp,
861
+ isAdministrativelyUp: switchInterface.isAdministrativelyUp,
862
+ utilizationPercent: switchInterface.utilizationPercent,
863
+ inRateMbps: switchInterface.inRateMbps,
864
+ outRateMbps: switchInterface.outRateMbps,
865
+ errorsPerSecond: switchInterface.errorsPerSecond,
866
+ }
867
+ : undefined;
868
+ /*
869
+ * Which end is up. A forwarding table is not symmetric the way a
870
+ * neighbour protocol is: the switch LEARNED this MAC on an access
871
+ * port, which is a statement that the device hangs off it - for a
872
+ * register, a phone, a camera, a server. It is no such statement
873
+ * about a router or a firewall, whose MAC a switch learns on the port
874
+ * that leads UPSTREAM, nor about another switch, where the table says
875
+ * nothing about which of the two is nearer the core. Those are left
876
+ * for the layout to infer, exactly as an LLDP edge is.
877
+ */
878
+ const declaredParentId = NetworkTopologyUtil.isLearningSwitchTheParent(data.deviceNode)
879
+ ? switchId
880
+ : undefined;
881
+ const edgeKey = [switchId, deviceId].sort().join("::");
882
+ const existing = data.edgeByKey.get(edgeKey);
883
+ if (!existing) {
884
+ const edge = {
885
+ fromNodeId: switchId,
886
+ toNodeId: deviceId,
887
+ fromPort: switchPort,
888
+ protocols: ["fdb"],
889
+ fromInterface: switchEndpoint,
890
+ parentNodeId: declaredParentId,
891
+ learnedByNodeId: switchId,
892
+ };
893
+ data.edgeByKey.set(edgeKey, edge);
894
+ data.edges.push(edge);
895
+ return;
896
+ }
897
+ /*
898
+ * The pair is already joined - by a neighbour protocol, by a link the
899
+ * operator drew, or by both. Merge rather than double the line.
900
+ */
901
+ if (existing.protocols && !existing.protocols.includes("fdb")) {
902
+ existing.protocols.push("fdb");
903
+ }
904
+ /*
905
+ * Which end learned. The existing edge may be stored device→switch,
906
+ * so this is the only reliable statement of which end the table
907
+ * belongs to; the freshest sighting draws, so a later one does not
908
+ * move it.
909
+ */
910
+ existing.learnedByNodeId = existing.learnedByNodeId || switchId;
911
+ /*
912
+ * The switch-end port. Measured beats typed: against a hand-drawn
913
+ * link the forwarding table's port replaces whatever was typed,
914
+ * because the operator's port is a best recollection and this is
915
+ * what the switch actually reports. Against a neighbour protocol both
916
+ * are measured, and the protocol's is kept - it names the port from
917
+ * both ends, where the table only knows one.
918
+ */
919
+ const isMeasuredAlready = Boolean((_a = existing.protocols) === null || _a === void 0 ? void 0 : _a.some((protocol) => {
920
+ return protocol === "lldp" || protocol === "cdp";
921
+ }));
922
+ const switchIsFromEnd = existing.fromNodeId === switchId;
923
+ const existingPort = switchIsFromEnd
924
+ ? existing.fromPort
925
+ : existing.toPort;
926
+ const existingInterface = switchIsFromEnd ? existing.fromInterface : existing.toInterface;
927
+ let mergedPort;
928
+ let mergedInterface;
929
+ if (!isMeasuredAlready) {
930
+ mergedPort = switchPort || existingPort;
931
+ mergedInterface = NetworkTopologyUtil.mergeEndpoints(switchEndpoint, existingInterface);
932
+ }
933
+ else if (NetworkTopologyUtil.describesSamePort(existingInterface, existingPort, switchEndpoint, switchPort)) {
934
+ /*
935
+ * Two reports of ONE port: the protocol's fields are kept and the
936
+ * table fills whatever the protocol's report left blank.
937
+ */
938
+ mergedPort = existingPort || switchPort;
939
+ mergedInterface = NetworkTopologyUtil.mergeEndpoints(existingInterface, switchEndpoint);
940
+ }
941
+ else {
942
+ /*
943
+ * Two reports of DIFFERENT ports - the device was re-patched and the
944
+ * table has caught up before the neighbour protocol, or the other
945
+ * way round. Splicing the two field by field would label the
946
+ * protocol's port with the other port's name and paint it with the
947
+ * other port's state, so the protocol's end is left whole and the
948
+ * table's port is not shown until the two agree.
949
+ */
950
+ mergedPort = existingPort;
951
+ mergedInterface = existingInterface;
952
+ }
953
+ if (switchIsFromEnd) {
954
+ existing.fromPort = mergedPort;
955
+ existing.fromInterface = mergedInterface;
956
+ }
957
+ else {
958
+ existing.toPort = mergedPort;
959
+ existing.toInterface = mergedInterface;
960
+ }
961
+ /*
962
+ * A hierarchy somebody DECLARED - on the link, or through a rule -
963
+ * beats the one the table implies. The specific statement beats the
964
+ * general one, the same precedence the manual-link merge applies.
965
+ */
966
+ existing.parentNodeId = existing.parentNodeId || declaredParentId;
967
+ }
968
+ /*
969
+ * Whether a neighbour protocol's report of the switch end and the
970
+ * forwarding table's attachment name the same port. By ifIndex when both
971
+ * know it; by port label when the protocol's end has no interface row
972
+ * behind it. Unknown on both sides is not "the same" - it is nothing to
973
+ * compare, and the protocol's end is kept as it was.
974
+ */
975
+ static describesSamePort(existingInterface, existingPort, switchEndpoint, switchPort) {
976
+ if ((existingInterface === null || existingInterface === void 0 ? void 0 : existingInterface.interfaceIndex) !== undefined &&
977
+ (switchEndpoint === null || switchEndpoint === void 0 ? void 0 : switchEndpoint.interfaceIndex) !== undefined) {
978
+ return existingInterface.interfaceIndex === switchEndpoint.interfaceIndex;
979
+ }
980
+ const existingLabel = NetworkTopologyUtil.normalizeKey((existingInterface === null || existingInterface === void 0 ? void 0 : existingInterface.interfaceName) || existingPort);
981
+ const switchLabel = NetworkTopologyUtil.normalizeKey((switchEndpoint === null || switchEndpoint === void 0 ? void 0 : switchEndpoint.interfaceName) || switchPort);
982
+ return (existingLabel !== undefined &&
983
+ switchLabel !== undefined &&
984
+ existingLabel === switchLabel);
985
+ }
986
+ /*
987
+ * What the tables knew about the device, onto its node, so the detail
988
+ * drawer can show the MAC, the address and the VLAN it was learned on.
989
+ * Fills gaps only: a MAC the operator declared is already the one that
990
+ * matched, and an address the row carries is left as it is.
991
+ */
992
+ static stampAdoptedIdentity(node, endpoint) {
993
+ const mac = normalizeMac(endpoint.macAddress);
994
+ if (mac && !node.macAddress) {
995
+ node.macAddress = mac;
996
+ }
997
+ if (endpoint.ipAddress && !node.ipAddress) {
998
+ node.ipAddress = endpoint.ipAddress;
999
+ }
1000
+ if (typeof endpoint.vlanId === "number" && node.vlanId === undefined) {
1001
+ node.vlanId = endpoint.vlanId;
1002
+ }
1003
+ }
1004
+ /*
1005
+ * Whether a switch that learned this device's MAC on an access port is
1006
+ * thereby its parent. True for anything that hangs off a switch; false
1007
+ * for a core-layer device (router, firewall, load balancer - or a
1008
+ * custom role the project marked core) and for another switch. The
1009
+ * configured flag wins where the project set one; the built-in core set
1010
+ * is the fallback, as everywhere else the stamp is read.
1011
+ */
1012
+ static isLearningSwitchTheParent(deviceNode) {
1013
+ if (!deviceNode) {
1014
+ return true;
1015
+ }
1016
+ const isCore = deviceNode.isCoreLayerRole !== undefined
1017
+ ? deviceNode.isCoreLayerRole
1018
+ : BUILT_IN_CORE_LAYER_ROLES.has(deviceNode.role || "unknown");
1019
+ if (isCore) {
1020
+ return false;
1021
+ }
1022
+ const roleKey = (deviceNode.roleKey || deviceNode.role || "")
1023
+ .trim()
1024
+ .toLowerCase();
1025
+ return roleKey !== "switch";
1026
+ }
552
1027
  /*
553
1028
  * Both protocols' neighbor entries, normalized. LLDP first so its
554
1029
  * (usually richer) identifiers win the first-report slots; CDP then
@@ -777,6 +1252,15 @@ export default class NetworkTopologyUtil {
777
1252
  add({ kind: "mac", value: mac });
778
1253
  }
779
1254
  }
1255
+ /*
1256
+ * The declared MAC too. A ping-only device has no interface walk to
1257
+ * report its MACs from, so this is the only MAC it can answer to when
1258
+ * a neighbour advertises it as an LLDP chassis id.
1259
+ */
1260
+ const declaredMac = normalizeMac(device.macAddress);
1261
+ if (declaredMac) {
1262
+ add({ kind: "mac", value: declaredMac });
1263
+ }
780
1264
  return keys;
781
1265
  }
782
1266
  /*