@oneuptime/common 13.0.8 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. package/Models/AnalyticsModels/AuditLog.ts +23 -15
  2. package/Models/DatabaseModels/GlobalConfig.ts +24 -0
  3. package/Models/DatabaseModels/Monitor.ts +3 -2
  4. package/Models/DatabaseModels/MonitorTemplate.ts +2 -1
  5. package/Scripts/generate-service-worker.js +65 -4
  6. package/Server/API/BaseAPI.ts +26 -5
  7. package/Server/API/GlobalConfigAPI.ts +228 -462
  8. package/Server/API/ProjectOIDC.ts +25 -16
  9. package/Server/API/ProjectSSO.ts +24 -16
  10. package/Server/API/StatusPageAPI.ts +100 -42
  11. package/Server/API/TeamMemberAPI.ts +10 -1
  12. package/Server/Enterprise/EnterpriseEdition.ts +649 -0
  13. package/Server/Enterprise/EnterpriseFeature.ts +48 -0
  14. package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +192 -0
  15. package/Server/Enterprise/EnterpriseServerModule.ts +233 -0
  16. package/Server/EnvironmentConfig.ts +147 -2
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.ts +25 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  19. package/Server/Middleware/UserAuthorization.ts +38 -16
  20. package/Server/Services/AuditLogService.ts +84 -1079
  21. package/Server/Services/EnterpriseLicenseService.ts +38 -0
  22. package/Server/Services/GlobalConfigService.ts +80 -1
  23. package/Server/Services/MonitorProbeService.ts +116 -20
  24. package/Server/Services/MonitorService.ts +56 -4
  25. package/Server/Services/MonitorStatusTimelineService.ts +284 -0
  26. package/Server/Services/MonitorTemplateService.ts +26 -0
  27. package/Server/Services/ProjectService.ts +284 -0
  28. package/Server/Services/RumSessionPinService.ts +25 -6
  29. package/Server/Services/StatusPagePrivateUserService.ts +7 -3
  30. package/Server/Services/StatusPageService.ts +78 -1
  31. package/Server/Services/TeamMemberService.ts +11 -0
  32. package/Server/Services/TeamService.ts +10 -0
  33. package/Server/Services/UserService.ts +10 -17
  34. package/Server/Types/Database/Permissions/CreatePermission.ts +15 -0
  35. package/Server/Types/Database/Permissions/EditionPermission.ts +208 -18
  36. package/Server/Types/Database/Permissions/TablePermission.ts +10 -3
  37. package/Server/Types/Database/Permissions/UpdatePermission.ts +29 -0
  38. package/Server/Types/Markdown.ts +41 -0
  39. package/Server/Utils/CommunityEditionSsoReport.ts +361 -0
  40. package/Server/Utils/EditionEnforcement.ts +233 -0
  41. package/Server/Utils/FrontendEnvironment.ts +41 -2
  42. package/Server/Utils/InstanceHealth/RedisHealth.ts +12 -277
  43. package/Server/Utils/Monitor/AffectedResourceList.ts +190 -0
  44. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +10 -1
  45. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +543 -333
  46. package/Server/Utils/Monitor/MonitoringIntervalValidator.ts +116 -0
  47. package/Server/Utils/Workspace/Slack/Slack.ts +423 -10
  48. package/Tests/App/Accounts/LoginPasskey.test.tsx +18 -0
  49. package/Tests/App/Accounts/LoginSsoEdition.test.tsx +616 -0
  50. package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +217 -0
  51. package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +357 -0
  52. package/Tests/App/AdminDashboard/EnterpriseLicensesShell.test.tsx +214 -0
  53. package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +271 -0
  54. package/Tests/App/AdminDashboard/HealthShells.test.tsx +659 -0
  55. package/Tests/App/Dashboard/AuditLogsAndComplianceShells.test.tsx +572 -0
  56. package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +547 -0
  57. package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +667 -0
  58. package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +319 -0
  59. package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +185 -0
  60. package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +45 -9
  61. package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +399 -0
  62. package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +214 -0
  63. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +1 -83
  64. package/Tests/App/Dashboard/SecurityEventsNavTabs.test.tsx +248 -0
  65. package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +1203 -0
  66. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +11 -91
  67. package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +319 -0
  68. package/Tests/Helpers/RefreshAwareApiScan.ts +757 -0
  69. package/Tests/Helpers/TenantScopedRequestScan.ts +515 -0
  70. package/Tests/Scripts/GenerateServiceWorker.test.ts +931 -0
  71. package/Tests/Server/API/BaseAPICreateItemDoesNotMutateModel.test.ts +130 -0
  72. package/Tests/Server/API/GlobalConfigLicense.test.ts +742 -497
  73. package/Tests/Server/API/OnCallReadinessAPI.test.ts +35 -909
  74. package/Tests/Server/API/SsoProviderListingsEdition.test.ts +432 -0
  75. package/Tests/Server/API/TeamMemberLeaveScimLockEdition.test.ts +252 -0
  76. package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +824 -0
  77. package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +643 -0
  78. package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +294 -0
  79. package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +93 -0
  80. package/Tests/Server/Enterprise/EnterpriseOnlyModulesLiveInEe.test.ts +582 -0
  81. package/Tests/Server/Enterprise/EnterpriseServerModule.test.ts +247 -0
  82. package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +502 -0
  83. package/Tests/Server/Enterprise/TestBillingFlag.ts +70 -0
  84. package/Tests/Server/EnvironmentConfigEnterpriseEdition.test.ts +314 -0
  85. package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +289 -0
  86. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +5 -1
  87. package/Tests/Server/Middleware/UserAuthorization.test.ts +37 -2
  88. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +516 -0
  89. package/Tests/Server/Services/AuditLogService.test.ts +460 -990
  90. package/Tests/Server/Services/CreateAndReadResponsesKeepIds.test.ts +263 -0
  91. package/Tests/Server/Services/EnterpriseLicenseServiceKeyGeneration.test.ts +151 -0
  92. package/Tests/Server/Services/GlobalConfigServiceLicenseColumns.test.ts +357 -0
  93. package/Tests/Server/Services/MonitorDestinationInfoIPv6.test.ts +101 -0
  94. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +333 -0
  95. package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +303 -0
  96. package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +701 -0
  97. package/Tests/Server/Services/ProjectServiceAuditLogSettingsCache.test.ts +31 -35
  98. package/Tests/Server/Services/ScimPushGroupsTeamLocksEdition.test.ts +431 -0
  99. package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +419 -0
  100. package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +769 -0
  101. package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +94 -0
  102. package/Tests/Server/Services/UserAuthenticationService.test.ts +35 -0
  103. package/Tests/Server/Services/UserServiceSeatLimit.test.ts +94 -56
  104. package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +1823 -0
  105. package/Tests/Server/Types/Markdown.test.ts +181 -0
  106. package/Tests/Server/Utils/CommunityEditionSsoReport.test.ts +565 -0
  107. package/Tests/Server/Utils/EditionEnforcement.test.ts +303 -0
  108. package/Tests/Server/Utils/FrontendEnvironment.test.ts +367 -0
  109. package/Tests/Server/Utils/InstanceHealth/RedisHealth.test.ts +38 -418
  110. package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +518 -0
  111. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +24 -4
  112. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +934 -96
  113. package/Tests/Server/Utils/Monitor/MonitoringIntervalValidator.test.ts +188 -0
  114. package/Tests/Server/Utils/Workspace/Slack/SlackMarkdownSectionSplit.test.ts +1209 -0
  115. package/Tests/Types/API/HostnameFromAuthority.test.ts +20 -10
  116. package/Tests/Types/API/HostnameIPv6.test.ts +149 -0
  117. package/Tests/Types/Database/CanonicalModelInstance.test.ts +190 -0
  118. package/Tests/UI/Components/EditionLabelLapseCopy.ts +104 -0
  119. package/Tests/UI/Components/EditionLabelLicenseManagerWiring.test.tsx +254 -0
  120. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +84 -178
  121. package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +1586 -0
  122. package/Tests/UI/Components/EditionLabelLicenseWritesGuard.test.tsx +157 -0
  123. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +49 -2
  124. package/Tests/UI/Components/MarkdownConverters.test.ts +684 -0
  125. package/Tests/UI/Components/ModelListEmptyState.test.tsx +289 -0
  126. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerEmptyContent.test.tsx +131 -0
  127. package/Tests/UI/EsbuildEnterpriseAlias.test.ts +1127 -0
  128. package/Tests/Utils/HostAddressUtil.test.ts +138 -0
  129. package/Tests/Utils/Monitor/MonitorDestinationUtil.test.ts +313 -0
  130. package/Tests/Utils/Monitor/MonitoringIntervalUtil.test.ts +304 -0
  131. package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +248 -0
  132. package/Types/API/Hostname.ts +63 -16
  133. package/Types/Database/AccessControl/ColumnAccessControl.ts +19 -7
  134. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +19 -7
  135. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +15 -6
  136. package/Types/Database/CanonicalModelInstance.ts +102 -0
  137. package/Types/Database/TableColumn.ts +20 -6
  138. package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +29 -0
  139. package/Types/IP/IPv6.ts +6 -1
  140. package/Types/Monitor/MonitorStep.ts +6 -1
  141. package/Types/Monitor/ProxmoxAlertTemplates.ts +1 -1
  142. package/Types/StatusPage/UptimeDailyAggregate.ts +110 -0
  143. package/UI/Components/EditionLabel/EditionLabel.tsx +796 -1195
  144. package/UI/Components/EditionLabel/LicenseManager.ts +119 -0
  145. package/UI/Components/Graphs/DayUptimeGraph.tsx +123 -15
  146. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  147. package/UI/Components/ModelList/ModelList.tsx +11 -1
  148. package/UI/Components/MonitorGraphs/Uptime.tsx +69 -0
  149. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +28 -12
  150. package/UI/esbuild-enterprise.js +263 -0
  151. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +7 -2
  152. package/Utils/HostAddressUtil.ts +94 -0
  153. package/Utils/Monitor/MonitorDestinationUtil.ts +157 -0
  154. package/Utils/Monitor/MonitoringIntervalUtil.ts +436 -0
  155. package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +192 -0
  156. package/build/dist/Models/AnalyticsModels/AuditLog.js +23 -15
  157. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
  158. package/build/dist/Models/DatabaseModels/GlobalConfig.js +25 -0
  159. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  160. package/build/dist/Models/DatabaseModels/Monitor.js +2 -2
  161. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  162. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +2 -1
  163. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  164. package/build/dist/Server/API/BaseAPI.js +26 -5
  165. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  166. package/build/dist/Server/API/GlobalConfigAPI.js +148 -344
  167. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  168. package/build/dist/Server/API/ProjectOIDC.js +24 -16
  169. package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
  170. package/build/dist/Server/API/ProjectSSO.js +24 -16
  171. package/build/dist/Server/API/ProjectSSO.js.map +1 -1
  172. package/build/dist/Server/API/StatusPageAPI.js +93 -43
  173. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  174. package/build/dist/Server/API/TeamMemberAPI.js +8 -1
  175. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  176. package/build/dist/Server/Enterprise/EnterpriseEdition.js +466 -0
  177. package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -0
  178. package/build/dist/Server/Enterprise/EnterpriseFeature.js +39 -0
  179. package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -0
  180. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +70 -0
  181. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js.map +1 -0
  182. package/build/dist/Server/Enterprise/EnterpriseServerModule.js +84 -0
  183. package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -0
  184. package/build/dist/Server/EnvironmentConfig.js +84 -2
  185. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  186. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js +18 -0
  187. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js.map +1 -0
  188. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  189. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  190. package/build/dist/Server/Middleware/UserAuthorization.js +30 -11
  191. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  192. package/build/dist/Server/Services/AuditLogService.js +71 -740
  193. package/build/dist/Server/Services/AuditLogService.js.map +1 -1
  194. package/build/dist/Server/Services/EnterpriseLicenseService.js +34 -0
  195. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  196. package/build/dist/Server/Services/GlobalConfigService.js +63 -0
  197. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  198. package/build/dist/Server/Services/MonitorProbeService.js +90 -17
  199. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  200. package/build/dist/Server/Services/MonitorService.js +45 -4
  201. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  202. package/build/dist/Server/Services/MonitorStatusTimelineService.js +242 -0
  203. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  204. package/build/dist/Server/Services/MonitorTemplateService.js +16 -0
  205. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  206. package/build/dist/Server/Services/ProjectService.js +214 -0
  207. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  208. package/build/dist/Server/Services/RumSessionPinService.js +24 -6
  209. package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
  210. package/build/dist/Server/Services/StatusPagePrivateUserService.js +7 -3
  211. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  212. package/build/dist/Server/Services/StatusPageService.js +71 -0
  213. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  214. package/build/dist/Server/Services/TeamMemberService.js +10 -0
  215. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  216. package/build/dist/Server/Services/TeamService.js +9 -0
  217. package/build/dist/Server/Services/TeamService.js.map +1 -1
  218. package/build/dist/Server/Services/UserService.js +10 -16
  219. package/build/dist/Server/Services/UserService.js.map +1 -1
  220. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +10 -0
  221. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  222. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +154 -14
  223. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
  224. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +10 -3
  225. package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
  226. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +17 -0
  227. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
  228. package/build/dist/Server/Types/Markdown.js +31 -0
  229. package/build/dist/Server/Types/Markdown.js.map +1 -1
  230. package/build/dist/Server/Utils/CommunityEditionSsoReport.js +223 -0
  231. package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +1 -0
  232. package/build/dist/Server/Utils/EditionEnforcement.js +187 -0
  233. package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -0
  234. package/build/dist/Server/Utils/FrontendEnvironment.js +31 -2
  235. package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -1
  236. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js +6 -193
  237. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js.map +1 -1
  238. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +96 -0
  239. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -0
  240. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
  241. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  242. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +449 -261
  243. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  244. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js +88 -0
  245. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js.map +1 -0
  246. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +331 -11
  247. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  248. package/build/dist/Types/API/Hostname.js +59 -11
  249. package/build/dist/Types/API/Hostname.js.map +1 -1
  250. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +19 -7
  251. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  252. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +19 -7
  253. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  254. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +13 -6
  255. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -1
  256. package/build/dist/Types/Database/CanonicalModelInstance.js +88 -0
  257. package/build/dist/Types/Database/CanonicalModelInstance.js.map +1 -0
  258. package/build/dist/Types/Database/TableColumn.js +19 -6
  259. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  260. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +28 -0
  261. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js.map +1 -0
  262. package/build/dist/Types/IP/IPv6.js +6 -1
  263. package/build/dist/Types/IP/IPv6.js.map +1 -1
  264. package/build/dist/Types/Monitor/MonitorStep.js +5 -0
  265. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  266. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +1 -1
  267. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js +32 -0
  268. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js.map +1 -0
  269. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +414 -585
  270. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  271. package/build/dist/UI/Components/EditionLabel/LicenseManager.js +2 -0
  272. package/build/dist/UI/Components/EditionLabel/LicenseManager.js.map +1 -0
  273. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +90 -17
  274. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  275. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  276. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  277. package/build/dist/UI/Components/ModelList/ModelList.js +4 -1
  278. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  279. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +42 -1
  280. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  281. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +2 -2
  282. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  283. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +6 -2
  284. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
  285. package/build/dist/Utils/HostAddressUtil.js +80 -0
  286. package/build/dist/Utils/HostAddressUtil.js.map +1 -0
  287. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js +127 -0
  288. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js.map +1 -0
  289. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js +374 -0
  290. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js.map +1 -0
  291. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
  292. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -0
  293. package/jest.config.json +3 -1
  294. package/package.json +1 -1
  295. package/test-setup.sh +103 -34
  296. package/tsconfig.json +13 -0
  297. package/Server/API/EnterpriseLicenseAPI.ts +0 -925
  298. package/Server/API/TeamComplianceAPI.ts +0 -158
  299. package/Server/Middleware/SCIMAuthorization.ts +0 -141
  300. package/Server/Services/TeamComplianceService.ts +0 -733
  301. package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +0 -186
  302. package/Server/Utils/InstanceHealth/PostgresHealth.ts +0 -286
  303. package/Tests/App/AdminDashboard/EnterpriseLicenseInstanceActivity.test.tsx +0 -451
  304. package/Tests/App/Dashboard/AuditLogsTable.test.tsx +0 -598
  305. package/Tests/App/Dashboard/SecurityEventsExplorer.test.tsx +0 -761
  306. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +0 -247
  307. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +0 -382
  308. package/Tests/App/Dashboard/SecurityEventsVolumeChart.test.tsx +0 -321
  309. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +0 -1851
  310. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +0 -525
  311. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +0 -1845
  312. package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +0 -495
  313. package/Tests/Server/Utils/InstanceHealth/PostgresHealth.test.ts +0 -337
  314. package/Tests/Utils/EnterpriseLicenseSeats.test.ts +0 -576
  315. package/Tests/Utils/EnterpriseLicenseSync.test.ts +0 -568
  316. package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +0 -287
  317. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +0 -244
  318. package/build/dist/Server/API/EnterpriseLicenseAPI.js +0 -646
  319. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +0 -1
  320. package/build/dist/Server/API/TeamComplianceAPI.js +0 -111
  321. package/build/dist/Server/API/TeamComplianceAPI.js.map +0 -1
  322. package/build/dist/Server/Middleware/SCIMAuthorization.js +0 -124
  323. package/build/dist/Server/Middleware/SCIMAuthorization.js.map +0 -1
  324. package/build/dist/Server/Services/TeamComplianceService.js +0 -537
  325. package/build/dist/Server/Services/TeamComplianceService.js.map +0 -1
  326. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +0 -145
  327. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +0 -1
  328. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js +0 -194
  329. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js.map +0 -1
  330. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +0 -168
  331. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +0 -1
  332. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +0 -174
  333. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +0 -1
