@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
@@ -1,10 +1,29 @@
1
+ import Team from "../../Models/DatabaseModels/Team";
2
+ import TeamMember from "../../Models/DatabaseModels/TeamMember";
1
3
  import User from "../../Models/DatabaseModels/User";
4
+ import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
5
+ import Query from "../../Types/BaseDatabase/Query";
6
+ import LIMIT_MAX, { DEFAULT_LIMIT } from "../../Types/Database/LimitMax";
7
+ import BadDataException from "../../Types/Exception/BadDataException";
2
8
  import NotFoundException from "../../Types/Exception/NotFoundException";
9
+ import { JSONArray, JSONObject } from "../../Types/JSON";
3
10
  import ObjectID from "../../Types/ObjectID";
11
+ import PositiveNumber from "../../Types/PositiveNumber";
12
+ import TeamMembersByProject, {
13
+ UserProjectMembership,
14
+ } from "../../Utils/TeamMembersByProject";
15
+ import MasterAdminAuthorization from "../Middleware/MasterAdminAuthorization";
16
+ import TeamMemberService from "../Services/TeamMemberService";
4
17
  import UserService, {
5
18
  Service as UserServiceType,
6
19
  } from "../Services/UserService";
7
- import { ExpressRequest, ExpressResponse } from "../Utils/Express";
20
+ import CommonAPI from "./CommonAPI";
21
+ import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
22
+ import {
23
+ ExpressRequest,
24
+ ExpressResponse,
25
+ NextFunction,
26
+ } from "../Utils/Express";
8
27
  import logger, { getLogAttributesFromRequest } from "../Utils/Logger";
9
28
  import Response from "../Utils/Response";
10
29
  import BaseAPI from "./BaseAPI";
