@oneuptime/common 12.0.11 → 12.0.13

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 (251) hide show
  1. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +32 -5
  2. package/Models/DatabaseModels/NetworkDevice.ts +64 -0
  3. package/Models/DatabaseModels/StatusPage.ts +59 -0
  4. package/Models/DatabaseModels/StatusPageOidc.ts +2 -0
  5. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -2
  6. package/Server/API/AIChatAPI.ts +55 -56
  7. package/Server/API/CommonAPI.ts +89 -0
  8. package/Server/API/MicrosoftTeamsAPI.ts +20 -9
  9. package/Server/API/SlackAPI.ts +13 -10
  10. package/Server/API/StatusPageAPI.ts +2197 -2128
  11. package/Server/API/TelemetryAPI.ts +72 -3
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.ts +30 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.ts +47 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  15. package/Server/Middleware/UserAuthorization.ts +11 -2
  16. package/Server/Services/ApiKeyPermissionService.ts +116 -2
  17. package/Server/Services/DatabaseService.ts +17 -6
  18. package/Server/Services/LogAggregationService.ts +34 -0
  19. package/Server/Services/LogService.ts +21 -0
  20. package/Server/Services/NetworkSiteService.ts +12 -0
  21. package/Server/Services/SpanService.ts +21 -0
  22. package/Server/Services/TraceAggregationService.ts +15 -0
  23. package/Server/Services/WorkspaceNotificationRuleService.ts +172 -220
  24. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
  25. package/Server/Types/Database/Permissions/AccessControlPermission.ts +47 -16
  26. package/Server/Types/Workflow/Components/Conditions/IfElse.ts +9 -0
  27. package/Server/Types/Workflow/Components/JavaScript.ts +9 -0
  28. package/Server/Utils/APIKey/AccessPermission.ts +16 -40
  29. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +144 -0
  30. package/Server/Utils/LogRedaction.ts +576 -0
  31. package/Server/Utils/Logger.ts +123 -46
  32. package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +94 -23
  33. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +12 -0
  34. package/Server/Utils/Monitor/MonitorResource.ts +29 -9
  35. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +28 -7
  36. package/Server/Utils/SessionReplay/SessionReplayGateCacheStore.ts +56 -10
  37. package/Server/Utils/StartServer.ts +30 -9
  38. package/Server/Utils/StatusPageSearchEngineIndexing.ts +33 -0
  39. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +441 -0
  40. package/Server/Utils/VM/VMRunner.ts +693 -874
  41. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +488 -46
  42. package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +300 -0
  43. package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +130 -0
  44. package/Tests/Models/DatabaseModels/DatabaseBaseModelToJSONObjectCache.test.ts +180 -0
  45. package/Tests/Models/StatusPageEnableSearchEngineIndexing.test.ts +157 -0
  46. package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +23 -1
  47. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +785 -0
  48. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +21 -8
  49. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +290 -0
  50. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +7 -1
  51. package/Tests/Server/API/StatusPageOverviewCache.test.ts +474 -0
  52. package/Tests/Server/API/StatusPageSeoSearchEngineIndexing.test.ts +189 -0
  53. package/Tests/Server/Infrastructure/InMemoryTTLCache.test.ts +158 -0
  54. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +9 -60
  55. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +24 -15
  56. package/Tests/Server/Middleware/UserAuthorization.test.ts +79 -0
  57. package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +332 -0
  58. package/Tests/Server/Services/ApiKeyPermissionService.test.ts +347 -0
  59. package/Tests/Server/Services/DatabaseServiceUpdateDebugLogging.test.ts +282 -0
  60. package/Tests/Server/Services/NetworkSiteService.test.ts +56 -2
  61. package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +267 -0
  62. package/Tests/Server/Services/WorkspaceNotificationRuleTestRuleChannels.test.ts +702 -0
  63. package/Tests/Server/Types/Database/Permissions/AccessControlPermission.test.ts +165 -0
  64. package/Tests/Server/Types/Workflow/Components/CustomCodeScriptError.test.ts +121 -0
  65. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +48 -36
  66. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +213 -0
  67. package/Tests/Server/Utils/LogRedaction.test.ts +415 -0
  68. package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +245 -0
  69. package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +374 -4
  70. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorJavascriptExpression.test.ts +92 -0
  71. package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +455 -0
  72. package/Tests/Server/Utils/Monitor/NetworkDeviceReachabilityRoundTrip.test.ts +369 -0
  73. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +86 -8
  74. package/Tests/Server/Utils/PrivacyFilterUtil.test.ts +64 -0
  75. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCacheStore.test.ts +207 -0
  76. package/Tests/Server/Utils/StartServerBodyVerify.test.ts +335 -0
  77. package/Tests/Server/Utils/StatusPageSearchEngineIndexing.test.ts +60 -0
  78. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +435 -0
  79. package/Tests/Server/Utils/VM/VMRunnerIsolation.test.ts +385 -0
  80. package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +3 -0
  81. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +607 -62
  82. package/Tests/Server/Utils/Workspace/MicrosoftTeamsInstalledTeamIdSpace.test.ts +778 -0
  83. package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +44 -10
  84. package/Tests/Types/API/HostnameFromAuthority.test.ts +164 -0
  85. package/Tests/Types/Database/AccessControl/ColumnAccessControlCache.test.ts +344 -0
  86. package/Tests/Types/Database/DatabasePropertyFindOperator.test.ts +518 -0
  87. package/Tests/Types/Database/TableColumnMetadataCache.test.ts +286 -0
  88. package/Tests/Types/Monitor/MonitorStep.test.ts +100 -0
  89. package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +360 -0
  90. package/Tests/Types/StatusPage/SearchEngineIndexing.test.ts +86 -0
  91. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +254 -0
  92. package/Tests/UI/Components/CardSelect.test.tsx +1021 -0
  93. package/Tests/UI/Components/MasterPage.test.tsx +36 -1
  94. package/Tests/Utils/Monitor/MonitorMetricType.test.ts +66 -1
  95. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +115 -6
  96. package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +610 -0
  97. package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +114 -45
  98. package/Types/API/Hostname.ts +79 -0
  99. package/Types/Database/AccessControl/ColumnAccessControl.ts +33 -11
  100. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +33 -11
  101. package/Types/Database/DatabaseProperty.ts +38 -0
  102. package/Types/Database/TableColumn.ts +34 -7
  103. package/Types/Events/Recurring.ts +16 -1
  104. package/Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse.ts +14 -2
  105. package/Types/Monitor/MonitorStep.ts +13 -1
  106. package/Types/Monitor/MonitorType.ts +345 -40
  107. package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +43 -0
  108. package/Types/StatusPage/SearchEngineIndexing.ts +61 -0
  109. package/Types/Telemetry/ResourceEntityFacet.ts +211 -0
  110. package/UI/Components/CardSelect/CardSelect.tsx +714 -105
  111. package/UI/Components/EmptyState/EmptyState.tsx +8 -1
  112. package/UI/Components/Footer/Footer.tsx +32 -6
  113. package/UI/Components/Forms/Fields/FormField.tsx +3 -0
  114. package/UI/Components/Forms/Types/Field.ts +8 -0
  115. package/UI/Components/Loader/PageLoader.tsx +29 -8
  116. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +26 -4
  117. package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +17 -1
  118. package/UI/Components/MasterPage/MasterPage.tsx +24 -11
  119. package/Utils/Monitor/MonitorMetricType.ts +16 -1
  120. package/Utils/Monitor/NetworkTopologyUtil.ts +60 -8
  121. package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +262 -0
  122. package/Utils/NetworkSite/SiteStatusRollupUtil.ts +41 -24
  123. package/Utils/Telemetry/CrossSignalScope.ts +75 -4
  124. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +20 -4
  125. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/NetworkDevice.js +66 -0
  127. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/StatusPage.js +60 -0
  129. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  130. package/build/dist/Models/DatabaseModels/StatusPageOidc.js +2 -0
  131. package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
  132. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  133. package/build/dist/Server/API/AIChatAPI.js +48 -35
  134. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  135. package/build/dist/Server/API/CommonAPI.js +59 -0
  136. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  137. package/build/dist/Server/API/MicrosoftTeamsAPI.js +20 -6
  138. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  139. package/build/dist/Server/API/SlackAPI.js +13 -10
  140. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  141. package/build/dist/Server/API/StatusPageAPI.js +745 -673
  142. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  143. package/build/dist/Server/API/TelemetryAPI.js +34 -3
  144. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js +23 -0
  146. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js.map +1 -0
  147. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js +34 -0
  148. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js.map +1 -0
  149. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  150. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  151. package/build/dist/Server/Middleware/UserAuthorization.js +6 -2
  152. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  153. package/build/dist/Server/Services/ApiKeyPermissionService.js +90 -1
  154. package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
  155. package/build/dist/Server/Services/DatabaseService.js +15 -6
  156. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  157. package/build/dist/Server/Services/LogAggregationService.js +2 -0
  158. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  159. package/build/dist/Server/Services/LogService.js +16 -0
  160. package/build/dist/Server/Services/LogService.js.map +1 -1
  161. package/build/dist/Server/Services/NetworkSiteService.js +12 -0
  162. package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
  163. package/build/dist/Server/Services/SpanService.js +16 -0
  164. package/build/dist/Server/Services/SpanService.js.map +1 -1
  165. package/build/dist/Server/Services/TraceAggregationService.js +2 -0
  166. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  167. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +137 -164
  168. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  169. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
  170. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  171. package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js +21 -13
  172. package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js.map +1 -1
  173. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +8 -0
  174. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
  175. package/build/dist/Server/Types/Workflow/Components/JavaScript.js +8 -0
  176. package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
  177. package/build/dist/Server/Utils/APIKey/AccessPermission.js +9 -32
  178. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  179. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +87 -0
  180. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  181. package/build/dist/Server/Utils/LogRedaction.js +449 -0
  182. package/build/dist/Server/Utils/LogRedaction.js.map +1 -0
  183. package/build/dist/Server/Utils/Logger.js +98 -45
  184. package/build/dist/Server/Utils/Logger.js.map +1 -1
  185. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +65 -13
  186. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
  187. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +12 -1
  188. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  189. package/build/dist/Server/Utils/Monitor/MonitorResource.js +20 -3
  190. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +27 -7
  192. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  193. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js +29 -10
  194. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js.map +1 -1
  195. package/build/dist/Server/Utils/StartServer.js +10 -7
  196. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  197. package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js +24 -0
  198. package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js.map +1 -0
  199. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +311 -0
  200. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -0
  201. package/build/dist/Server/Utils/VM/VMRunner.js +402 -525
  202. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  203. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +366 -37
  204. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  205. package/build/dist/Types/API/Hostname.js +63 -0
  206. package/build/dist/Types/API/Hostname.js.map +1 -1
  207. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +20 -6
  208. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  209. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +20 -6
  210. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  211. package/build/dist/Types/Database/DatabaseProperty.js +38 -0
  212. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  213. package/build/dist/Types/Database/TableColumn.js +24 -6
  214. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  215. package/build/dist/Types/Events/Recurring.js +13 -1
  216. package/build/dist/Types/Events/Recurring.js.map +1 -1
  217. package/build/dist/Types/Monitor/MonitorStep.js +9 -1
  218. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  219. package/build/dist/Types/Monitor/MonitorType.js +330 -31
  220. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  221. package/build/dist/Types/StatusPage/SearchEngineIndexing.js +55 -0
  222. package/build/dist/Types/StatusPage/SearchEngineIndexing.js.map +1 -0
  223. package/build/dist/Types/Telemetry/ResourceEntityFacet.js +156 -0
  224. package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -0
  225. package/build/dist/UI/Components/CardSelect/CardSelect.js +360 -40
  226. package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
  227. package/build/dist/UI/Components/EmptyState/EmptyState.js +1 -1
  228. package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
  229. package/build/dist/UI/Components/Footer/Footer.js +7 -4
  230. package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
  231. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  232. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  233. package/build/dist/UI/Components/Loader/PageLoader.js +15 -5
  234. package/build/dist/UI/Components/Loader/PageLoader.js.map +1 -1
  235. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +16 -3
  236. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  237. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js +13 -1
  238. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
  239. package/build/dist/UI/Components/MasterPage/MasterPage.js +10 -2
  240. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  241. package/build/dist/Utils/Monitor/MonitorMetricType.js +13 -1
  242. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  243. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +34 -7
  244. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  245. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +177 -0
  246. package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -0
  247. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +23 -27
  248. package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
  249. package/build/dist/Utils/Telemetry/CrossSignalScope.js +44 -4
  250. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  251. package/package.json +1 -1
