@oneuptime/common 12.0.2 → 12.0.4

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 (384) hide show
  1. package/Models/DatabaseModels/CephCluster.ts +14 -0
  2. package/Models/DatabaseModels/CodeRepository.ts +14 -6
  3. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
  4. package/Models/DatabaseModels/DockerHost.ts +14 -0
  5. package/Models/DatabaseModels/GlobalOidcProject.ts +18 -0
  6. package/Models/DatabaseModels/GlobalSsoProject.ts +18 -0
  7. package/Models/DatabaseModels/Index.ts +2 -0
  8. package/Models/DatabaseModels/IoTFleet.ts +14 -0
  9. package/Models/DatabaseModels/KubernetesCluster.ts +14 -0
  10. package/Models/DatabaseModels/LogDropFilter.ts +9 -20
  11. package/Models/DatabaseModels/NetworkDevice.ts +14 -0
  12. package/Models/DatabaseModels/NetworkInterface.ts +18 -0
  13. package/Models/DatabaseModels/Project.ts +16 -2
  14. package/Models/DatabaseModels/ProxmoxCluster.ts +14 -0
  15. package/Models/DatabaseModels/Service.ts +30 -0
  16. package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
  17. package/Models/DatabaseModels/StatusPagePrivateUser.ts +30 -0
  18. package/Models/DatabaseModels/StatusPageResource.ts +73 -0
  19. package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
  20. package/Models/DatabaseModels/User.ts +30 -0
  21. package/Server/API/AIAgentDataAPI.ts +31 -0
  22. package/Server/API/BaseAPI.ts +13 -2
  23. package/Server/API/DashboardAPI.ts +458 -12
  24. package/Server/API/GitHubAPI.ts +119 -283
  25. package/Server/API/UserAPI.ts +263 -1
  26. package/Server/EnvironmentConfig.ts +38 -4
  27. package/Server/Infrastructure/GlobalCache.ts +152 -0
  28. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
  29. package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.ts +21 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.ts +68 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +100 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +214 -0
  34. package/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.ts +69 -0
  35. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
  36. package/Server/Services/AlertStateTimelineService.ts +20 -0
  37. package/Server/Services/AnalyticsDatabaseService.ts +72 -1
  38. package/Server/Services/CephClusterService.ts +26 -86
  39. package/Server/Services/CloudResourceService.ts +23 -83
  40. package/Server/Services/CodeRepositoryService.ts +105 -2
  41. package/Server/Services/DatabaseService.ts +296 -2
  42. package/Server/Services/DockerHostService.ts +19 -79
  43. package/Server/Services/DockerSwarmClusterService.ts +27 -88
  44. package/Server/Services/HostService.ts +46 -99
  45. package/Server/Services/IncidentStateTimelineService.ts +20 -0
  46. package/Server/Services/Index.ts +2 -0
  47. package/Server/Services/IoTFleetService.ts +19 -79
  48. package/Server/Services/KubernetesClusterService.ts +17 -78
  49. package/Server/Services/LabelService.ts +57 -10
  50. package/Server/Services/MetricTypeService.ts +84 -0
  51. package/Server/Services/MonitorService.ts +89 -14
  52. package/Server/Services/MonitorStatusTimelineService.ts +29 -0
  53. package/Server/Services/MonitorTemplateService.ts +65 -0
  54. package/Server/Services/OpenTelemetryIngestService.ts +230 -26
  55. package/Server/Services/PodmanHostService.ts +19 -79
  56. package/Server/Services/ProxmoxClusterService.ts +23 -83
  57. package/Server/Services/RumApplicationService.ts +19 -80
  58. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
  59. package/Server/Services/ServerlessFunctionService.ts +24 -85
  60. package/Server/Services/ServiceService.ts +299 -73
  61. package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
  62. package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
  63. package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
  64. package/Server/Services/TelemetryEntityService.ts +1 -0
  65. package/Server/Services/UserService.ts +184 -0
  66. package/Server/Utils/AI/Toolbox/CodeTools.ts +45 -4
  67. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +13 -0
  68. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +157 -4
  69. package/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.ts +130 -0
  70. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
  71. package/Server/Utils/Monitor/MonitorResource.ts +63 -36
  72. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
  73. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
  74. package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
  75. package/Server/Utils/PasswordHash.ts +306 -0
  76. package/Server/Utils/SingleFlight.ts +83 -0
  77. package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
  78. package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
  79. package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
  80. package/Server/Utils/Telemetry/Telemetry.ts +351 -122
  81. package/Tests/App/Dashboard/OverviewCustomFields.test.tsx +333 -0
  82. package/Tests/App/Dashboard/UserCustomFields.test.tsx +392 -0
  83. package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
  84. package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
  85. package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
  86. package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
  87. package/Tests/Server/API/AIAgentDataRepositoryToken.test.ts +381 -0
  88. package/Tests/Server/API/DashboardPublicAttributeValuesAPI.test.ts +943 -0
  89. package/Tests/Server/API/DashboardPublicMetricsAggregateAPI.test.ts +1296 -0
  90. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
  91. package/Tests/Server/API/DashboardPublicTemplatePayloads.test.ts +598 -0
  92. package/Tests/Server/API/GitHubAppInstallationBindingAPI.test.ts +539 -0
  93. package/Tests/Server/API/Helpers.ts +6 -1
  94. package/Tests/Server/API/UserProjectsAPI.test.ts +852 -0
  95. package/Tests/Server/Infrastructure/GlobalCache.test.ts +354 -0
  96. package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
  97. package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +215 -0
  98. package/Tests/Server/Services/AddPerUserPasswordSaltMigration.test.ts +178 -0
  99. package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +582 -0
  100. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +5 -0
  101. package/Tests/Server/Services/CodeRepositoryInstallationBinding.test.ts +305 -0
  102. package/Tests/Server/Services/CodeRepositoryResolutionBinding.test.ts +147 -0
  103. package/Tests/Server/Services/DatabaseServicePerUserPasswordSalt.test.ts +870 -0
  104. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
  105. package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
  106. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
  107. package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
  108. package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
  109. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
  110. package/Tests/Server/Services/OpenTelemetryServiceResolutionCache.test.ts +568 -0
  111. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
  112. package/Tests/Server/Services/RestoreDroppedUniqueIndexesMigration.test.ts +386 -0
  113. package/Tests/Server/Services/RestoreServiceLowerNameIndexMigration.test.ts +240 -0
  114. package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
  115. package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
  116. package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
  117. package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
  118. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +885 -0
  119. package/Tests/Server/Services/WidenHashedStringColumnsForScryptMigration.test.ts +220 -0
  120. package/Tests/Server/Utils/AI/CodeTools.test.ts +47 -1
  121. package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +10 -0
  122. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
  123. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +120 -0
  124. package/Tests/Server/Utils/CodeRepository/GitHubInstallationBinding.test.ts +292 -0
  125. package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
  126. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
  127. package/Tests/Server/Utils/GitHubInstallationOwnershipVerification.test.ts +340 -0
  128. package/Tests/Server/Utils/GitHubWebhookAndTreeCacheIsolation.test.ts +250 -0
  129. package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
  130. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
  131. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
  132. package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
  133. package/Tests/Server/Utils/PasswordHash.test.ts +555 -0
  134. package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
  135. package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
  136. package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
  137. package/Tests/Types/Database/ColumnLength.test.ts +7 -1
  138. package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
  139. package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
  140. package/Tests/Types/Decimal.test.ts +37 -0
  141. package/Tests/Types/HashedStringPerUserSalt.test.ts +476 -0
  142. package/Tests/Types/Monitor/DockerAlertTemplates.test.ts +293 -0
  143. package/Tests/Types/Monitor/HostAlertTemplates.test.ts +249 -0
  144. package/Tests/Types/Monitor/IotAlertTemplates.test.ts +320 -0
  145. package/Tests/Types/Monitor/PodmanAlertTemplates.test.ts +271 -0
  146. package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
  147. package/Tests/Types/Port.test.ts +38 -0
  148. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +696 -0
  149. package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
  150. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
  151. package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
  152. package/Tests/UI/Utils/UserProjectsModelAPI.test.ts +714 -0
  153. package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
  154. package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
  155. package/Tests/Utils/TeamMembersByProject.test.ts +655 -0
  156. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  157. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  158. package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
  159. package/Types/Dashboard/DashboardTemplates.ts +397 -0
  160. package/Types/Database/BigIntColumnTransformer.ts +74 -0
  161. package/Types/Database/ColumnLength.ts +9 -1
  162. package/Types/Database/DatabaseProperty.ts +26 -0
  163. package/Types/Database/NumericColumnValue.ts +128 -0
  164. package/Types/Database/TableColumn.ts +11 -0
  165. package/Types/Database/UnsynchronizedIndex.ts +48 -0
  166. package/Types/Decimal.ts +16 -4
  167. package/Types/HashedString.ts +139 -4
  168. package/Types/Permission.ts +48 -0
  169. package/Types/Port.ts +16 -4
  170. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +85 -11
  171. package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
  172. package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
  173. package/UI/Components/Navbar/NavBar.tsx +12 -2
  174. package/UI/Components/Navbar/NavBarMenuModal.tsx +19 -7
  175. package/UI/Utils/ModelAPI/UserProjectsModelAPI.ts +257 -0
  176. package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
  177. package/Utils/Dashboard/Components/Index.ts +7 -0
  178. package/Utils/TeamMembersByProject.ts +237 -0
  179. package/build/dist/Models/DatabaseModels/CephCluster.js +16 -1
  180. package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
  181. package/build/dist/Models/DatabaseModels/CodeRepository.js +14 -6
  182. package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -1
  183. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
  184. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  185. package/build/dist/Models/DatabaseModels/DockerHost.js +16 -1
  186. package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
  187. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +16 -1
  188. package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -1
  189. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +16 -1
  190. package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -1
  191. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  192. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  193. package/build/dist/Models/DatabaseModels/IoTFleet.js +16 -1
  194. package/build/dist/Models/DatabaseModels/IoTFleet.js.map +1 -1
  195. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +16 -1
  196. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  197. package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
  198. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
  199. package/build/dist/Models/DatabaseModels/NetworkDevice.js +16 -1
  200. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  201. package/build/dist/Models/DatabaseModels/NetworkInterface.js +16 -1
  202. package/build/dist/Models/DatabaseModels/NetworkInterface.js.map +1 -1
  203. package/build/dist/Models/DatabaseModels/Project.js +16 -2
  204. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  205. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +16 -1
  206. package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
  207. package/build/dist/Models/DatabaseModels/Service.js +28 -1
  208. package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
  209. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
  210. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
  211. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +32 -0
  212. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
  213. package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
  214. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  215. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
  216. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
  217. package/build/dist/Models/DatabaseModels/User.js +32 -0
  218. package/build/dist/Models/DatabaseModels/User.js.map +1 -1
  219. package/build/dist/Server/API/AIAgentDataAPI.js +21 -2
  220. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  221. package/build/dist/Server/API/BaseAPI.js +11 -1
  222. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  223. package/build/dist/Server/API/DashboardAPI.js +343 -10
  224. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  225. package/build/dist/Server/API/GitHubAPI.js +94 -167
  226. package/build/dist/Server/API/GitHubAPI.js.map +1 -1
  227. package/build/dist/Server/API/UserAPI.js +187 -2
  228. package/build/dist/Server/API/UserAPI.js.map +1 -1
  229. package/build/dist/Server/EnvironmentConfig.js +34 -4
  230. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  231. package/build/dist/Server/Infrastructure/GlobalCache.js +120 -0
  232. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  233. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
  234. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  235. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
  236. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
  237. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js +14 -0
  238. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js.map +1 -0
  239. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js +59 -0
  240. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js.map +1 -0
  241. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +93 -0
  242. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js.map +1 -0
  243. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js +101 -0
  244. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js.map +1 -0
  245. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js +66 -0
  246. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js.map +1 -0
  247. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
  248. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  249. package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
  250. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  251. package/build/dist/Server/Services/AnalyticsDatabaseService.js +57 -1
  252. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  253. package/build/dist/Server/Services/CephClusterService.js +26 -76
  254. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  255. package/build/dist/Server/Services/CloudResourceService.js +23 -73
  256. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  257. package/build/dist/Server/Services/CodeRepositoryService.js +90 -2
  258. package/build/dist/Server/Services/CodeRepositoryService.js.map +1 -1
  259. package/build/dist/Server/Services/DatabaseService.js +246 -2
  260. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  261. package/build/dist/Server/Services/DockerHostService.js +19 -69
  262. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  263. package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
  264. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  265. package/build/dist/Server/Services/HostService.js +58 -102
  266. package/build/dist/Server/Services/HostService.js.map +1 -1
  267. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  268. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  269. package/build/dist/Server/Services/Index.js +2 -0
  270. package/build/dist/Server/Services/Index.js.map +1 -1
  271. package/build/dist/Server/Services/IoTFleetService.js +19 -69
  272. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  273. package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
  274. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  275. package/build/dist/Server/Services/LabelService.js +42 -7
  276. package/build/dist/Server/Services/LabelService.js.map +1 -1
  277. package/build/dist/Server/Services/MetricTypeService.js +74 -0
  278. package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
  279. package/build/dist/Server/Services/MonitorService.js +81 -9
  280. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  281. package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
  282. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  283. package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
  284. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  285. package/build/dist/Server/Services/OpenTelemetryIngestService.js +155 -25
  286. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  287. package/build/dist/Server/Services/PodmanHostService.js +19 -69
  288. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  289. package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
  290. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  291. package/build/dist/Server/Services/RumApplicationService.js +19 -70
  292. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  293. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
  294. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  295. package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
  296. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  297. package/build/dist/Server/Services/ServiceService.js +236 -61
  298. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  299. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
  300. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
  301. package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
  302. package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
  303. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
  304. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  305. package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
  306. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  307. package/build/dist/Server/Services/UserService.js +165 -0
  308. package/build/dist/Server/Services/UserService.js.map +1 -1
  309. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +31 -1
  310. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -1
  311. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +13 -0
  312. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  313. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +126 -4
  314. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  315. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js +136 -0
  316. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js.map +1 -0
  317. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
  318. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  319. package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
  320. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  321. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
  322. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  323. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
  324. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
  325. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
  326. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
  327. package/build/dist/Server/Utils/PasswordHash.js +237 -0
  328. package/build/dist/Server/Utils/PasswordHash.js.map +1 -0
  329. package/build/dist/Server/Utils/SingleFlight.js +76 -0
  330. package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
  331. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
  332. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
  333. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
  334. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  335. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
  336. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
  337. package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
  338. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  339. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  340. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  341. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  342. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  343. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
  344. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
  345. package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
  346. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  347. package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
  348. package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
  349. package/build/dist/Types/Database/ColumnLength.js +9 -1
  350. package/build/dist/Types/Database/ColumnLength.js.map +1 -1
  351. package/build/dist/Types/Database/DatabaseProperty.js +25 -0
  352. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  353. package/build/dist/Types/Database/NumericColumnValue.js +97 -0
  354. package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
  355. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  356. package/build/dist/Types/Database/UnsynchronizedIndex.js +46 -0
  357. package/build/dist/Types/Database/UnsynchronizedIndex.js.map +1 -0
  358. package/build/dist/Types/Decimal.js +14 -3
  359. package/build/dist/Types/Decimal.js.map +1 -1
  360. package/build/dist/Types/HashedString.js +95 -5
  361. package/build/dist/Types/HashedString.js.map +1 -1
  362. package/build/dist/Types/Permission.js +42 -0
  363. package/build/dist/Types/Permission.js.map +1 -1
  364. package/build/dist/Types/Port.js +15 -5
  365. package/build/dist/Types/Port.js.map +1 -1
  366. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +51 -10
  367. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  368. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
  369. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
  370. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
  371. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
  372. package/build/dist/UI/Components/Navbar/NavBar.js +3 -1
  373. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  374. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +19 -7
  375. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  376. package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js +156 -0
  377. package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js.map +1 -0
  378. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
  379. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
  380. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  381. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  382. package/build/dist/Utils/TeamMembersByProject.js +145 -0
  383. package/build/dist/Utils/TeamMembersByProject.js.map +1 -0
  384. package/package.json +1 -1