@@ -16,6 +35,162 @@ export default class UserAPI extends BaseAPI<User, UserServiceType> {
16
35
  public constructor() {
17
36
  super(User, UserService);
18
37
 
38
+ /*
39
+ * Every project one user belongs to, one row per project, for the Admin
40
+ * Dashboard's User > Projects page.
41
+ *
42
+ * There is no CRUD list that answers this. `GET /team-member` scoped to a
43
+ * user returns MEMBERSHIPS - a (user, team) pair - so a user on three teams
44
+ * of one project comes back as three rows and reads as three projects. This
45
+ * endpoint folds those into one row per project carrying every team the
46
+ * user is on there, which is both what the page renders and what any
47
+ * caller asking "which projects is this person in?" actually wants.
48
+ *
49
+ * Master-admin only: it deliberately reads across every tenant, which is
50
+ * exactly what no project-scoped caller is allowed to do. The middleware is
51
+ * the gate; the read below runs as root because a master admin is not a
52
+ * member of the projects being listed and so has no tenant permissions to
53
+ * read them with.
54
+ */
55
+ this.router.post(
56
+ `${new this.entityType().getCrudApiPath()?.toString()}/:userId/projects`,
57
+ MasterAdminAuthorization.isAuthorizedMasterAdminMiddleware,
58
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
59
+ try {
60
+ const userId: ObjectID = UserAPI.getUserIdFromParams(req);
61
+
62
+ const memberships: Array<TeamMember> = await TeamMemberService.findBy(
63
+ {
64
+ query: {
65
+ userId: userId,
66
+ },
67
+ select: {
68
+ _id: true,
69
+ projectId: true,
70
+ teamId: true,
71
+ hasAcceptedInvitation: true,
72
+ createdAt: true,
73
+ project: {
74
+ _id: true,
75
+ name: true,
76
+ slug: true,
77
+ },
78
+ team: {
79
+ _id: true,
80
+ name: true,
81
+ },
82
+ },
83
+ limit: LIMIT_MAX,
84
+ skip: 0,
85
+ sort: {},
86
+ props: {
87
+ isRoot: true,
88
+ },
89
+ },
90
+ );
91
+
92
+ const rows: Array<UserProjectMembership> =
93
+ TeamMembersByProject.sortByProjectName(
94
+ TeamMembersByProject.groupByProject(memberships),
95
+ );
96
+
97
+ const skip: number = UserAPI.getPositiveIntegerParam(
98
+ req.query["skip"],
99
+ 0,
100
+ );
101
+ const limit: number = UserAPI.getPositiveIntegerParam(
102
+ req.query["limit"],
103
+ DEFAULT_LIMIT,
104
+ );
105
+
106
+ const page: Array<UserProjectMembership> = rows.slice(
107
+ skip,
108
+ skip + limit,
109
+ );
110
+
111
+ return Response.sendJsonArrayResponse(
112
+ req,
113
+ res,
114
+ page.map((row: UserProjectMembership) => {
115
+ return UserAPI.serializeUserProjectMembership(userId, row);
116
+ }),
117
+ new PositiveNumber(rows.length),
118
+ );
119
+ } catch (err) {
120
+ return next(err);
121
+ }
122
+ },
123
+ );
124
+
125
+ /*
126
+ * Removes one user from one project - every team they belong to in it - in
127
+ * a single call.
128
+ *
129
+ * A row on the User > Projects page is a project, so "remove" there means
130
+ * "remove from the project". Doing that as N separate DELETEs from the
131
+ * browser is not equivalent: TeamMemberService.onBeforeDelete refuses to
132
+ * remove the last accepted member of a team that shouldHaveAtLeastOneMember
133
+ * (the Owners team), and it refuses per request - so a client-side loop
134
+ * deletes every other membership first and only then reports the failure,
135
+ * leaving the user stripped of the teams the admin was just told they had
136
+ * not lost. One deleteBy over the whole set runs that guard against all of
137
+ * the user's memberships in the project before anything is deleted.
138
+ *
139
+ * This is the master-admin twin of
140
+ * POST /team-member/remove-user-from-project, which takes its project from
141
+ * the request's tenant. A master admin is not a member of the project and
142
+ * sends no tenant, so the project is named in the body here - the
143
+ * master-admin middleware, not a tenant, is what authorizes the call.
144
+ *
145
+ * The delete runs with the CALLER'S props rather than as root, so the
146
+ * Owners-team and SCIM Push Groups guards still apply: a master admin can
147
+ * remove people, but not leave a project ownerless or fight the customer's
148
+ * identity provider behind its back.
149
+ */
150
+ this.router.post(
151
+ `${new this.entityType().getCrudApiPath()?.toString()}/:userId/remove-from-project`,
152
+ MasterAdminAuthorization.isAuthorizedMasterAdminMiddleware,
153
+ async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
154
+ try {
155
+ const userId: ObjectID = UserAPI.getUserIdFromParams(req);
156
+
157
+ const projectIdParam: string = (
158
+ (req.body?.["projectId"] as string) || ""
159
+ ).trim();
160
+
161
+ if (!projectIdParam) {
162
+ return Response.sendErrorResponse(
163
+ req,
164
+ res,
165
+ new BadDataException("Project ID is required"),
166
+ );
167
+ }
168
+
169
+ ObjectID.validateUUID(projectIdParam);
170
+
171
+ const props: DatabaseCommonInteractionProps =
172
+ await CommonAPI.getDatabaseCommonInteractionProps(req);
173
+
174
+ const numberOfMembershipsDeleted: number =
175
+ await TeamMemberService.deleteBy({
176
+ query: {
177
+ userId: userId,
178
+ projectId: new ObjectID(projectIdParam),
179
+ } as Query<TeamMember>,
180
+ limit: LIMIT_MAX,
181
+ skip: 0,
182
+ props: props,
183
+ });
184
+
185
+ return Response.sendJsonObjectResponse(req, res, {
186
+ numberOfMembershipsDeleted: numberOfMembershipsDeleted,
187
+ });
188
+ } catch (err) {
189
+ return next(err);
190
+ }
191
+ },
192
+ );
193
+
19
194
  this.router.get(
20
195
  `${new this.entityType().getCrudApiPath()?.toString()}/profile-picture/:userId`,
21
196
  async (req: ExpressRequest, res: ExpressResponse) => {
@@ -78,6 +253,93 @@ export default class UserAPI extends BaseAPI<User, UserServiceType> {
78
253
  );
79
254
  }
80
255
 
256
+ /**
257
+ * The `:userId` in the path, rejected loudly if it is not a uuid.
258
+ *
259
+ * Validating rather than trusting matters even behind the master-admin gate:
260
+ * the value flows into a query, and a malformed id should fail as bad input
261
+ * rather than reach the database layer.
262
+ */
263
+ private static getUserIdFromParams(req: ExpressRequest): ObjectID {
264
+ const userIdParam: string = ((req.params["userId"] as string) || "").trim();
265
+
266
+ if (!userIdParam) {
267
+ throw new BadDataException("User ID is required");
268
+ }
269
+
270
+ ObjectID.validateUUID(userIdParam);
271
+
272
+ return new ObjectID(userIdParam);
273
+ }
274
+
275
+ /**
276
+ * A non-negative integer from a query string, or the fallback.
277
+ *
278
+ * Anything unparseable (missing, "abc", "-5", "1e9999") falls back rather
279
+ * than becoming NaN - a NaN skip/limit turns Array.slice into "return
280
+ * everything from 0", which would silently ignore paging instead of failing.
281
+ */
282
+ private static getPositiveIntegerParam(
283
+ value: unknown,
284
+ fallback: number,
285
+ ): number {
286
+ if (value === undefined || value === null || value === "") {
287
+ return fallback;
288
+ }
289
+
290
+ const parsed: number = Number(value.toString());
291
+
292
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 0) {
293
+ return fallback;
294
+ }
295
+
296
+ return parsed;
297
+ }
298
+
299
+ /**
300
+ * One grouped row as JSON.
301
+ *
302
+ * The row is sent as a TeamMember - the model the row is built from, and the
303
+ * one the Admin Dashboard's table is typed against - carrying the project it
304
+ * stands for. The aggregate fields (`teams`, the counts) are not columns of
305
+ * TeamMember, so they are attached after serialization; the client reads them
306
+ * off the raw JSON before hydrating the model.
307
+ */
308
+ private static serializeUserProjectMembership(
309
+ userId: ObjectID,
310
+ row: UserProjectMembership,
311
+ ): JSONObject {
312
+ const teamMember: TeamMember = new TeamMember();
313
+
314
+ if (row.id) {
315
+ teamMember._id = row.id;
316
+ }
317
+
318
+ teamMember.userId = userId;
319
+ teamMember.hasAcceptedInvitation = row.hasAcceptedInvitation;
320
+
321
+ if (row.projectId) {
322
+ teamMember.projectId = row.projectId;
323
+ }
324
+
325
+ if (row.project) {
326
+ teamMember.project = row.project;
327
+ }
328
+
329
+ if (row.joinedAt) {
330
+ teamMember.createdAt = row.joinedAt;
331
+ }
332
+
333
+ const json: JSONObject = BaseModel.toJSON(teamMember, TeamMember);
334
+
335
+ json["teams"] = BaseModel.toJSONArray(row.teams, Team) as JSONArray;
336
+ json["teamCount"] = row.teams.length;
337
+ json["pendingTeamCount"] = row.pendingTeamCount;
338
+ json["teamMemberIds"] = [...row.teamMemberIds];
339
+
340
+ return json;
341
+ }
342
+
81
343
  private sendBlankProfile(req: ExpressRequest, res: ExpressResponse): void {
82
344
  Response.setNoCacheHeaders(res);
83
345
 
@@ -167,13 +167,43 @@ export const PostgresStatementTimeoutMs: number = parseInt(
167
167
  10,
168
168
  );
169
169
 
170
+ /*
171
+ * Postgres-side lock timeout (ms). Caps how long a statement will WAIT for a
172
+ * row/table lock before giving up — distinct from statement_timeout, which
173
+ * caps total execution.
174
+ *
175
+ * Without it, contention on a hot row degrades into a strictly-ordered queue:
176
+ * every waiter pins a backend for the sum of everyone ahead of it. That is the
177
+ * row-lock convoy that took production down — 892 connections parked on locks,
178
+ * the tail waiting 3.7 hours — and no amount of database capacity changes it,
179
+ * because the queue itself is the failure. A short lock_timeout converts an
180
+ * unbounded wait into a fast, retryable error.
181
+ *
182
+ * Must stay well below statement_timeout so a lock wait surfaces as
183
+ * `lock_not_available` (55P03) rather than a generic statement timeout — the
184
+ * two want very different handling.
185
+ */
186
+ export const PostgresLockTimeoutMs: number = parseInt(
187
+ process.env["DATABASE_LOCK_TIMEOUT_MS"] || "3000",
188
+ 10,
189
+ );
190
+
170
191
  /*
171
192
  * Node-postgres client-side query timeout (ms). Belt-and-braces for the
172
193
  * server-side statement_timeout — fires even if the connection has gone
173
194
  * silent or the server-side timeout doesn't kick in.
195
+ *
196
+ * Deliberately LONGER than statement_timeout. These two used to be equal, and
197
+ * because the client timer starts before the packet even reaches the backend,
198
+ * the client always won by a round trip — so the app never observed Postgres's
199
+ * real SQLSTATE and, worse, the client-side timeout only ABANDONS the query:
200
+ * the backend keeps running and keeps its place in the lock queue. Letting the
201
+ * server win means contention is cancelled server-side instead of accumulating
202
+ * invisibly behind a pooler.
174
203
  */
175
204
  export const PostgresQueryTimeoutMs: number = parseInt(
176
- process.env["DATABASE_QUERY_TIMEOUT_MS"] || "30000",
205
+ process.env["DATABASE_QUERY_TIMEOUT_MS"] ||
206
+ String(PostgresStatementTimeoutMs + 5000),
177
207
  10,
178
208
  );
179
209
 
@@ -447,10 +477,14 @@ export const DisableQueueWorkers: boolean =
447
477
  * When "false", this process does NOT run schema or data migrations on boot.
448
478
  * Set on runtime pods (app/worker/nginx) when a dedicated one-shot migrate Job
449
479
  * (App/Migrate.ts) owns migrations instead, so the fleet's many replicas never
450
- * run them — which is what makes PgBouncer transaction-mode pooling safe (the
451
- * data-migration session advisory lock then only ever runs in the single Job).
480
+ * run them — which keeps boot DDL off pooled connections and, since the data
481
+ * migration runner no longer takes an advisory lock, is also what keeps two
482
+ * replicas from running the same migration concurrently.
483
+ *
452
484
  * Default true preserves the original self-migrating-on-boot behavior used by
453
- * docker-compose and any deploy that does not run the migrate Job.
485
+ * docker-compose and any deploy that does not run the migrate Job. Those
486
+ * deployments DO run several unserialized runners, so data migrations must be
487
+ * written to tolerate it (see Workers/Utils/DataMigration.ts).
454
488
  */
455
489
  export const RunDatabaseMigrationsOnBoot: boolean =
456
490
  process.env["RUN_DATABASE_MIGRATIONS_ON_BOOT"] !== "false";
@@ -176,6 +176,158 @@ export default abstract class GlobalCache {
176
176
  await client.set(`${namespace}-${key}`, value, "EX", expiresInSeconds);
177
177
  }
178
178
 
179
+ /*
180
+ * Atomic acquire-once fence: SET ... EX ... NX. Returns true ONLY for the
181
+ * caller that created the key; every concurrent caller gets false until it
182
+ * expires.
183
+ *
184
+ * This exists because `getString()` followed by `setString()` is a
185
+ * check-then-act race, and at ingest concurrency that race is not
186
+ * theoretical — it is the whole problem. When N workers resolve the same
187
+ * row in the same instant they ALL read a miss and they ALL proceed, so a
188
+ * fence meant to admit one writer per window admits N. With 100 worker pods
189
+ * that turned a once-a-minute heartbeat into thousands of simultaneous
190
+ * UPDATEs against the same handful of rows, and the resulting row-lock
191
+ * convoy starved the Postgres connection pool (some statements queued for
192
+ * hours). Redis evaluates SET NX atomically, so exactly one caller wins no
193
+ * matter how many arrive together.
194
+ *
195
+ * Fence keys should carry the TTL jitter from `withJitter()` — see there for
196
+ * why synchronized expiry re-creates the herd this is meant to prevent.
197
+ */
198
+ @CaptureSpan()
199
+ public static async setStringIfNotExists(
200
+ namespace: string,
201
+ key: string,
202
+ value: string,
203
+ options?: CacheSetOptions,
204
+ ): Promise<boolean> {
205
+ const client: ClientType | null = Redis.getClient();
206
+
207
+ if (!client || !Redis.isConnected()) {
208
+ throw new DatabaseNotConnectedException("Cache is not connected");
209
+ }
210
+
211
+ const expiresInSeconds: number =
212
+ options?.expiresInSeconds ?? OneUptimeDate.getSecondsInDays(30);
213
+
214
+ const result: string | null = await client.set(
215
+ `${namespace}-${key}`,
216
+ value,
217
+ "EX",
218
+ expiresInSeconds,
219
+ "NX",
220
+ );
221
+
222
+ /*
223
+ * ioredis resolves to "OK" when the key was created and to null when it
224
+ * already existed. Anything else (a driver/protocol change) is treated as
225
+ * "did not acquire" — losing the fence is safe, winning it wrongly is not.
226
+ */
227
+ return result === "OK";
228
+ }
229
+
230
+ /*
231
+ * Atomic compare-and-claim: claim the window unless the key ALREADY holds
232
+ * `value`. Returns true only for the caller that claimed it.
233
+ *
234
+ * `setStringIfNotExists` is the right primitive for a presence fence and the
235
+ * wrong one for a fingerprint throttle. Plain NX fails whenever the key
236
+ * exists — including when it holds a STALE fingerprint — so a genuinely
237
+ * changed payload (a new service.version after a deploy, a changed host IP)
238
+ * would be suppressed for the whole window and nobody would persist it. This
239
+ * keeps the bust-on-change behaviour that callers depend on while collapsing
240
+ * the GET-then-SET race into one atomic evaluation.
241
+ *
242
+ * The key is passed as KEYS[1] rather than inlined into the script body so
243
+ * the script stays correct on Redis Cluster, which routes by declared keys.
244
+ */
245
+ @CaptureSpan()
246
+ public static async setStringIfChanged(
247
+ namespace: string,
248
+ key: string,
249
+ value: string,
250
+ options?: CacheSetOptions,
251
+ ): Promise<boolean> {
252
+ const client: ClientType | null = Redis.getClient();
253
+
254
+ if (!client || !Redis.isConnected()) {
255
+ throw new DatabaseNotConnectedException("Cache is not connected");
256
+ }
257
+
258
+ const expiresInSeconds: number =
259
+ options?.expiresInSeconds ?? OneUptimeDate.getSecondsInDays(30);
260
+
261
+ const result: unknown = await client.eval(
262
+ "if redis.call('GET', KEYS[1]) == ARGV[1] then return 0 " +
263
+ "else redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2]) return 1 end",
264
+ 1,
265
+ `${namespace}-${key}`,
266
+ value,
267
+ String(expiresInSeconds),
268
+ );
269
+
270
+ return result === 1;
271
+ }
272
+
273
+ /*
274
+ * Spread a fence TTL over [ttl, ttl + 25%].
275
+ *
276
+ * A fixed TTL makes every fence for a fleet of rows expire in lockstep once
277
+ * their writes have been aligned by a common event — a deploy, a worker
278
+ * scale-up, a Redis restart. The window then reopens for thousands of rows
279
+ * in the same second and the herd re-forms on a one-minute period. Jitter
280
+ * breaks that alignment permanently: the fences drift apart after the first
281
+ * window and stay apart.
282
+ *
283
+ * The upper bound stays well inside the 15-minute disconnection sweep, so a
284
+ * jittered heartbeat can never make a live resource look disconnected.
285
+ */
286
+ public static withJitter(expiresInSeconds: number): number {
287
+ if (expiresInSeconds <= 0) {
288
+ return expiresInSeconds;
289
+ }
290
+
291
+ return Math.ceil(expiresInSeconds * (1 + Math.random() * 0.25));
292
+ }
293
+
294
+ /*
295
+ * Atomic compare-and-delete: drop the key only if it still holds `value`.
296
+ * Returns true only when this caller's value was the one removed.
297
+ *
298
+ * This is the release half of a lease taken with `setStringIfNotExists`, and
299
+ * the comparison is what makes the release safe. A plain `deleteKey` is a
300
+ * check-then-act race in disguise: if the holder overruns its TTL the key
301
+ * expires, a second worker legitimately acquires the lease, and the first
302
+ * worker's release then deletes the SECOND worker's lease — handing the same
303
+ * lease to a third. Comparing the holder token collapses that into one
304
+ * atomic evaluation, so a late release is a no-op instead of a double-grant.
305
+ *
306
+ * The key is passed as KEYS[1] rather than inlined into the script body so
307
+ * the script stays correct on Redis Cluster, which routes by declared keys.
308
+ */
309
+ @CaptureSpan()
310
+ public static async deleteKeyIfValue(
311
+ namespace: string,
312
+ key: string,
313
+ value: string,
314
+ ): Promise<boolean> {
315
+ const client: ClientType | null = Redis.getClient();
316
+
317
+ if (!client || !Redis.isConnected()) {
318
+ throw new DatabaseNotConnectedException("Cache is not connected");
319
+ }
320
+
321
+ const result: unknown = await client.eval(
322
+ "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end",
323
+ 1,
324
+ `${namespace}-${key}`,
325
+ value,
326
+ );
327
+
328
+ return result === 1;
329
+ }
330
+
179
331
  @CaptureSpan()
180
332
  public static async deleteKey(namespace: string, key: string): Promise<void> {
181
333
  const client: ClientType | null = Redis.getClient();
@@ -10,6 +10,7 @@ import {
10
10
  DatabaseUsername,
11
11
  MaxPostgresConnections,
12
12
  PostgresConnectionAcquireTimeoutMs,
13
+ PostgresLockTimeoutMs,
13
14
  PostgresIdleInTransactionTimeoutMs,
14
15
  PostgresIdleSessionTimeoutMs,
15
16
  PostgresIdleTimeoutMs,
@@ -61,6 +62,21 @@ const dataSourceOptions: DataSourceOptions = {
61
62
  statement_timeout: PostgresStatementTimeoutMs,
62
63
  query_timeout: PostgresQueryTimeoutMs,
63
64
  idle_in_transaction_session_timeout: PostgresIdleInTransactionTimeoutMs,
65
+ /*
66
+ * Bound how long a statement WAITS for a lock, so contention on a hot row
67
+ * fails fast instead of forming a queue (see PostgresLockTimeoutMs).
68
+ *
69
+ * Excluded on the migration path, and that exclusion is load-bearing:
70
+ * App/Migrate.ts loads these same options and connects DIRECTLY to the
71
+ * backend (bypassing any pooler), where startup parameters really do take
72
+ * effect. A 3s lock_timeout there would abort ACCESS EXCLUSIVE DDL on any
73
+ * table with live traffic — only the two migrations that set their own
74
+ * `SET LOCAL lock_timeout` expect to fail that way; the rest must be free
75
+ * to wait.
76
+ */
77
+ ...(PostgresLockTimeoutMs > 0 && !RunDatabaseMigrationsOnBoot
78
+ ? { lock_timeout: PostgresLockTimeoutMs }
79
+ : {}),
64
80
  /*
65
81
  * Detect dead TCP peers (ungraceful client exit / network partition) so
66
82
  * orphaned server-side connections get torn down instead of lingering
@@ -0,0 +1,123 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddStatusPageMonitorRule1786005052769
4
+ implements MigrationInterface
5
+ {
6
+ public name = "AddStatusPageMonitorRule1786005052769";
7
+
8
+ public async up(queryRunner: QueryRunner): Promise<void> {
9
+ await queryRunner.query(
10
+ `CREATE TABLE "StatusPageMonitorRule" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "statusPageId" uuid NOT NULL, "name" character varying(100) NOT NULL, "description" character varying(500), "isEnabled" boolean NOT NULL DEFAULT true, "monitorNamePattern" character varying(500), "monitorDescriptionPattern" character varying(500), "statusPageGroupId" uuid, "showCurrentStatus" boolean NOT NULL DEFAULT true, "showUptimePercent" boolean NOT NULL DEFAULT true, "uptimePercentPrecision" character varying, "showStatusHistoryChart" boolean NOT NULL DEFAULT true, "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_f8d44792805d19b9a80921b6aa6" PRIMARY KEY ("_id"))`,
11
+ );
12
+ await queryRunner.query(
13
+ `CREATE INDEX "IDX_1dfd95ad32f7b687ef4d7ba591" ON "StatusPageMonitorRule" ("projectId") `,
14
+ );
15
+ await queryRunner.query(
16
+ `CREATE INDEX "IDX_473317deaa55a8305a047e4098" ON "StatusPageMonitorRule" ("statusPageId") `,
17
+ );
18
+ await queryRunner.query(
19
+ `CREATE INDEX "IDX_61196848685b7e7f5a6a54278b" ON "StatusPageMonitorRule" ("name") `,
20
+ );
21
+ await queryRunner.query(
22
+ `CREATE INDEX "IDX_71d1869039530178fd5f7e6faa" ON "StatusPageMonitorRule" ("isEnabled") `,
23
+ );
24
+ await queryRunner.query(
25
+ `CREATE INDEX "IDX_3e6d0bc4d444371aaa13a1c14c" ON "StatusPageMonitorRule" ("statusPageGroupId") `,
26
+ );
27
+ await queryRunner.query(
28
+ `CREATE TABLE "StatusPageMonitorRuleMonitorLabel" ("statusPageMonitorRuleId" uuid NOT NULL, "labelId" uuid NOT NULL, CONSTRAINT "PK_59234d33b435d8e771cd3728ea4" PRIMARY KEY ("statusPageMonitorRuleId", "labelId"))`,
29
+ );
30
+ await queryRunner.query(
31
+ `CREATE INDEX "IDX_0eb2b2a3601ea9813b8e162bd3" ON "StatusPageMonitorRuleMonitorLabel" ("statusPageMonitorRuleId") `,
32
+ );
33
+ await queryRunner.query(
34
+ `CREATE INDEX "IDX_bc08b888a870ecc9167516230a" ON "StatusPageMonitorRuleMonitorLabel" ("labelId") `,
35
+ );
36
+ await queryRunner.query(
37
+ `ALTER TABLE "StatusPageResource" ADD "statusPageMonitorRuleId" uuid`,
38
+ );
39
+ await queryRunner.query(
40
+ `CREATE INDEX "IDX_4e1ed45d9506da817dc7811601" ON "StatusPageResource" ("statusPageMonitorRuleId") `,
41
+ );
42
+ await queryRunner.query(
43
+ `ALTER TABLE "StatusPageMonitorRule" ADD CONSTRAINT "FK_1dfd95ad32f7b687ef4d7ba5910" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
44
+ );
45
+ await queryRunner.query(
46
+ `ALTER TABLE "StatusPageMonitorRule" ADD CONSTRAINT "FK_473317deaa55a8305a047e4098b" FOREIGN KEY ("statusPageId") REFERENCES "StatusPage"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
47
+ );
48
+ await queryRunner.query(
49
+ `ALTER TABLE "StatusPageMonitorRule" ADD CONSTRAINT "FK_3e6d0bc4d444371aaa13a1c14cc" FOREIGN KEY ("statusPageGroupId") REFERENCES "StatusPageGroup"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
50
+ );
51
+ await queryRunner.query(
52
+ `ALTER TABLE "StatusPageMonitorRule" ADD CONSTRAINT "FK_60d22031be1695cbd1405376cbf" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
53
+ );
54
+ await queryRunner.query(
55
+ `ALTER TABLE "StatusPageMonitorRule" ADD CONSTRAINT "FK_3416ec12899c9dde670d8dea290" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
56
+ );
57
+ await queryRunner.query(
58
+ `ALTER TABLE "StatusPageResource" ADD CONSTRAINT "FK_4e1ed45d9506da817dc78116010" FOREIGN KEY ("statusPageMonitorRuleId") REFERENCES "StatusPageMonitorRule"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
59
+ );
60
+ await queryRunner.query(
61
+ `ALTER TABLE "StatusPageMonitorRuleMonitorLabel" ADD CONSTRAINT "FK_0eb2b2a3601ea9813b8e162bd3b" FOREIGN KEY ("statusPageMonitorRuleId") REFERENCES "StatusPageMonitorRule"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
62
+ );
63
+ await queryRunner.query(
64
+ `ALTER TABLE "StatusPageMonitorRuleMonitorLabel" ADD CONSTRAINT "FK_bc08b888a870ecc9167516230ad" FOREIGN KEY ("labelId") REFERENCES "Label"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
65
+ );
66
+ }
67
+
68
+ public async down(queryRunner: QueryRunner): Promise<void> {
69
+ await queryRunner.query(
70
+ `ALTER TABLE "StatusPageMonitorRuleMonitorLabel" DROP CONSTRAINT "FK_bc08b888a870ecc9167516230ad"`,
71
+ );
72
+ await queryRunner.query(
73
+ `ALTER TABLE "StatusPageMonitorRuleMonitorLabel" DROP CONSTRAINT "FK_0eb2b2a3601ea9813b8e162bd3b"`,
74
+ );
75
+ await queryRunner.query(
76
+ `ALTER TABLE "StatusPageResource" DROP CONSTRAINT "FK_4e1ed45d9506da817dc78116010"`,
77
+ );
78
+ await queryRunner.query(
79
+ `ALTER TABLE "StatusPageMonitorRule" DROP CONSTRAINT "FK_3416ec12899c9dde670d8dea290"`,
80
+ );
81
+ await queryRunner.query(
82
+ `ALTER TABLE "StatusPageMonitorRule" DROP CONSTRAINT "FK_60d22031be1695cbd1405376cbf"`,
83
+ );
84
+ await queryRunner.query(
85
+ `ALTER TABLE "StatusPageMonitorRule" DROP CONSTRAINT "FK_3e6d0bc4d444371aaa13a1c14cc"`,
86
+ );
87
+ await queryRunner.query(
88
+ `ALTER TABLE "StatusPageMonitorRule" DROP CONSTRAINT "FK_473317deaa55a8305a047e4098b"`,
89
+ );
90
+ await queryRunner.query(
91
+ `ALTER TABLE "StatusPageMonitorRule" DROP CONSTRAINT "FK_1dfd95ad32f7b687ef4d7ba5910"`,
92
+ );
93
+ await queryRunner.query(
94
+ `DROP INDEX "public"."IDX_4e1ed45d9506da817dc7811601"`,
95
+ );
96
+ await queryRunner.query(
97
+ `ALTER TABLE "StatusPageResource" DROP COLUMN "statusPageMonitorRuleId"`,
98
+ );
99
+ await queryRunner.query(
100
+ `DROP INDEX "public"."IDX_bc08b888a870ecc9167516230a"`,
101
+ );
102
+ await queryRunner.query(
103
+ `DROP INDEX "public"."IDX_0eb2b2a3601ea9813b8e162bd3"`,
104
+ );
105
+ await queryRunner.query(`DROP TABLE "StatusPageMonitorRuleMonitorLabel"`);
106
+ await queryRunner.query(
107
+ `DROP INDEX "public"."IDX_3e6d0bc4d444371aaa13a1c14c"`,
108
+ );
109
+ await queryRunner.query(
110
+ `DROP INDEX "public"."IDX_71d1869039530178fd5f7e6faa"`,
111
+ );
112
+ await queryRunner.query(
113
+ `DROP INDEX "public"."IDX_61196848685b7e7f5a6a54278b"`,
114
+ );
115
+ await queryRunner.query(
116
+ `DROP INDEX "public"."IDX_473317deaa55a8305a047e4098"`,
117
+ );
118
+ await queryRunner.query(
119
+ `DROP INDEX "public"."IDX_1dfd95ad32f7b687ef4d7ba591"`,
120
+ );
121
+ await queryRunner.query(`DROP TABLE "StatusPageMonitorRule"`);
122
+ }
123
+ }
@@ -0,0 +1,21 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddPerUserPasswordSalt1786018109307 implements MigrationInterface {
4
+ public name = "AddPerUserPasswordSalt1786018109307";
5
+
6
+ public async up(queryRunner: QueryRunner): Promise<void> {
7
+ await queryRunner.query(
8
+ `ALTER TABLE "User" ADD "passwordSalt" character varying(100)`,
9
+ );
10
+ await queryRunner.query(
11
+ `ALTER TABLE "StatusPagePrivateUser" ADD "passwordSalt" character varying(100)`,
12
+ );
13
+ }
14
+
15
+ public async down(queryRunner: QueryRunner): Promise<void> {
16
+ await queryRunner.query(
17
+ `ALTER TABLE "StatusPagePrivateUser" DROP COLUMN "passwordSalt"`,
18
+ );
19
+ await queryRunner.query(`ALTER TABLE "User" DROP COLUMN "passwordSalt"`);
20
+ }
21
+ }