@@ -0,0 +1,369 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
2
+ import NetworkInventoryUtil from "../../../../Server/Utils/Monitor/NetworkInventoryUtil";
3
+ import NetworkDeviceService from "../../../../Server/Services/NetworkDeviceService";
4
+ import NetworkEndpointService from "../../../../Server/Services/NetworkEndpointService";
5
+ import NetworkInterfaceService from "../../../../Server/Services/NetworkInterfaceService";
6
+ import NetworkDevice from "../../../../Models/DatabaseModels/NetworkDevice";
7
+ import NetworkInterface from "../../../../Models/DatabaseModels/NetworkInterface";
8
+ import DeviceReachabilityUtil, {
9
+ NetworkDeviceReachability,
10
+ } from "../../../../Utils/NetworkDevice/DeviceReachabilityUtil";
11
+ import SiteStatusRollupUtil, {
12
+ RollupStatusOption,
13
+ } from "../../../../Utils/NetworkSite/SiteStatusRollupUtil";
14
+ import NetworkTopologyUtil, {
15
+ TopologyBuildResult,
16
+ } from "../../../../Utils/Monitor/NetworkTopologyUtil";
17
+ import { NetworkTopologyNode } from "../../../../Types/Monitor/SnmpMonitor/NetworkTopology";
18
+ import ObjectID from "../../../../Types/ObjectID";
19
+ import OneUptimeDate from "../../../../Types/Date";
20
+ import SnmpInterface from "../../../../Types/Monitor/SnmpMonitor/SnmpInterface";
21
+ import SnmpMonitorResponse from "../../../../Types/Monitor/SnmpMonitor/SnmpMonitorResponse";
22
+
23
+ /*
24
+ * Issue #3220, end to end.
25
+ *
26
+ * "Network Device shown as Down (unreachable) despite device responding to
27
+ * ping and interfaces showing Up." A router that answered SNMP fine — its
28
+ * own Interfaces tab listed 14 ports up, written by that very walk — was
29
+ * listed as Down, with "last seen 21 minutes ago" underneath. 323 of 980
30
+ * devices in that project were in the same state, and the topology graph
31
+ * and network map drew all of them red.
32
+ *
33
+ * The two halves of the cause:
34
+ *
35
+ * 1. a probe is handed a bounded batch of devices per one-minute fetch,
36
+ * so a fleet of ~1000 devices took ~20 minutes to poll all the way
37
+ * round however short the configured interval was; and
38
+ * 2. every reader decided up/down from `lastSeenAt` being newer than a
39
+ * fixed 15 minutes, which a 20-minute cycle can never satisfy.
40
+ *
41
+ * The unit suites cover each layer on its own. What this file does is join
42
+ * them: it drives the REAL write path with a walk exactly like the reported
43
+ * one, and then asks every reader — the device pill, the site rollup and
44
+ * the topology graph — what it makes of the row that was actually written.
45
+ * A regression in either the writer or any single reader shows up here.
46
+ */
47
+
48
+ const PROJECT_ID: string = "1c9d4a7b-0000-4000-8000-000000000011";
49
+ const DEVICE_ID: string = "8f2c1f0e-0000-4000-8000-0000000000aa";
50
+
51
+ // The moment the probe finally got round to UN1234WANRTR01.
52
+ const WALKED_AT: Date = new Date("2026-08-17T18:12:00.000Z");
53
+ // The moment the operator was looking at the screen: 21 minutes later.
54
+ const OBSERVED_AT: Date = new Date("2026-08-17T18:33:00.000Z");
55
+
56
+ const OPERATIONAL: RollupStatusOption = {
57
+ monitorStatusId: "status-operational",
58
+ priority: 1,
59
+ };
60
+ const OFFLINE: RollupStatusOption = {
61
+ monitorStatusId: "status-offline",
62
+ priority: 3,
63
+ };
64
+
65
+ let deviceUpdateSpy: jest.SpyInstance;
66
+
67
+ function mockServices(): void {
68
+ const ownedDevice: NetworkDevice = new NetworkDevice();
69
+ ownedDevice.id = new ObjectID(DEVICE_ID);
70
+
71
+ jest
72
+ .spyOn(NetworkDeviceService, "findOneBy")
73
+ .mockResolvedValue(ownedDevice as never);
74
+ deviceUpdateSpy = jest
75
+ .spyOn(NetworkDeviceService, "updateOneById")
76
+ .mockResolvedValue(1 as never);
77
+ jest.spyOn(NetworkInterfaceService, "findBy").mockResolvedValue([] as never);
78
+ jest
79
+ .spyOn(NetworkInterfaceService, "updateOneById")
80
+ .mockResolvedValue(1 as never);
81
+ jest
82
+ .spyOn(NetworkInterfaceService, "create")
83
+ .mockResolvedValue(new NetworkInterface() as never);
84
+ jest
85
+ .spyOn(NetworkEndpointService, "upsertDiscoveredEndpoints")
86
+ .mockResolvedValue(undefined as never);
87
+ }
88
+
89
+ // 14 ports up and 12 down — the mix the reported device's Interfaces tab showed.
90
+ function reportedInterfaces(): Array<SnmpInterface> {
91
+ const interfaces: Array<SnmpInterface> = [];
92
+
93
+ for (let index: number = 1; index <= 26; index++) {
94
+ interfaces.push({
95
+ interfaceIndex: index,
96
+ name: `Gi0/${index}`,
97
+ isAdministrativelyUp: true,
98
+ isOperationallyUp: index <= 14,
99
+ } as SnmpInterface);
100
+ }
101
+
102
+ return interfaces;
103
+ }
104
+
105
+ /*
106
+ * Runs the real writer and returns the device row as it would then be read
107
+ * back — the columns the update wrote, nothing invented.
108
+ */
109
+ async function walkAndReadBackDevice(data: {
110
+ isOnline: boolean;
111
+ interfaces: Array<SnmpInterface>;
112
+ }): Promise<Record<string, unknown>> {
113
+ const snmpResponse: SnmpMonitorResponse = {
114
+ isOnline: data.isOnline,
115
+ responseTimeInMs: 274,
116
+ failureCause: "",
117
+ oidResponses: [],
118
+ interfaces: data.interfaces,
119
+ systemInfo: { sysName: "UN1234WANRTR01" },
120
+ } as SnmpMonitorResponse;
121
+
122
+ await NetworkInventoryUtil.updateFromWalk({
123
+ projectId: new ObjectID(PROJECT_ID),
124
+ deviceId: new ObjectID(DEVICE_ID),
125
+ snmpResponse: snmpResponse,
126
+ isOnline: data.isOnline,
127
+ });
128
+
129
+ expect(deviceUpdateSpy).toHaveBeenCalled();
130
+
131
+ return {
132
+ // The device's configured schedule, unchanged by the walk.
133
+ pollingIntervalInMinutes: 5,
134
+ ...(deviceUpdateSpy.mock.calls[0]![0].data as Record<string, unknown>),
135
+ };
136
+ }
137
+
138
+ beforeEach(() => {
139
+ jest.useFakeTimers({
140
+ doNotFake: [
141
+ "performance",
142
+ "hrtime",
143
+ "queueMicrotask",
144
+ "requestAnimationFrame",
145
+ "cancelAnimationFrame",
146
+ "requestIdleCallback",
147
+ "cancelIdleCallback",
148
+ "setImmediate",
149
+ "clearImmediate",
150
+ "setInterval",
151
+ "clearInterval",
152
+ "setTimeout",
153
+ "clearTimeout",
154
+ ],
155
+ });
156
+ // The walk lands at 18:12; every reader below looks at 18:33.
157
+ jest.setSystemTime(WALKED_AT);
158
+ mockServices();
159
+ });
160
+
161
+ afterEach(() => {
162
+ jest.useRealTimers();
163
+ jest.restoreAllMocks();
164
+ });
165
+
166
+ describe("issue #3220 — a device that answered 21 minutes ago", () => {
167
+ test("the walk records that it answered, and when it was asked", async () => {
168
+ const row: Record<string, unknown> = await walkAndReadBackDevice({
169
+ isOnline: true,
170
+ interfaces: reportedInterfaces(),
171
+ });
172
+
173
+ expect(row["isReachable"]).toBe(true);
174
+ expect(row["lastPolledAt"]).toEqual(WALKED_AT);
175
+ expect(row["lastSeenAt"]).toEqual(WALKED_AT);
176
+ });
177
+
178
+ test("the same walk is what put 14 up / 12 down on the Interfaces tab", async () => {
179
+ const row: Record<string, unknown> = await walkAndReadBackDevice({
180
+ isOnline: true,
181
+ interfaces: reportedInterfaces(),
182
+ });
183
+
184
+ /*
185
+ * The contradiction the report was really about: these counts and the
186
+ * reachability verdict come out of ONE walk, so they cannot honestly
187
+ * disagree.
188
+ */
189
+ expect(row["interfacesUp"]).toBe(14);
190
+ expect(row["interfacesDown"]).toBe(12);
191
+ expect(row["isReachable"]).toBe(true);
192
+ });
193
+
194
+ test("the device pill reads Up, 21 minutes later", async () => {
195
+ const row: Record<string, unknown> = await walkAndReadBackDevice({
196
+ isOnline: true,
197
+ interfaces: reportedInterfaces(),
198
+ });
199
+
200
+ expect(DeviceReachabilityUtil.getStatus(row, OBSERVED_AT)).toBe(
201
+ NetworkDeviceReachability.Up,
202
+ );
203
+ });
204
+
205
+ test("the site rollup above it stays operational", async () => {
206
+ const row: Record<string, unknown> = await walkAndReadBackDevice({
207
+ isOnline: true,
208
+ interfaces: reportedInterfaces(),
209
+ });
210
+
211
+ expect(
212
+ SiteStatusRollupUtil.worstStatus({
213
+ deviceStates: [row],
214
+ operationalStatus: OPERATIONAL,
215
+ offlineStatus: OFFLINE,
216
+ now: OBSERVED_AT,
217
+ }),
218
+ ).toBe(OPERATIONAL.monitorStatusId);
219
+ });
220
+
221
+ test("the topology graph draws it up, not red", async () => {
222
+ const row: Record<string, unknown> = await walkAndReadBackDevice({
223
+ isOnline: true,
224
+ interfaces: reportedInterfaces(),
225
+ });
226
+
227
+ const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
228
+ [
229
+ {
230
+ id: DEVICE_ID,
231
+ name: "UN1234WANRTR01",
232
+ hostname: "10.15.128.165",
233
+ isReachable: row["isReachable"] as boolean,
234
+ lastPolledAt: row["lastPolledAt"] as Date,
235
+ lastSeenAt: row["lastSeenAt"] as Date,
236
+ pollingIntervalInMinutes: 5,
237
+ },
238
+ ],
239
+ OBSERVED_AT,
240
+ );
241
+
242
+ const node: NetworkTopologyNode | undefined = result.nodes.find(
243
+ (candidate: NetworkTopologyNode) => {
244
+ return candidate.id === DEVICE_ID;
245
+ },
246
+ );
247
+
248
+ expect(node?.status).toBe("up");
249
+ });
250
+ });
251
+
252
+ /*
253
+ * The fix must not have bought its way out of false positives with false
254
+ * negatives. A device that genuinely stops answering has to go down — and
255
+ * go down on the very next poll, not after a timeout.
256
+ */
257
+ describe("a device that genuinely stops answering still goes down", () => {
258
+ test("the failing walk records the failure and leaves lastSeenAt behind", async () => {
259
+ const row: Record<string, unknown> = await walkAndReadBackDevice({
260
+ isOnline: false,
261
+ interfaces: [],
262
+ });
263
+
264
+ expect(row["isReachable"]).toBe(false);
265
+ expect(row["lastPolledAt"]).toEqual(WALKED_AT);
266
+ expect(row).not.toHaveProperty("lastSeenAt");
267
+ });
268
+
269
+ test("every reader calls it down, immediately", async () => {
270
+ const written: Record<string, unknown> = await walkAndReadBackDevice({
271
+ isOnline: false,
272
+ interfaces: [],
273
+ });
274
+
275
+ // The row as it now stands: last answered before the failing poll.
276
+ const row: Record<string, unknown> = {
277
+ ...written,
278
+ lastSeenAt: OneUptimeDate.addRemoveMinutes(WALKED_AT, -5),
279
+ };
280
+
281
+ // One second after the failed walk — no waiting for a window to expire.
282
+ const justAfter: Date = new Date(WALKED_AT.getTime() + 1000);
283
+
284
+ expect(DeviceReachabilityUtil.getStatus(row, justAfter)).toBe(
285
+ NetworkDeviceReachability.Down,
286
+ );
287
+
288
+ expect(
289
+ SiteStatusRollupUtil.worstStatus({
290
+ deviceStates: [row],
291
+ operationalStatus: OPERATIONAL,
292
+ offlineStatus: OFFLINE,
293
+ now: justAfter,
294
+ }),
295
+ ).toBe(OFFLINE.monitorStatusId);
296
+
297
+ const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
298
+ [
299
+ {
300
+ id: DEVICE_ID,
301
+ name: "UN1234WANRTR01",
302
+ isReachable: row["isReachable"] as boolean,
303
+ lastPolledAt: row["lastPolledAt"] as Date,
304
+ lastSeenAt: row["lastSeenAt"] as Date,
305
+ pollingIntervalInMinutes: 5,
306
+ },
307
+ ],
308
+ justAfter,
309
+ );
310
+
311
+ expect(
312
+ result.nodes.find((candidate: NetworkTopologyNode) => {
313
+ return candidate.id === DEVICE_ID;
314
+ })?.status,
315
+ ).toBe("down");
316
+ });
317
+ });
318
+
319
+ /*
320
+ * And the polling pipeline going silent. The fleet keeps its last known
321
+ * verdicts — manufacturing an outage nobody observed is the failure this
322
+ * change removes — but every device is flagged stale, which is what sends
323
+ * the operator to the probe instead of to 300 devices.
324
+ */
325
+ describe("a fleet whose probe has gone silent is flagged, not condemned", () => {
326
+ test("a good verdict nobody has refreshed for hours stays Up and reads stale", async () => {
327
+ const row: Record<string, unknown> = await walkAndReadBackDevice({
328
+ isOnline: true,
329
+ interfaces: reportedInterfaces(),
330
+ });
331
+
332
+ const hoursLater: Date = new Date(WALKED_AT.getTime() + 4 * 60 * 60 * 1000);
333
+
334
+ const reachability: {
335
+ status: NetworkDeviceReachability;
336
+ isStale: boolean;
337
+ } = DeviceReachabilityUtil.getReachability(row, hoursLater);
338
+
339
+ expect(reachability.status).toBe(NetworkDeviceReachability.Up);
340
+ /*
341
+ * isStale is what puts the amber "Stale" pill next to the verdict and
342
+ * points the tooltip at the probe — a different investigation from
343
+ * "this device is down", and the reason it is not expressed as Down.
344
+ */
345
+ expect(reachability.isStale).toBe(true);
346
+ });
347
+
348
+ /*
349
+ * The property that keeps the device list self-consistent: the summary
350
+ * tiles and the Status chip are SQL over `isReachable`, so whatever the
351
+ * clock says, the pill has to be reproducible from that column alone.
352
+ */
353
+ test("the verdict stays reproducible from the stored column alone", async () => {
354
+ const row: Record<string, unknown> = await walkAndReadBackDevice({
355
+ isOnline: true,
356
+ interfaces: reportedInterfaces(),
357
+ });
358
+
359
+ for (const hoursLater of [0, 1, 4, 24, 24 * 30]) {
360
+ const at: Date = new Date(WALKED_AT.getTime() + hoursLater * 3600 * 1000);
361
+
362
+ expect(DeviceReachabilityUtil.getStatus(row, at)).toBe(
363
+ row["isReachable"] === true
364
+ ? NetworkDeviceReachability.Up
365
+ : NetworkDeviceReachability.Down,
366
+ );
367
+ }
368
+ });
369
+ });
@@ -236,8 +236,33 @@ describe("NetworkInventoryUtil.updateFromWalk — project-membership guard", ()
236
236
  });