@@ -10,6 +10,7 @@ import PodmanHostService from "../../../Server/Services/PodmanHostService";
10
10
  import ProxmoxClusterService from "../../../Server/Services/ProxmoxClusterService";
11
11
  import RumApplicationService from "../../../Server/Services/RumApplicationService";
12
12
  import ServerlessFunctionService from "../../../Server/Services/ServerlessFunctionService";
13
+ import SingleFlight from "../../../Server/Utils/SingleFlight";
13
14
  import ObjectID from "../../../Types/ObjectID";
14
15
  import {
15
16
  afterEach,
@@ -21,26 +22,42 @@ import {
21
22
  } from "@jest/globals";
22
23
 
23
24
  /*
24
- * Issue #3006 was reported against Host, but HostService.updateLastSeen was
25
- * never the only service shaped that way. Ten siblings write the same two
26
- * liveness columns (lastSeenAt + otelCollectorStatus) in the SAME hook-free
27
- * UPDATE as a handful of optional columns harvested from OpenTelemetry
28
- * resource attributes — and each has its own markDisconnected* job that
29
- * flips the resource to "disconnected" 15 minutes after lastSeenAt stops
30
- * advancing.
25
+ * Every resource type's liveness heartbeat, pinned together.
31
26
  *
32
- * The clamp that stops a Postgres rejection now lives inside
33
- * updateColumnsByIdWithoutHooks (see HookFreeWriteLengthClamp.test.ts), so
34
- * the statement should not fail in the first place. This suite pins the
35
- * SECOND, independent guard, per service: if the enriched write fails
36
- * ANYWAY for any reason at all, a constraint nobody predicted, a column
37
- * type nobody clamped the liveness columns are retried on their own.
38
- * Enrichment is what gets dropped, never the heartbeat.
27
+ * Eleven services write the same two liveness columns (lastSeenAt +
28
+ * otelCollectorStatus) alongside optional columns harvested from
29
+ * OpenTelemetry resource attributes, and each has its own markDisconnected*
30
+ * job that flips the resource to "disconnected" 15 minutes after lastSeenAt
31
+ * stops advancing. They now share one implementation ResourceHeartbeat
32
+ * because eleven hand-copied versions meant eleven copies of the same defect.
39
33
  *
40
- * Also pinned per service: the throttle that keeps the steady state cheap
41
- * must not swallow the retry, and a Redis outage must fail OPEN. Marking a
42
- * live resource disconnected because the cache went down would be the same
43
- * bug wearing a different hat.
34
+ * WHAT THE DEFECT WAS. The throttle keyed on the row id but STORED a
35
+ * fingerprint of the incoming payload, skipping only on a match. That gives
36
+ * zero throttling whenever the fingerprinted payload changes as fast as the
37
+ * batches arrive, and on these paths it does:
38
+ *
39
+ * - Cluster and fleet snapshots hash per-scrape GAUGES into the
40
+ * fingerprint — runningTaskCount, osdUpCount, onlineDeviceCount,
41
+ * capacityUsedPercent. A float utilisation percentage changes on
42
+ * literally every scrape, so the fingerprint never repeated and the
43
+ * window never engaged.
44
+ * - Four of these paths (Proxmox, Ceph, IoTFleet and DockerSwarm snapshot
45
+ * writebacks) have NO upstream shouldRunMaintenance fence either, so that
46
+ * broken throttle was the only thing standing between the ingest firehose
47
+ * and a row shared by every node in the cluster.
48
+ *
49
+ * Result: one UPDATE per batch per shared row, which is the row-lock convoy
50
+ * that took production down.
51
+ *
52
+ * WHAT IS PINNED HERE, per service:
53
+ *
54
+ * 1. Liveness is gated on key PRESENCE, so no payload — however fast it
55
+ * churns — can force a second heartbeat inside a window. This is the
56
+ * assertion that would have caught the outage.
57
+ * 2. Enrichment is bounded to one write per window even under continuous
58
+ * change.
59
+ * 3. Liveness survives a failing enriched write (issue #3006), a Redis
60
+ * outage (fail open), and losing the row-lock race.
44
61
  *
45
62
  * Everything external is mocked — no Postgres, no Redis.
46
63
  */
@@ -52,16 +69,23 @@ type ServiceCase = {
52
69
  name: string;
53
70
  service: {
54
71
  updateLastSeen(id: ObjectID, extra?: never): Promise<void>;
55
- updateColumnsByIdWithoutHooks(input: {
72
+ updateColumnsByIdIfUnlockedWithoutHooks(input: {
56
73
  id: ObjectID;
57
74
  data: unknown;
58
- }): Promise<void>;
75
+ }): Promise<boolean>;
59
76
  };
77
+ /** The Redis namespace this model's gates live under. */
78
+ namespace: string;
60
79
  /** A metadata payload this service accepts, and a different one. */
61
80
  extra: Record<string, unknown>;
62
81
  otherExtra: Record<string, unknown>;
63
82
  /** A string column whose value the collector supplies. */
64
83
  oversizedExtra: Record<string, unknown>;
84
+ /**
85
+ * A payload whose values move on every scrape, if this service has one.
86
+ * These are what defeated the old throttle — see the header.
87
+ */
88
+ churningExtras?: Array<Record<string, unknown>>;
65
89
  };
66
90
 
67
91
  /** Longer than any bounded column on any of these models. */
@@ -72,6 +96,7 @@ const SERVICE_CASES: Array<ServiceCase> = [
72
96
  {
73
97
  name: "DockerHostService",
74
98
  service: DockerHostService as any,
99
+ namespace: "docker-host-last-seen",
75
100
  extra: { osType: "linux" },
76
101
  otherExtra: { osType: "windows" },
77
102
  oversizedExtra: { osVersion: OVERSIZED },
@@ -79,6 +104,7 @@ const SERVICE_CASES: Array<ServiceCase> = [
79
104
  {
80
105
  name: "PodmanHostService",
81
106
  service: PodmanHostService as any,
107
+ namespace: "podman-host-last-seen",
82
108
  extra: { osType: "linux" },
83
109
  otherExtra: { osType: "windows" },
84
110
  oversizedExtra: { osVersion: OVERSIZED },
@@ -86,6 +112,7 @@ const SERVICE_CASES: Array<ServiceCase> = [
86
112
  {
87
113
  name: "KubernetesClusterService",
88
114
  service: KubernetesClusterService as any,
115
+ namespace: "k8s-cluster-last-seen",
89
116
  extra: { agentVersion: "1.2.3" },
90
117
  otherExtra: { agentVersion: "1.2.4" },
91
118
  oversizedExtra: { agentVersion: OVERSIZED },
@@ -93,34 +120,59 @@ const SERVICE_CASES: Array<ServiceCase> = [
93
120
  {
94
121
  name: "ProxmoxClusterService",
95
122
  service: ProxmoxClusterService as any,
123
+ namespace: "proxmox-cluster-last-seen",
96
124
  extra: { pveVersion: "8.1.4" },
97
125
  otherExtra: { pveVersion: "8.2.0" },
98
126
  oversizedExtra: { pveVersion: OVERSIZED },
127
+ churningExtras: [
128
+ { pveVersion: "8.1.4", onlineNodeCount: 11, guestCount: 240 },
129
+ { pveVersion: "8.1.4", onlineNodeCount: 12, guestCount: 241 },
130
+ ],
99
131
  },
100
132
  {
101
133
  name: "IoTFleetService",
102
134
  service: IoTFleetService as any,
135
+ namespace: "iot-fleet-last-seen",
103
136
  extra: { agentVersion: "1.2.3" },
104
137
  otherExtra: { agentVersion: "1.2.4" },
105
138
  oversizedExtra: { agentVersion: OVERSIZED },
139
+ churningExtras: [
140
+ { agentVersion: "1.2.3", onlineDeviceCount: 8014 },
141
+ { agentVersion: "1.2.3", onlineDeviceCount: 8015 },
142
+ ],
106
143
  },
107
144
  {
108
145
  name: "DockerSwarmClusterService",
109
146
  service: DockerSwarmClusterService as any,
147
+ namespace: "docker-swarm-cluster-last-seen",
110
148
  extra: { dockerVersion: "25.0.3" },
111
149
  otherExtra: { dockerVersion: "26.0.0" },
112
150
  oversizedExtra: { swarmId: OVERSIZED },
151
+ churningExtras: [
152
+ { dockerVersion: "25.0.3", runningTaskCount: 512, taskCount: 530 },
153
+ { dockerVersion: "25.0.3", runningTaskCount: 513, taskCount: 530 },
154
+ ],
113
155
  },
114
156
  {
115
157
  name: "CephClusterService",
116
158
  service: CephClusterService as any,
159
+ namespace: "ceph-cluster-last-seen",
117
160
  extra: { cephVersion: "18.2.2" },
118
161
  otherExtra: { cephVersion: "19.0.0" },
119
162
  oversizedExtra: { fsid: OVERSIZED },
163
+ /*
164
+ * The worst of them: a float utilisation percentage moves on every single
165
+ * scrape, so the old fingerprint was guaranteed never to repeat.
166
+ */
167
+ churningExtras: [
168
+ { cephVersion: "18.2.2", capacityUsedPercent: 61.4, osdUpCount: 48 },
169
+ { cephVersion: "18.2.2", capacityUsedPercent: 61.41, osdUpCount: 48 },
170
+ ],
120
171
  },
121
172
  {
122
173
  name: "ServerlessFunctionService",
123
174
  service: ServerlessFunctionService as any,
175
+ namespace: "serverless-function-last-seen",
124
176
  extra: { runtimeName: "nodejs" },
125
177
  otherExtra: { runtimeName: "python" },
126
178
  oversizedExtra: { functionVersion: OVERSIZED },
@@ -128,6 +180,7 @@ const SERVICE_CASES: Array<ServiceCase> = [
128
180
  {
129
181
  name: "CloudResourceService",
130
182
  service: CloudResourceService as any,
183
+ namespace: "cloud-resource-last-seen",
131
184
  extra: { cloudProvider: "aws" },
132
185
  otherExtra: { cloudProvider: "gcp" },
133
186
  oversizedExtra: { cloudRegion: OVERSIZED },
@@ -135,6 +188,7 @@ const SERVICE_CASES: Array<ServiceCase> = [
135
188
  {
136
189
  name: "RumApplicationService",
137
190
  service: RumApplicationService as any,
191
+ namespace: "rum-application-last-seen",
138
192
  extra: { sdkLanguage: "webjs" },
139
193
  otherExtra: { sdkLanguage: "swift" },
140
194
  oversizedExtra: { clientType: OVERSIZED },
@@ -147,31 +201,83 @@ const SERVICE_CASES: Array<ServiceCase> = [
147
201
  */
148
202
  name: "HostService",
149
203
  service: HostService as any,
204
+ namespace: "host-last-seen",
150
205
  extra: { osType: "linux" },
151
206
  otherExtra: { osType: "windows" },
152
207
  oversizedExtra: { hostId: OVERSIZED },
208
+ churningExtras: [
209
+ { osType: "linux", processCount: 412, totalMemoryBytes: 17179869184 },
210
+ { osType: "linux", processCount: 413, totalMemoryBytes: 17179869184 },
211
+ ],
153
212
  },
154
213
  ];
155
214
  /* eslint-enable @typescript-eslint/no-explicit-any */
156
215
 
157
216
  describe.each(SERVICE_CASES)(
158
217
  "$name.updateLastSeen",
159
- ({ service, extra, otherExtra, oversizedExtra }: ServiceCase) => {
218
+ ({
219
+ service,
220
+ namespace,
221
+ extra,
222
+ otherExtra,
223
+ oversizedExtra,
224
+ churningExtras,
225
+ }: ServiceCase) => {
160
226
  let writes: Array<WriteCall>;
161
227
  let cache: Map<string, string>;
162
228
  let deletedCacheKeys: Array<string>;
229
+ /** Set false to simulate the row being held by another writer. */
230
+ let writeLands: boolean;
163
231
 
164
232
  const RESOURCE_ID: ObjectID = ObjectID.generate();
165
233
 
234
+ /**
235
+ * A faithful in-memory Redis. SET NX succeeds only on an absent key and
236
+ * compare-and-claim only on a differing value — stubbing either to a
237
+ * constant would make this suite agree with a broken implementation.
238
+ */
239
+ function mockCache(): void {
240
+ jest
241
+ .spyOn(GlobalCache, "setStringIfNotExists")
242
+ .mockImplementation(async (ns: string, key: string, value: string) => {
243
+ const full: string = `${ns}:${key}`;
244
+ if (cache.has(full)) {
245
+ return false;
246
+ }
247
+ cache.set(full, value);
248
+ return true;
249
+ });
250
+
251
+ jest
252
+ .spyOn(GlobalCache, "setStringIfChanged")
253
+ .mockImplementation(async (ns: string, key: string, value: string) => {
254
+ const full: string = `${ns}:${key}`;
255
+ if (cache.get(full) === value) {
256
+ return false;
257
+ }
258
+ cache.set(full, value);
259
+ return true;
260
+ });
261
+
262
+ jest
263
+ .spyOn(GlobalCache, "deleteKey")
264
+ .mockImplementation(async (ns: string, key: string) => {
265
+ const full: string = `${ns}:${key}`;
266
+ deletedCacheKeys.push(full);
267
+ cache.delete(full);
268
+ });
269
+ }
270
+
166
271
  /** Records every hook-free UPDATE instead of issuing it. */
167
272
  function mockWritesSucceeding(): void {
168
273
  jest
169
- .spyOn(service, "updateColumnsByIdWithoutHooks")
274
+ .spyOn(service, "updateColumnsByIdIfUnlockedWithoutHooks")
170
275
  .mockImplementation(async (input: { id: ObjectID; data: unknown }) => {
171
276
  writes.push({
172
277
  id: input.id,
173
278
  data: { ...(input.data as Record<string, unknown>) },
174
279
  });
280
+ return writeLands;
175
281
  });
176
282
  }
177
283
 
@@ -181,7 +287,7 @@ describe.each(SERVICE_CASES)(
181
287
  */
182
288
  function mockEnrichedWriteFailing(): void {
183
289
  jest
184
- .spyOn(service, "updateColumnsByIdWithoutHooks")
290
+ .spyOn(service, "updateColumnsByIdIfUnlockedWithoutHooks")
185
291
  .mockImplementation(async (input: { id: ObjectID; data: unknown }) => {
186
292
  const data: Record<string, unknown> = {
187
293
  ...(input.data as Record<string, unknown>),
@@ -190,6 +296,7 @@ describe.each(SERVICE_CASES)(
190
296
  if (Object.keys(data).length > 2) {
191
297
  throw new Error("value too long for type character varying(100)");
192
298
  }
299
+ return true;
193
300
  });
194
301
  }
195
302
 
@@ -197,33 +304,24 @@ describe.each(SERVICE_CASES)(
197
304
  return writes[writes.length - 1]!.data;
198
305
  }
199
306
 
307
+ /** Re-opens the liveness window without touching the enrichment gates. */
308
+ function expireLivenessWindow(): void {
309
+ cache.delete(`${namespace}:${RESOURCE_ID.toString()}`);
310
+ SingleFlight.clear();
311
+ }
312
+
200
313
  beforeEach(() => {
201
314
  writes = [];
202
315
  cache = new Map<string, string>();
203
316
  deletedCacheKeys = [];
204
-
205
- jest
206
- .spyOn(GlobalCache, "getString")
207
- .mockImplementation(async (namespace: string, key: string) => {
208
- return cache.get(`${namespace}:${key}`) ?? null;
209
- });
210
- jest
211
- .spyOn(GlobalCache, "setString")
212
- .mockImplementation(
213
- async (namespace: string, key: string, value: string) => {
214
- cache.set(`${namespace}:${key}`, value);
215
- },
216
- );
217
- jest
218
- .spyOn(GlobalCache, "deleteKey")
219
- .mockImplementation(async (namespace: string, key: string) => {
220
- deletedCacheKeys.push(`${namespace}:${key}`);
221
- cache.delete(`${namespace}:${key}`);
222
- });
317
+ writeLands = true;
318
+ SingleFlight.clear();
319
+ mockCache();
223
320
  });
224
321
 
225
322
  afterEach(() => {
226
323
  jest.restoreAllMocks();
324
+ SingleFlight.clear();
227
325
  });
228
326
 
229
327
  describe("liveness columns", () => {
@@ -247,25 +345,88 @@ describe.each(SERVICE_CASES)(
247
345
 
248
346
  test("skips the write when the same metadata was written recently", async () => {
249
347
  await service.updateLastSeen(RESOURCE_ID, extra as never);
348
+ SingleFlight.clear();
250
349
  await service.updateLastSeen(RESOURCE_ID, extra as never);
251
350
 
252
351
  expect(writes).toHaveLength(1);
253
352
  });
254
353
 
255
- test("writes again as soon as any metadata changes", async () => {
354
+ test("passes collector metadata through to the write path", async () => {
256
355
  await service.updateLastSeen(RESOURCE_ID, extra as never);
257
- await service.updateLastSeen(RESOURCE_ID, otherExtra as never);
258
356
 
259
- expect(writes).toHaveLength(2);
357
+ for (const [column, value] of Object.entries(extra)) {
358
+ expect(lastWrite()[column]).toBe(value);
359
+ }
260
360
  });
261
361
 
262
- test("passes collector metadata through to the write path", async () => {
362
+ /*
363
+ * A changed attribute still lands — but in the next window, not on the
364
+ * very next batch. That is a deliberate narrowing of the old contract:
365
+ * change detection cannot tell a real update apart from a gauge that
366
+ * moves every scrape, both present as "it changed", and only one of
367
+ * those is bounded. Bounding both is what removes the convoy. The delay
368
+ * is at most one throttle window and `lastSeenAt` is unaffected, since
369
+ * it rides the separate liveness gate.
370
+ */
371
+ test("a metadata change lands in the next window", async () => {
263
372
  await service.updateLastSeen(RESOURCE_ID, extra as never);
373
+ expireLivenessWindow();
374
+ cache.delete(`${namespace}-write-window:${RESOURCE_ID.toString()}`);
264
375
 
265
- for (const [column, value] of Object.entries(extra)) {
376
+ await service.updateLastSeen(RESOURCE_ID, otherExtra as never);
377
+
378
+ expect(writes).toHaveLength(2);
379
+ for (const [column, value] of Object.entries(otherExtra)) {
266
380
  expect(lastWrite()[column]).toBe(value);
267
381
  }
268
382
  });
383
+
384
+ /*
385
+ * THE OUTAGE, reproduced per service. Continuously-changing metadata
386
+ * must not be able to buy more than one write per window. Under the old
387
+ * throttle each of these calls issued its own UPDATE against a row
388
+ * shared by every node reporting into it.
389
+ */
390
+ test("continuously changing metadata cannot force a write per batch", async () => {
391
+ for (let batch: number = 0; batch < 20; batch++) {
392
+ SingleFlight.clear();
393
+ await service.updateLastSeen(RESOURCE_ID, {
394
+ ...extra,
395
+ ...(batch % 2 === 0 ? otherExtra : {}),
396
+ } as never);
397
+ }
398
+
399
+ expect(writes.length).toBeLessThanOrEqual(2);
400
+ });
401
+
402
+ if (churningExtras) {
403
+ /*
404
+ * The concrete production shape for this service: a payload whose
405
+ * gauges move on every scrape (task counts, online device counts,
406
+ * capacity percentages). These are hashed into the fingerprint, so
407
+ * the old throttle never engaged for exactly the resources producing
408
+ * the most batches.
409
+ */
410
+ test("per-scrape gauges cannot defeat the throttle", async () => {
411
+ for (let scrape: number = 0; scrape < 20; scrape++) {
412
+ SingleFlight.clear();
413
+ await service.updateLastSeen(
414
+ RESOURCE_ID,
415
+ churningExtras[scrape % churningExtras.length] as never,
416
+ );
417
+ }
418
+
419
+ expect(writes.length).toBeLessThanOrEqual(2);
420
+ });
421
+
422
+ test("the gauges are still written when a write happens", async () => {
423
+ await service.updateLastSeen(RESOURCE_ID, churningExtras[0] as never);
424
+
425
+ for (const [column, value] of Object.entries(churningExtras[0]!)) {
426
+ expect(lastWrite()[column]).toBe(value);
427
+ }
428
+ });
429
+ }
269
430
  });
270
431
 
271
432
  describe("fallback when the enriched write fails", () => {
@@ -297,16 +458,26 @@ describe.each(SERVICE_CASES)(
297
458
  expect(writes[1]!.id.toString()).toBe(RESOURCE_ID.toString());
298
459
  });
299
460
 
300
- test("busts the throttle cache so the next batch is not skipped", async () => {
461
+ /*
462
+ * The enrichment gates were claimed before the write that failed, so
463
+ * without releasing them the next batch would short-circuit and the
464
+ * retry would never happen.
465
+ */
466
+ test("re-opens the enrichment gates so the next batch retries", async () => {
301
467
  await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
302
468
 
303
- expect(deletedCacheKeys).toHaveLength(1);
304
- expect(deletedCacheKeys[0]).toContain(RESOURCE_ID.toString());
469
+ expect(deletedCacheKeys).toContain(
470
+ `${namespace}-fingerprint:${RESOURCE_ID.toString()}`,
471
+ );
472
+ expect(deletedCacheKeys).toContain(
473
+ `${namespace}-write-window:${RESOURCE_ID.toString()}`,
474
+ );
305
475
  });
306
476
 
307
- test("the next batch retries rather than short-circuiting on a cache hit", async () => {
477
+ test("the next batch retries rather than short-circuiting", async () => {
308
478
  await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
309
479
  writes = [];
480
+ expireLivenessWindow();
310
481
 
311
482
  await service.updateLastSeen(RESOURCE_ID, oversizedExtra as never);
312
483
 
@@ -330,7 +501,7 @@ describe.each(SERVICE_CASES)(
330
501
  test("surfaces a genuinely broken database instead of hiding it", async () => {
331
502
  // Both attempts fail — the caller must find out.
332
503
  jest
333
- .spyOn(service, "updateColumnsByIdWithoutHooks")
504
+ .spyOn(service, "updateColumnsByIdIfUnlockedWithoutHooks")
334
505
  .mockRejectedValue(new Error("connection terminated"));
335
506
 
336
507
  await expect(
@@ -344,24 +515,90 @@ describe.each(SERVICE_CASES)(
344
515
  mockWritesSucceeding();
345
516
  });
346
517
 
347
- test("a read failure still writes — never mark a live resource disconnected", async () => {
518
+ /*
519
+ * Never mark a live resource disconnected because the cache went down.
520
+ * Affordable only because the write cannot queue — see the SKIP LOCKED
521
+ * note on the write primitive.
522
+ */
523
+ test("a liveness gate failure still writes", async () => {
348
524
  jest
349
- .spyOn(GlobalCache, "getString")
525
+ .spyOn(GlobalCache, "setStringIfNotExists")
350
526
  .mockRejectedValue(new Error("redis down"));
351
527
 
352
528
  await service.updateLastSeen(RESOURCE_ID, extra as never);
353
529
 
354
530
  expect(writes).toHaveLength(1);
531
+ expect(lastWrite()["lastSeenAt"]).toBeInstanceOf(Date);
355
532
  });
356
533
 
357
- test("a write failure still writes", async () => {
534
+ test("keeps writing liveness for as long as the cache is down", async () => {
358
535
  jest
359
- .spyOn(GlobalCache, "setString")
536
+ .spyOn(GlobalCache, "setStringIfNotExists")
537
+ .mockRejectedValue(new Error("redis down"));
538
+
539
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
540
+ SingleFlight.clear();
541
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
542
+
543
+ expect(writes).toHaveLength(2);
544
+ });
545
+
546
+ /*
547
+ * Enrichment gets the OPPOSITE treatment: opening that gate during an
548
+ * outage would restore the per-batch write storm, and nothing is
549
+ * mis-reported by writing a descriptive column a little later.
550
+ */
551
+ test("a change-detection failure suppresses enrichment, not liveness", async () => {
552
+ jest
553
+ .spyOn(GlobalCache, "setStringIfChanged")
360
554
  .mockRejectedValue(new Error("redis down"));
361
555
 
362
556
  await service.updateLastSeen(RESOURCE_ID, extra as never);
363
557
 
364
558
  expect(writes).toHaveLength(1);
559
+ expect(Object.keys(lastWrite()).sort()).toEqual([
560
+ "lastSeenAt",
561
+ "otelCollectorStatus",
562
+ ]);
563
+ });
564
+ });
565
+
566
+ describe("losing the row-lock race", () => {
567
+ beforeEach(() => {
568
+ mockWritesSucceeding();
569
+ writeLands = false;
570
+ });
571
+
572
+ test("uses the non-blocking write primitive", async () => {
573
+ await service.updateLastSeen(RESOURCE_ID);
574
+
575
+ expect(
576
+ service.updateColumnsByIdIfUnlockedWithoutHooks,
577
+ ).toHaveBeenCalled();
578
+ });
579
+
580
+ /*
581
+ * A skipped liveness bump is a non-event — the writer holding the lock
582
+ * is storing the same timestamp. Retrying it would generate pointless
583
+ * statements against a row that is demonstrably being written.
584
+ */
585
+ test("a skipped liveness-only write is dropped silently", async () => {
586
+ await service.updateLastSeen(RESOURCE_ID);
587
+
588
+ expect(deletedCacheKeys).toHaveLength(0);
589
+ });
590
+
591
+ /*
592
+ * Enrichment is different: the lock holder may be writing an OLDER
593
+ * payload, and the gates this call claimed would suppress the newer one
594
+ * for the rest of the window.
595
+ */
596
+ test("a skipped enrichment write re-opens the gates", async () => {
597
+ await service.updateLastSeen(RESOURCE_ID, extra as never);
598
+
599
+ expect(deletedCacheKeys).toContain(
600
+ `${namespace}-fingerprint:${RESOURCE_ID.toString()}`,
601
+ );
365
602
  });
366
603
  });
367
604
  },
@@ -384,4 +621,16 @@ describe("liveness fallback coverage", () => {
384
621
 
385
622
  expect(new Set(names).size).toBe(names.length);
386
623
  });
624
+
625
+ /*
626
+ * The namespace in each case must match the one the service actually uses,
627
+ * or the gate assertions above silently check keys nobody writes.
628
+ */
629
+ test("no namespace is shared between services", () => {
630
+ const namespaces: Array<string> = SERVICE_CASES.map((c: ServiceCase) => {
631
+ return c.namespace;
632
+ });
633
+
634
+ expect(new Set(namespaces).size).toBe(namespaces.length);
635
+ });
387
636
  });