@@ -0,0 +1,333 @@
1
+ import { Service as MonitorProbeServiceType } from "../../../Server/Services/MonitorProbeService";
2
+ import ObjectID from "../../../Types/ObjectID";
3
+ import logger from "../../../Server/Utils/Logger";
4
+ import {
5
+ CapturedStatement,
6
+ captureClaimStatements,
7
+ expectPlaceholdersMatchParameters,
8
+ } from "../TestingUtils/PastDueMonitoringClaimSql";
9
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
10
+
11
+ /*
12
+ * WHAT THIS FILE IS DEFENDING
13
+ *
14
+ * A customer reported that monitoring had "completely stopped" for three
15
+ * days. It had not — every monitor was being checked the whole time. What
16
+ * had actually happened is subtler and worse: all 19 of their monitors
17
+ * stored the monitoringInterval "5m", which is not a cron expression.
18
+ * cron-parser threw on it, and claimMonitorProbesForProbing caught the throw
19
+ * with a bare `catch {}` and fell back to "one minute from now".
20
+ *
21
+ * So a monitor configured for five minutes was probed every ~75 seconds.
22
+ * Measured in production: 1,152 checks per probe per day against the 288 the
23
+ * setting implies — four times the requested rate, with nothing logged
24
+ * anywhere, for as long as the row existed. 103 monitors across 10 projects
25
+ * were affected, most of them having copied the value out of OUR OWN
26
+ * Terraform examples.
27
+ *
28
+ * These tests drive the real claim query through the same harness the
29
+ * past-due suite uses, and assert the SCHEDULE it computes — because the
30
+ * bug was never in whether monitors were claimed, only in how far apart.
31
+ */
32
+
33
+ const PROBE_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
34
+
35
+ const MONITOR_PROBE_ID: string = "11111111-1111-4111-8111-111111111111";
36
+
37
+ /*
38
+ * Drive one claim and hand back the nextPingAt the UPDATE would write.
39
+ *
40
+ * The UPDATE binds [currentDate, ids, ...nextPingDates], so the first
41
+ * monitor's computed schedule is parameter index 2.
42
+ */
43
+ async function claimAndReadNextPingAt(
44
+ monitoringInterval: string | null,
45
+ ): Promise<{ nextPingAt: Date; now: Date; update: CapturedStatement }> {
46
+ const service: MonitorProbeServiceType = new MonitorProbeServiceType();
47
+
48
+ const statements: Array<CapturedStatement> = captureClaimStatements(service, [
49
+ { _id: MONITOR_PROBE_ID, monitoringInterval },
50
+ ]);
51
+
52
+ const before: Date = new Date();
53
+
54
+ await service.claimMonitorProbesForProbing({
55
+ probeId: PROBE_ID,
56
+ limit: 25,
57
+ });
58
+
59
+ expect(statements.length).toBeGreaterThanOrEqual(2);
60
+
61
+ const update: CapturedStatement = statements[1]!;
62
+ const nextPingAt: Date = update.parameters[2] as Date;
63
+
64
+ expect(nextPingAt).toBeInstanceOf(Date);
65
+
66
+ return { nextPingAt, now: before, update };
67
+ }
68
+
69
+ function minutesBetween(from: Date, to: Date): number {
70
+ return (to.getTime() - from.getTime()) / 1000 / 60;
71
+ }
72
+
73
+ describe("claimMonitorProbesForProbing computes the schedule the customer asked for", () => {
74
+ afterEach(() => {
75
+ jest.restoreAllMocks();
76
+ });
77
+
78
+ describe("the production scenario", () => {
79
+ test('a monitor storing "5m" is scheduled ~5 minutes out, not 1', async () => {
80
+ const { nextPingAt, now } = await claimAndReadNextPingAt("5m");
81
+
82
+ const gap: number = minutesBetween(now, nextPingAt);
83
+
84
+ /*
85
+ * "*_/5 * * * *" fires on the next multiple of five, so the gap from
86
+ * an arbitrary "now" is anywhere in (0, 5]. The assertion that matters
87
+ * is that it is NOT the old hard-coded one minute: before the fix this
88
+ * was always ~1.0, and a 5-minute cron can only land within one minute
89
+ * of now for one fifth of any given five-minute window.
90
+ */
91
+ expect(gap).toBeGreaterThan(0);
92
+ expect(gap).toBeLessThanOrEqual(5);
93
+
94
+ // The schedule must land on a 5-minute boundary, which +1 minute never does.
95
+ expect(nextPingAt.getSeconds()).toBe(0);
96
+ expect(nextPingAt.getMinutes() % 5).toBe(0);
97
+ });
98
+
99
+ test.each([
100
+ ["Every 5 minutes", 5],
101
+ ["Every 5 Minutes", 5],
102
+ ["Every Five Minutes", 5],
103
+ ["5 minutes", 5],
104
+ ["Every 2 minutes", 2],
105
+ ["every-1-min", 1],
106
+ ["1 minute", 1],
107
+ ])(
108
+ "a monitor storing %j lands on a %i-minute boundary",
109
+ async (interval: string, everyMinutes: number) => {
110
+ const { nextPingAt } = await claimAndReadNextPingAt(interval);
111
+
112
+ expect(nextPingAt.getSeconds()).toBe(0);
113
+ expect(nextPingAt.getMinutes() % everyMinutes).toBe(0);
114
+ },
115
+ );
116
+
117
+ test('a monitor storing "1 day" is scheduled for midnight, not a minute away', async () => {
118
+ const { nextPingAt, now } = await claimAndReadNextPingAt("1 day");
119
+
120
+ expect(nextPingAt.getHours()).toBe(0);
121
+ expect(nextPingAt.getMinutes()).toBe(0);
122
+ expect(minutesBetween(now, nextPingAt)).toBeGreaterThan(1);
123
+ });
124
+ });
125
+
126
+ describe("values that were always valid are unaffected", () => {
127
+ test.each([
128
+ ["* * * * *", 1],
129
+ ["*/2 * * * *", 2],
130
+ ["*/5 * * * *", 5],
131
+ ["*/10 * * * *", 10],
132
+ ["*/15 * * * *", 15],
133
+ ["*/30 * * * *", 30],
134
+ ])(
135
+ "%j still lands on a %i-minute boundary",
136
+ async (cron: string, everyMinutes: number) => {
137
+ const { nextPingAt } = await claimAndReadNextPingAt(cron);
138
+
139
+ expect(nextPingAt.getSeconds()).toBe(0);
140
+ expect(nextPingAt.getMinutes() % everyMinutes).toBe(0);
141
+ },
142
+ );
143
+
144
+ test("a cron-parser shortcut the isomorphic grammar cannot read is still honoured", async () => {
145
+ /*
146
+ * Raw-parse-first is load-bearing: "@hourly" is Unrecognized to the
147
+ * normalizer but perfectly readable to cron-parser. If the order were
148
+ * reversed this would silently become a 1-minute fallback.
149
+ */
150
+ const { nextPingAt } = await claimAndReadNextPingAt("@hourly");
151
+
152
+ expect(nextPingAt.getMinutes()).toBe(0);
153
+ expect(nextPingAt.getSeconds()).toBe(0);
154
+ });
155
+ });
156
+
157
+ describe("values with no schedule fall back, quietly and correctly", () => {
158
+ test.each([null, ""])(
159
+ "%j falls back to the 1-minute default without logging an error",
160
+ async (interval: string | null) => {
161
+ const { nextPingAt, now } = await claimAndReadNextPingAt(interval);
162
+
163
+ /*
164
+ * A Manual monitor has no interval and that is not a fault, so this
165
+ * must not reach the offender log.
166
+ */
167
+ expect(minutesBetween(now, nextPingAt)).toBeGreaterThan(0.9);
168
+ expect(minutesBetween(now, nextPingAt)).toBeLessThan(1.2);
169
+ },
170
+ );
171
+
172
+ test("a genuinely unreadable value falls back to 1 minute", async () => {
173
+ const { nextPingAt, now } = await claimAndReadNextPingAt("banana");
174
+
175
+ expect(minutesBetween(now, nextPingAt)).toBeGreaterThan(0.9);
176
+ expect(minutesBetween(now, nextPingAt)).toBeLessThan(1.2);
177
+ });
178
+ });
179
+
180
+ describe("the failure is no longer silent", () => {
181
+ test("an unreadable interval is reported once per batch, naming the monitor", async () => {
182
+ /*
183
+ * The original bug was invisible: a bare `catch {}` with no logging,
184
+ * so nothing anywhere could be grepped for. The regression guard is
185
+ * that SOMETHING is logged, that it names the offending row, and that
186
+ * it is one line for the batch rather than one per monitor.
187
+ */
188
+ const errorSpy: ReturnType<typeof jest.spyOn> = jest
189
+ .spyOn(logger, "error")
190
+ .mockImplementation(() => {});
191
+
192
+ const service: MonitorProbeServiceType = new MonitorProbeServiceType();
193
+
194
+ captureClaimStatements(service, [
195
+ { _id: MONITOR_PROBE_ID, monitoringInterval: "banana" },
196
+ {
197
+ _id: "22222222-2222-4222-8222-222222222222",
198
+ monitoringInterval: "asap",
199
+ },
200
+ // A valid one, which must NOT appear in the report.
201
+ {
202
+ _id: "44444444-4444-4444-8444-444444444444",
203
+ monitoringInterval: "*/5 * * * *",
204
+ },
205
+ ]);
206
+
207
+ await service.claimMonitorProbesForProbing({
208
+ probeId: PROBE_ID,
209
+ limit: 25,
210
+ });
211
+
212
+ const offenderLogs: Array<string> = errorSpy.mock.calls
213
+ .map((call: Array<unknown>) => {
214
+ return String(call[0]);
215
+ })
216
+ .filter((message: string) => {
217
+ return message.includes("claimMonitorProbesForProbing");
218
+ });
219
+
220
+ // One aggregated line, not one per offending row.
221
+ expect(offenderLogs).toHaveLength(1);
222
+
223
+ const message: string = offenderLogs[0]!;
224
+
225
+ expect(message).toContain("2 monitor(s)");
226
+ expect(message).toContain(MONITOR_PROBE_ID);
227
+ expect(message).toContain("banana");
228
+ // The valid row is not an offender.
229
+ expect(message).not.toContain("44444444-4444-4444-8444-444444444444");
230
+ });
231
+
232
+ test("a clean batch logs nothing", async () => {
233
+ const errorSpy: ReturnType<typeof jest.spyOn> = jest
234
+ .spyOn(logger, "error")
235
+ .mockImplementation(() => {});
236
+
237
+ const service: MonitorProbeServiceType = new MonitorProbeServiceType();
238
+
239
+ captureClaimStatements(service, [
240
+ { _id: MONITOR_PROBE_ID, monitoringInterval: "*/5 * * * *" },
241
+ // "5m" is understood now, so it is not an offender either.
242
+ {
243
+ _id: "22222222-2222-4222-8222-222222222222",
244
+ monitoringInterval: "5m",
245
+ },
246
+ ]);
247
+
248
+ await service.claimMonitorProbesForProbing({
249
+ probeId: PROBE_ID,
250
+ limit: 25,
251
+ });
252
+
253
+ const offenderLogs: Array<unknown> = errorSpy.mock.calls.filter(
254
+ (call: Array<unknown>) => {
255
+ return String(call[0]).includes("claimMonitorProbesForProbing");
256
+ },
257
+ );
258
+
259
+ expect(offenderLogs).toHaveLength(0);
260
+ });
261
+ });
262
+
263
+ describe("the SQL contract is unchanged", () => {
264
+ test("placeholders still match parameters after the interval change", async () => {
265
+ /*
266
+ * The nextPingAt CASE fragments are built with $${i + 3} numbering
267
+ * against a [currentDate, ids, ...dates] parameter list. Touching the
268
+ * loop that builds them is exactly how that numbering gets broken.
269
+ */
270
+ const service: MonitorProbeServiceType = new MonitorProbeServiceType();
271
+
272
+ const statements: Array<CapturedStatement> = captureClaimStatements(
273
+ service,
274
+ [
275
+ { _id: MONITOR_PROBE_ID, monitoringInterval: "5m" },
276
+ {
277
+ _id: "22222222-2222-4222-8222-222222222222",
278
+ monitoringInterval: "banana",
279
+ },
280
+ {
281
+ _id: "44444444-4444-4444-8444-444444444444",
282
+ monitoringInterval: null,
283
+ },
284
+ ],
285
+ );
286
+
287
+ await service.claimMonitorProbesForProbing({
288
+ probeId: PROBE_ID,
289
+ limit: 25,
290
+ });
291
+
292
+ for (const statement of statements) {
293
+ expectPlaceholdersMatchParameters(statement);
294
+ }
295
+ });
296
+
297
+ test("every claimed row still gets a schedule, whatever its interval says", async () => {
298
+ const service: MonitorProbeServiceType = new MonitorProbeServiceType();
299
+
300
+ const statements: Array<CapturedStatement> = captureClaimStatements(
301
+ service,
302
+ [
303
+ { _id: MONITOR_PROBE_ID, monitoringInterval: "5m" },
304
+ {
305
+ _id: "22222222-2222-4222-8222-222222222222",
306
+ monitoringInterval: "banana",
307
+ },
308
+ {
309
+ _id: "44444444-4444-4444-8444-444444444444",
310
+ monitoringInterval: null,
311
+ },
312
+ ],
313
+ );
314
+
315
+ const claimed: Array<ObjectID> =
316
+ await service.claimMonitorProbesForProbing({
317
+ probeId: PROBE_ID,
318
+ limit: 25,
319
+ });
320
+
321
+ expect(claimed).toHaveLength(3);
322
+
323
+ const update: CapturedStatement = statements[1]!;
324
+
325
+ // [currentDate, ids, ...3 dates]
326
+ expect(update.parameters).toHaveLength(5);
327
+
328
+ for (const date of update.parameters.slice(2)) {
329
+ expect(date).toBeInstanceOf(Date);
330
+ }
331
+ });
332
+ });
333
+ });
@@ -0,0 +1,303 @@
1
+ import { Service as MonitorStatusTimelineServiceType } from "../../../Server/Services/MonitorStatusTimelineService";
2
+ import ObjectID from "../../../Types/ObjectID";
3
+ import {
4
+ MonitorUptimeDailyAggregate,
5
+ UptimeDailyAggregate,
6
+ UptimeDayBucket,
7
+ } from "../../../Types/StatusPage/UptimeDailyAggregate";
8
+ import { describe, expect, test } from "@jest/globals";
9
+
10
+ /*
11
+ * WHAT THIS FILE IS DEFENDING
12
+ *
13
+ * The status-page uptime bars used to be painted from raw MonitorStatusTimeline
14
+ * rows fetched under a single `limit: LIMIT_MAX` (10,000) across EVERY monitor
15
+ * on the page, sorted `startsAt DESC`.
16
+ *
17
+ * Measured on a real status page: 254,550 rows matched the 60-day window and
18
+ * 10,000 came back - 3.9%. The oldest surviving row started four days before
19
+ * the request, so 56 of 60 bars were painted from data that had been dropped,
20
+ * and the uptime percentage was computed from the same 3.9%.
21
+ *
22
+ * The cap is GLOBAL across monitors and the sort is newest-first, so a quiet
23
+ * monitor's covering row - which has an OLD startsAt - sorts last and is cut
24
+ * first. On the reporting page, three flapping monitors held 9,995 of the
25
+ * 10,000 slots. The healthier a monitor is, the more likely it loses its whole
26
+ * history to a noisy neighbour.
27
+ *
28
+ * These tests cover the SHAPING half (no database): turning the SQL's
29
+ * (monitor, day, status) rows into buckets, and in particular keeping a
30
+ * no-coverage day distinguishable from a day spent up.
31
+ */
32
+
33
+ const MONITOR_A: string = "11111111-1111-4111-8111-111111111111";
34
+ const MONITOR_B: string = "22222222-2222-4222-8222-222222222222";
35
+ const STATUS_UP: string = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
36
+ const STATUS_DOWN: string = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
37
+
38
+ type Row = {
39
+ monitorId: string;
40
+ bucketStart: Date;
41
+ bucketEnd: Date;
42
+ daySeconds: string | number;
43
+ monitorStatusId: string | null;
44
+ seconds: string | number;
45
+ };
46
+
47
+ function row(
48
+ monitorId: string,
49
+ day: string,
50
+ statusId: string | null,
51
+ seconds: number,
52
+ daySeconds: number = 86400,
53
+ ): Row {
54
+ const start: Date = new Date(`${day}T00:00:00.000Z`);
55
+ const end: Date = new Date(start.getTime() + daySeconds * 1000);
56
+
57
+ return {
58
+ monitorId,
59
+ bucketStart: start,
60
+ bucketEnd: end,
61
+ /* The pg driver returns bigint columns as strings. */
62
+ daySeconds: String(daySeconds),
63
+ monitorStatusId: statusId,
64
+ seconds: String(seconds),
65
+ };
66
+ }
67
+
68
+ function aggregate(rows: Array<Row>): UptimeDailyAggregate {
69
+ return MonitorStatusTimelineServiceType.toUptimeDailyAggregate(rows);
70
+ }
71
+
72
+ function bucketsFor(
73
+ result: UptimeDailyAggregate,
74
+ monitorId: string,
75
+ ): Array<UptimeDayBucket> {
76
+ const monitor: MonitorUptimeDailyAggregate | undefined = result.monitors.find(
77
+ (m: MonitorUptimeDailyAggregate) => {
78
+ return m.monitorId.toString() === monitorId;
79
+ },
80
+ );
81
+
82
+ return monitor ? monitor.buckets : [];
83
+ }
84
+
85
+ describe("MonitorStatusTimelineService.toUptimeDailyAggregate", () => {
86
+ describe("a day with no coverage is not a day spent up", () => {
87
+ test("a NULL-status row yields a bucket with zero coverage and no durations", () => {
88
+ /*
89
+ * This is the LEFT JOIN's "nothing overlapped this day" row. It is the
90
+ * single most important case in this file: before the fix such a day
91
+ * fell through to the status page's defaultBarColor, which is GREEN on
92
+ * 4,642 status pages - the page silently asserting uptime for a period
93
+ * it has no data for.
94
+ */
95
+ const result: UptimeDailyAggregate = aggregate([
96
+ row(MONITOR_A, "2026-07-01", null, 0),
97
+ ]);
98
+
99
+ const buckets: Array<UptimeDayBucket> = bucketsFor(result, MONITOR_A);
100
+
101
+ expect(buckets).toHaveLength(1);
102
+ expect(buckets[0]!.coveredSeconds).toBe(0);
103
+ expect(buckets[0]!.statusDurations).toHaveLength(0);
104
+ expect(buckets[0]!.daySeconds).toBe(86400);
105
+ });
106
+
107
+ test("a zero-second status row does not manufacture coverage", () => {
108
+ const result: UptimeDailyAggregate = aggregate([
109
+ row(MONITOR_A, "2026-07-01", STATUS_UP, 0),
110
+ ]);
111
+
112
+ expect(bucketsFor(result, MONITOR_A)[0]!.coveredSeconds).toBe(0);
113
+ expect(bucketsFor(result, MONITOR_A)[0]!.statusDurations).toHaveLength(0);
114
+ });
115
+
116
+ test("a fully covered day is distinguishable from a no-data day", () => {
117
+ const result: UptimeDailyAggregate = aggregate([
118
+ row(MONITOR_A, "2026-07-01", null, 0),
119
+ row(MONITOR_A, "2026-07-02", STATUS_UP, 86400),
120
+ ]);
121
+
122
+ const buckets: Array<UptimeDayBucket> = bucketsFor(result, MONITOR_A);
123
+
124
+ expect(buckets[0]!.coveredSeconds).toBe(0);
125
+ expect(buckets[1]!.coveredSeconds).toBe(86400);
126
+ });
127
+ });
128
+
129
+ describe("partial coverage is a real reading, not a no-data day", () => {
130
+ test("the day a monitor was created reports what was measured", () => {
131
+ /*
132
+ * A monitor created at noon has half a day of data. That is a genuine
133
+ * reading of a shorter interval and must render normally - not as
134
+ * no-data, and not penalised for the hours before it existed.
135
+ */
136
+ const result: UptimeDailyAggregate = aggregate([
137
+ row(MONITOR_A, "2026-07-01", STATUS_UP, 43200),
138
+ ]);
139
+
140
+ const bucket: UptimeDayBucket = bucketsFor(result, MONITOR_A)[0]!;
141
+
142
+ expect(bucket.coveredSeconds).toBe(43200);
143
+ expect(bucket.daySeconds).toBe(86400);
144
+ expect(bucket.coveredSeconds).toBeLessThan(bucket.daySeconds);
145
+ expect(bucket.statusDurations).toHaveLength(1);
146
+ });
147
+ });
148
+
149
+ describe("multiple statuses in one day", () => {
150
+ test("durations are kept separately and summed into coverage", () => {
151
+ const result: UptimeDailyAggregate = aggregate([
152
+ row(MONITOR_A, "2026-07-01", STATUS_UP, 82800),
153
+ row(MONITOR_A, "2026-07-01", STATUS_DOWN, 3600),
154
+ ]);
155
+
156
+ const buckets: Array<UptimeDayBucket> = bucketsFor(result, MONITOR_A);
157
+
158
+ expect(buckets).toHaveLength(1);
159
+ expect(buckets[0]!.coveredSeconds).toBe(86400);
160
+ expect(buckets[0]!.statusDurations).toHaveLength(2);
161
+
162
+ const down: number =
163
+ buckets[0]!.statusDurations.find((d: { monitorStatusId: ObjectID }) => {
164
+ return d.monitorStatusId.toString() === STATUS_DOWN;
165
+ })?.seconds || 0;
166
+
167
+ expect(down).toBe(3600);
168
+ });
169
+ });
170
+
171
+ describe("day length is never assumed to be 86400", () => {
172
+ test("a clipped first bucket keeps its real length", () => {
173
+ /*
174
+ * The window's first and last buckets are clipped to the window, and a
175
+ * local day crossing a DST boundary is genuinely 23 or 25 hours. Any
176
+ * consumer dividing by a hardcoded 86400 would misreport both.
177
+ */
178
+ const result: UptimeDailyAggregate = aggregate([
179
+ row(MONITOR_A, "2026-07-01", STATUS_UP, 45147, 45147),
180
+ ]);
181
+
182
+ expect(bucketsFor(result, MONITOR_A)[0]!.daySeconds).toBe(45147);
183
+ });
184
+
185
+ test("a 23-hour DST day is reported as 82800 seconds", () => {
186
+ const result: UptimeDailyAggregate = aggregate([
187
+ row(MONITOR_A, "2026-03-29", STATUS_UP, 82800, 82800),
188
+ ]);
189
+
190
+ expect(bucketsFor(result, MONITOR_A)[0]!.daySeconds).toBe(82800);
191
+ });
192
+
193
+ test("a 25-hour DST day is reported as 90000 seconds", () => {
194
+ const result: UptimeDailyAggregate = aggregate([
195
+ row(MONITOR_A, "2026-10-25", STATUS_UP, 90000, 90000),
196
+ ]);
197
+
198
+ expect(bucketsFor(result, MONITOR_A)[0]!.daySeconds).toBe(90000);
199
+ });
200
+ });
201
+
202
+ describe("monitors are kept apart", () => {
203
+ test("a noisy monitor cannot consume a quiet one's buckets", () => {
204
+ /*
205
+ * The shape of the original bug: a global cap let three flapping
206
+ * monitors hold 9,995 of 10,000 slots and starve the quiet ones. The
207
+ * aggregate is per monitor, so a quiet monitor's days survive
208
+ * regardless of what its neighbours are doing.
209
+ */
210
+ const rows: Array<Row> = [];
211
+
212
+ for (let i: number = 0; i < 500; i++) {
213
+ rows.push(row(MONITOR_A, "2026-07-01", STATUS_UP, 1));
214
+ rows.push(row(MONITOR_A, "2026-07-01", STATUS_DOWN, 1));
215
+ }
216
+
217
+ rows.push(row(MONITOR_B, "2026-07-01", STATUS_UP, 86400));
218
+
219
+ const result: UptimeDailyAggregate = aggregate(rows);
220
+
221
+ expect(bucketsFor(result, MONITOR_B)).toHaveLength(1);
222
+ expect(bucketsFor(result, MONITOR_B)[0]!.coveredSeconds).toBe(86400);
223
+ });
224
+ });
225
+
226
+ describe("output shape", () => {
227
+ test("buckets come back in chronological order whatever order the rows arrive in", () => {
228
+ const result: UptimeDailyAggregate = aggregate([
229
+ row(MONITOR_A, "2026-07-03", STATUS_UP, 86400),
230
+ row(MONITOR_A, "2026-07-01", STATUS_UP, 86400),
231
+ row(MONITOR_A, "2026-07-02", STATUS_UP, 86400),
232
+ ]);
233
+
234
+ const starts: Array<number> = bucketsFor(result, MONITOR_A).map(
235
+ (b: UptimeDayBucket) => {
236
+ return b.bucketStart.getTime();
237
+ },
238
+ );
239
+
240
+ expect(starts).toEqual([...starts].sort());
241
+ });
242
+
243
+ test("bigint columns arriving as strings are coerced to numbers", () => {
244
+ /*
245
+ * The pg driver returns bigint as a string. Left uncoerced, coverage
246
+ * arithmetic would concatenate instead of adding.
247
+ */
248
+ const result: UptimeDailyAggregate = aggregate([
249
+ row(MONITOR_A, "2026-07-01", STATUS_UP, 3600),
250
+ row(MONITOR_A, "2026-07-01", STATUS_DOWN, 1800),
251
+ ]);
252
+
253
+ const bucket: UptimeDayBucket = bucketsFor(result, MONITOR_A)[0]!;
254
+
255
+ expect(typeof bucket.coveredSeconds).toBe("number");
256
+ expect(typeof bucket.daySeconds).toBe("number");
257
+ expect(bucket.coveredSeconds).toBe(5400);
258
+ });
259
+
260
+ test("fractional seconds are not rounded away per status group", () => {
261
+ /*
262
+ * Seconds come back as double precision on purpose. An earlier draft
263
+ * cast each (monitor, day, status) sum to a bigint, which loses up to
264
+ * half a second per status - so a fully covered day with two statuses
265
+ * reported 86,399 of 86,400 seconds and looked one second short of
266
+ * complete. Observed against real data, on a day with 21,914 flapping
267
+ * transitions.
268
+ */
269
+ const result: UptimeDailyAggregate = aggregate([
270
+ { ...row(MONITOR_A, "2026-07-01", STATUS_UP, 0), seconds: 86399.6 },
271
+ { ...row(MONITOR_A, "2026-07-01", STATUS_DOWN, 0), seconds: 0.4 },
272
+ ]);
273
+
274
+ const bucket: UptimeDayBucket = bucketsFor(result, MONITOR_A)[0]!;
275
+
276
+ expect(bucket.coveredSeconds).toBeCloseTo(86400, 5);
277
+ expect(bucket.statusDurations).toHaveLength(2);
278
+ });
279
+
280
+ test("an empty result set is complete, not incomplete", () => {
281
+ const result: UptimeDailyAggregate = aggregate([]);
282
+
283
+ expect(result.monitors).toHaveLength(0);
284
+ expect(result.isComplete).toBe(true);
285
+ expect(result.completeFrom).toBeNull();
286
+ });
287
+
288
+ test("there is no silent cap: the aggregate always declares completeness", () => {
289
+ /*
290
+ * Requirement 4. The old path had a bound that nothing surfaced. These
291
+ * fields exist so a bound added later has to SHOW rather than be
292
+ * absorbed into a green bar.
293
+ */
294
+ const result: UptimeDailyAggregate = aggregate([
295
+ row(MONITOR_A, "2026-07-01", STATUS_UP, 86400),
296
+ ]);
297
+
298
+ expect(result).toHaveProperty("isComplete");
299
+ expect(result).toHaveProperty("completeFrom");
300
+ expect(result.isComplete).toBe(true);
301
+ });
302
+ });
303
+ });