237
237
  });
238
238
 
239
- describe("NetworkInventoryUtil.updateFromWalk — reachability gating", () => {
240
- test("an unreachable poll with no walk data writes nothing (no phantom lastSeenAt)", async () => {
239
+ /*
240
+ * The three columns this util writes on every walk are what the whole
241
+ * up/down story rests on, so they get their own block:
242
+ *
243
+ * lastPolledAt - when we ASKED (always)
244
+ * isReachable - what we got back (always)
245
+ * lastSeenAt - when it ANSWERED (successful walks only)
246
+ *
247
+ * The bug this pins (issue #3220) was that only lastSeenAt existed, so a
248
+ * device that answered its last poll 21 minutes ago was indistinguishable
249
+ * from one that had failed its last poll — and the UI, having to guess,
250
+ * called both of them Down.
251
+ */
252
+ describe("NetworkInventoryUtil.updateFromWalk — reachability recording", () => {
253
+ test("a reachable poll stamps all three columns", async () => {
254
+ mockServices();
255
+
256
+ await runWalk();
257
+
258
+ const update: DeviceUpdatePayload = deviceUpdatePayload();
259
+
260
+ expect(update["lastPolledAt"]).toEqual(NOW);
261
+ expect(update["isReachable"]).toBe(true);
262
+ expect(update["lastSeenAt"]).toEqual(NOW);
263
+ });
264
+
265
+ test("an unreachable poll with no walk data still records the attempt", async () => {
241
266
  mockServices();
242
267
 
243
268
  await runWalk(
@@ -249,8 +274,17 @@ describe("NetworkInventoryUtil.updateFromWalk — reachability gating", () => {
249
274
  { isOnline: false },
250
275
  );
251
276
 
252
- // lastSeenAt drives the up/down pill; a failed poll must not bump it.
253
- expect(deviceUpdateSpy).not.toHaveBeenCalled();
277
+ const update: DeviceUpdatePayload = deviceUpdatePayload();
278
+
279
+ /*
280
+ * This write used to be skipped entirely ("nothing worth writing"),
281
+ * which is exactly what left the reader unable to tell a failing device
282
+ * from one nothing had got round to polling.
283
+ */
284
+ expect(update["lastPolledAt"]).toEqual(NOW);
285
+ expect(update["isReachable"]).toBe(false);
286
+ // The device did not answer, so its last contact must not move.
287
+ expect(update).not.toHaveProperty("lastSeenAt");
254
288
  });
255
289
 
256
290
  test("an unreachable poll with walk data still enriches but never stamps lastSeenAt", async () => {
@@ -267,15 +301,54 @@ describe("NetworkInventoryUtil.updateFromWalk — reachability gating", () => {
267
301
  const update: DeviceUpdatePayload = deviceUpdatePayload();
268
302
 
269
303
  expect(update["sysName"]).toBe("core-sw-01");
304
+ expect(update["isReachable"]).toBe(false);
305
+ expect(update["lastPolledAt"]).toEqual(NOW);
270
306
  expect(update).not.toHaveProperty("lastSeenAt");
271
307
  });
272
308
 
273
- test("a walk that reports no reachability at all is treated as seen", async () => {
309
+ test("a walk that reports no reachability at all is treated as answered", async () => {
274
310
  mockServices();
275
311
 
276
312
  await runWalk(undefined, { isOnline: undefined });
277
313
 
278
- expect(deviceUpdatePayload()["lastSeenAt"]).toEqual(NOW);
314
+ const update: DeviceUpdatePayload = deviceUpdatePayload();
315
+
316
+ expect(update["isReachable"]).toBe(true);
317
+ expect(update["lastPolledAt"]).toEqual(NOW);
318
+ expect(update["lastSeenAt"]).toEqual(NOW);
319
+ });
320
+
321
+ test("lastPolledAt and lastSeenAt are the SAME instant on a good walk", async () => {
322
+ mockServices();
323
+
324
+ await runWalk();
325
+
326
+ const update: DeviceUpdatePayload = deviceUpdatePayload();
327
+
328
+ /*
329
+ * The reader treats "polled later than seen" as evidence of a failed
330
+ * attempt, so a successful walk must not leave even a millisecond of
331
+ * skew between the two.
332
+ */
333
+ expect((update["lastPolledAt"] as Date).getTime()).toBe(
334
+ (update["lastSeenAt"] as Date).getTime(),
335
+ );
336
+ });
337
+
338
+ test("a failed poll after a successful one leaves lastSeenAt behind lastPolledAt", async () => {
339
+ mockServices();
340
+
341
+ await runWalk();
342
+ const good: DeviceUpdatePayload = deviceUpdatePayload();
343
+
344
+ deviceUpdateSpy.mockClear();
345
+ await runWalk({ isOnline: false }, { isOnline: false });
346
+ const bad: DeviceUpdatePayload = deviceUpdatePayload();
347
+
348
+ expect(bad["isReachable"]).toBe(false);
349
+ expect(bad).not.toHaveProperty("lastSeenAt");
350
+ // The previous success stays the device's last contact.
351
+ expect(good["lastSeenAt"]).toEqual(NOW);
279
352
  });
280
353
  });
281
354
 
@@ -690,7 +763,7 @@ describe("NetworkInventoryUtil.updateFromWalk — walks without interfaces", ()
690
763
  expect(interfaceCreateSpy).not.toHaveBeenCalled();
691
764
  });
692
765
 
693
- test("a walk with no snmpResponse at all still stamps lastSeenAt", async () => {
766
+ test("a walk with no snmpResponse at all still records the poll", async () => {
694
767
  mockServices();
695
768
 
696
769
  await NetworkInventoryUtil.updateFromWalk({
@@ -700,7 +773,12 @@ describe("NetworkInventoryUtil.updateFromWalk — walks without interfaces", ()
700
773
  isOnline: true,
701
774
  });
702
775
 
703
- expect(deviceUpdatePayload()).toEqual({ lastSeenAt: NOW });
776
+ // Exactly the reachability columns and nothing else — no walk, no data.
777
+ expect(deviceUpdatePayload()).toEqual({
778
+ lastPolledAt: NOW,
779
+ isReachable: true,
780
+ lastSeenAt: NOW,
781
+ });
704
782
  expect(interfaceFindSpy).not.toHaveBeenCalled();
705
783
  });
706
784
  });
@@ -38,6 +38,70 @@ describe("combineWithPrivacyClause", () => {
38
38
  expect(children[1]).toBe(clause);
39
39
  });
40
40
 
41
+ it("keeps nested ObjectID values as UUID strings after database transformation", () => {
42
+ /*
43
+ * The clause this function builds lands on an ObjectID column
44
+ * (incidentId, alertId, the episode ids, an ownedThrough FK), and those
45
+ * columns carry ObjectID.getDatabaseTransformer(). TypeORM hands each
46
+ * child of the And() to that transformer, so a transformer that cannot
47
+ * cope with an operator replaces the caller's own equality with
48
+ * "[object Object]" — which Postgres then rejects for a uuid column with
49
+ * 22P02, i.e. a 500 on the incident/alert Feed, Internal Notes and Owners
50
+ * reads for anyone who is not a project owner or admin.
51
+ * https://github.com/OneUptime/oneuptime/issues/2497
52
+ */
53
+ const incidentId: ObjectID = ObjectID.generate();
54
+ const clause: FindOperatorType = makePrivacyClause();
55
+ const combined: any = combineWithPrivacyClause(incidentId, clause as any);
56
+
57
+ const transformed: any = ObjectID.getDatabaseTransformer().to(combined);
58
+
59
+ expect(transformed).toBeInstanceOf(FindOperator);
60
+ expect(transformed.type).toBe("and");
61
+ expect(transformed.value[0]).toBeInstanceOf(FindOperator);
62
+ expect(transformed.value[0].type).toBe("equal");
63
+ expect(transformed.value[0].value).toBe(incidentId.toString());
64
+ expect(transformed.value[0].value).not.toBe("[object Object]");
65
+ expect(transformed.value[1]).toBe(clause);
66
+ });
67
+
68
+ it("survives the transformation twice, as findBy then countBy would", () => {
69
+ /*
70
+ * BaseAPI.getList hands the same query object to findBy and then countBy,
71
+ * and the transformer mutates the operator in place.
72
+ */
73
+ const incidentId: ObjectID = ObjectID.generate();
74
+ const clause: FindOperatorType = makePrivacyClause();
75
+ const combined: any = combineWithPrivacyClause(incidentId, clause as any);
76
+
77
+ ObjectID.getDatabaseTransformer().to(combined);
78
+ const transformed: any = ObjectID.getDatabaseTransformer().to(combined);
79
+
80
+ expect(transformed.value[0].value).toBe(incidentId.toString());
81
+ });
82
+
83
+ it("keeps a re-combined clause intact when the privacy filter runs twice", () => {
84
+ /*
85
+ * countBy's hook sees the And that findBy's hook already built and wraps
86
+ * it again, so the operator handed to the transformer is And(And(...)).
87
+ */
88
+ const incidentId: ObjectID = ObjectID.generate();
89
+ const firstClause: FindOperatorType = makePrivacyClause();
90
+ const secondClause: FindOperatorType = makePrivacyClause();
91
+
92
+ const once: any = combineWithPrivacyClause(incidentId, firstClause as any);
93
+ const twice: any = combineWithPrivacyClause(once, secondClause as any);
94
+
95
+ const transformed: any = ObjectID.getDatabaseTransformer().to(twice);
96
+
97
+ expect(transformed.type).toBe("and");
98
+ expect(transformed.value[0]).toBe(once);
99
+ expect(transformed.value[0].type).toBe("and");
100
+ expect(transformed.value[0].value[0].value).toBe(incidentId.toString());
101
+ expect(transformed.value[0].value[1]).toBe(firstClause);
102
+ expect(transformed.value[1]).toBe(secondClause);
103
+ });
104
+
41
105
  it("ANDs an existing string equality with the privacy clause", () => {
42
106
  const clause: FindOperatorType = makePrivacyClause();
43
107
  const combined: any = combineWithPrivacyClause("some-id", clause as any);