@oneuptime/common 13.0.7 → 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 (759) 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/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +1 -1
  6. package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +1 -1
  7. package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +1 -1
  8. package/Scripts/generate-service-worker.js +65 -4
  9. package/Server/API/AIBillingAPI.ts +1 -0
  10. package/Server/API/AIInsightAPI.ts +1 -4
  11. package/Server/API/AIInvestigationAPI.ts +1 -4
  12. package/Server/API/AIReadinessAPI.ts +4 -5
  13. package/Server/API/AlertAPI.ts +1 -0
  14. package/Server/API/AutoRemediationAPI.ts +1 -4
  15. package/Server/API/BaseAPI.ts +34 -0
  16. package/Server/API/BaseAnalyticsAPI.ts +80 -9
  17. package/Server/API/BillingAPI.ts +2 -0
  18. package/Server/API/BillingInvoiceAPI.ts +1 -0
  19. package/Server/API/BillingPaymentMethodAPI.ts +2 -0
  20. package/Server/API/CodeFixRunAPI.ts +1 -3
  21. package/Server/API/CommonAPI.ts +68 -4
  22. package/Server/API/DashboardAPI.ts +12 -12
  23. package/Server/API/GlobalConfigAPI.ts +243 -460
  24. package/Server/API/IncidentAPI.ts +2 -0
  25. package/Server/API/IncidentEpisodeAPI.ts +1 -0
  26. package/Server/API/NotificationAPI.ts +1 -0
  27. package/Server/API/OnCallCalendarAPI.ts +4 -5
  28. package/Server/API/OnCallDutyPolicyAPI.ts +7 -0
  29. package/Server/API/OnCallReadinessAPI.ts +4 -1
  30. package/Server/API/OnCallScheduleTimelineAPI.ts +731 -0
  31. package/Server/API/ProjectAPI.ts +1 -0
  32. package/Server/API/ProjectOIDC.ts +25 -16
  33. package/Server/API/ProjectSSO.ts +24 -16
  34. package/Server/API/ScheduledMaintenanceAPI.ts +1 -0
  35. package/Server/API/StatusPageAPI.ts +261 -74
  36. package/Server/API/TeamMemberAPI.ts +13 -1
  37. package/Server/API/UserCallAPI.ts +2 -0
  38. package/Server/API/UserEmailAPI.ts +2 -0
  39. package/Server/API/UserIncomingCallNumberAPI.ts +2 -0
  40. package/Server/API/UserMicrosoftTeamsAPI.ts +1 -0
  41. package/Server/API/UserSlackAPI.ts +1 -0
  42. package/Server/API/UserSmsAPI.ts +2 -0
  43. package/Server/API/UserTotpAuthAPI.ts +1 -0
  44. package/Server/API/UserWebAuthnAPI.ts +2 -0
  45. package/Server/API/UserWebhookAPI.ts +1 -0
  46. package/Server/API/UserWhatsAppAPI.ts +2 -0
  47. package/Server/Enterprise/EnterpriseEdition.ts +649 -0
  48. package/Server/Enterprise/EnterpriseFeature.ts +48 -0
  49. package/Server/Enterprise/EnterpriseLicenseSnapshot.ts +192 -0
  50. package/Server/Enterprise/EnterpriseServerModule.ts +233 -0
  51. package/Server/EnvironmentConfig.ts +147 -2
  52. package/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.ts +25 -0
  53. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  54. package/Server/Middleware/MasterAdminAuthorization.ts +13 -2
  55. package/Server/Middleware/UserAuthorization.ts +117 -21
  56. package/Server/Services/ApiKeyPermissionService.ts +101 -4
  57. package/Server/Services/AuditLogService.ts +84 -1079
  58. package/Server/Services/DatabaseService.ts +245 -0
  59. package/Server/Services/DetectionRuleService.ts +5 -4
  60. package/Server/Services/EnterpriseLicenseService.ts +38 -0
  61. package/Server/Services/GlobalConfigService.ts +80 -1
  62. package/Server/Services/MonitorProbeService.ts +116 -20
  63. package/Server/Services/MonitorService.ts +56 -4
  64. package/Server/Services/MonitorStatusTimelineService.ts +284 -0
  65. package/Server/Services/MonitorTemplateService.ts +26 -0
  66. package/Server/Services/OnCallDutyPolicyChildService.ts +261 -0
  67. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +2 -2
  68. package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +2 -2
  69. package/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.ts +2 -2
  70. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +2 -2
  71. package/Server/Services/PayAsYouGoBillingService.ts +23 -28
  72. package/Server/Services/ProjectService.ts +302 -0
  73. package/Server/Services/RumSessionPinService.ts +293 -22
  74. package/Server/Services/StatusPagePrivateUserService.ts +7 -3
  75. package/Server/Services/StatusPageService.ts +78 -1
  76. package/Server/Services/TeamMemberService.ts +11 -0
  77. package/Server/Services/TeamPermissionService.ts +120 -26
  78. package/Server/Services/TeamService.ts +10 -0
  79. package/Server/Services/ThreatIntelFeedService.ts +6 -3
  80. package/Server/Services/UserService.ts +10 -17
  81. package/Server/Types/Database/Permissions/BasePermission.ts +9 -7
  82. package/Server/Types/Database/Permissions/CreatePermission.ts +15 -0
  83. package/Server/Types/Database/Permissions/EditionPermission.ts +208 -18
  84. package/Server/Types/Database/Permissions/Index.ts +75 -11
  85. package/Server/Types/Database/Permissions/TablePermission.ts +34 -31
  86. package/Server/Types/Database/Permissions/UpdatePermission.ts +29 -0
  87. package/Server/Types/Markdown.ts +41 -0
  88. package/Server/Utils/AI/AIChatPrivacyFilter.ts +8 -0
  89. package/Server/Utils/AI/AIRunPrivacyFilter.ts +8 -0
  90. package/Server/Utils/CommunityEditionSsoReport.ts +361 -0
  91. package/Server/Utils/EditionEnforcement.ts +233 -0
  92. package/Server/Utils/FrontendEnvironment.ts +41 -2
  93. package/Server/Utils/InstanceHealth/RedisHealth.ts +12 -277
  94. package/Server/Utils/MasterAdminPageAccess.ts +61 -0
  95. package/Server/Utils/Monitor/AffectedResourceList.ts +190 -0
  96. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +10 -1
  97. package/Server/Utils/Monitor/MonitorAlert.ts +3 -1
  98. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +543 -333
  99. package/Server/Utils/Monitor/MonitorIncident.ts +3 -1
  100. package/Server/Utils/Monitor/MonitoringIntervalValidator.ts +116 -0
  101. package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1 -1
  102. package/Server/Utils/Realtime.ts +163 -45
  103. package/Server/Utils/SecurityEvent/DetectionRuleEvaluator.ts +11 -28
  104. package/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.ts +170 -0
  105. package/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.ts +1 -1
  106. package/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.ts +6 -2
  107. package/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.ts +10 -9
  108. package/Server/Utils/Workspace/Slack/Slack.ts +423 -10
  109. package/Tests/App/Accounts/LoginPasskey.test.tsx +18 -0
  110. package/Tests/App/Accounts/LoginSsoEdition.test.tsx +616 -0
  111. package/Tests/App/AdminDashboard/AdminAuthenticationSsoEdition.test.tsx +217 -0
  112. package/Tests/App/AdminDashboard/AdminEnterprisePluginPage.test.tsx +357 -0
  113. package/Tests/App/AdminDashboard/EnterpriseLicensesShell.test.tsx +214 -0
  114. package/Tests/App/AdminDashboard/GlobalSSOEnterpriseShells.test.tsx +271 -0
  115. package/Tests/App/AdminDashboard/HealthShells.test.tsx +659 -0
  116. package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +1059 -0
  117. package/Tests/App/Dashboard/AffectedResourcesPickerEditForm.test.tsx +376 -0
  118. package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +757 -0
  119. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +307 -0
  120. package/Tests/App/Dashboard/ArgumentsFormGroupByUnlistedAttributes.test.tsx +131 -0
  121. package/Tests/App/Dashboard/AuditLogsAndComplianceShells.test.tsx +572 -0
  122. package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +1 -1
  123. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +9 -1
  124. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +24 -0
  125. package/Tests/App/Dashboard/DashboardVariableSelector.test.tsx +162 -8
  126. package/Tests/App/Dashboard/EnterpriseFeatureUpgrade.test.tsx +547 -0
  127. package/Tests/App/Dashboard/EnterprisePluginPage.test.tsx +667 -0
  128. package/Tests/App/Dashboard/EntityFilterDropdownArchivedSlos.test.tsx +105 -6
  129. package/Tests/App/Dashboard/EntityFilterDropdownUnlistedSelection.test.tsx +434 -0
  130. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +276 -1
  131. package/Tests/App/Dashboard/IdentityEnterpriseShells.test.tsx +319 -0
  132. package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +9 -1
  133. package/Tests/App/Dashboard/OnboardingSsoEmptyState.test.tsx +185 -0
  134. package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +58 -42
  135. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +42 -5
  136. package/Tests/App/Dashboard/ResourceAuditLogsSideMenus.test.tsx +34 -23
  137. package/Tests/App/Dashboard/ScheduleTimeline.test.tsx +1580 -0
  138. package/Tests/App/Dashboard/ScheduleTimelineAPI.test.ts +280 -0
  139. package/Tests/App/Dashboard/ScheduleTimelineModel.test.ts +673 -0
  140. package/Tests/App/Dashboard/SecurityEventConnectionDiagnostics.test.tsx +133 -70
  141. package/Tests/App/Dashboard/SecurityEventDetailPanel.test.tsx +399 -0
  142. package/Tests/App/Dashboard/SecurityEventListRow.test.tsx +214 -0
  143. package/Tests/App/Dashboard/SecurityEventsDetectionRulesPage.test.tsx +86 -3
  144. package/Tests/App/Dashboard/SecurityEventsEmptyState.test.tsx +1 -58
  145. package/Tests/App/Dashboard/SecurityEventsGuideContent.test.ts +1313 -0
  146. package/Tests/App/Dashboard/SecurityEventsHowItWorksCard.test.tsx +536 -0
  147. package/Tests/App/Dashboard/SecurityEventsNavTabs.test.tsx +248 -0
  148. package/Tests/App/Dashboard/SecurityEventsNoResults.test.tsx +222 -0
  149. package/Tests/App/Dashboard/SecurityEventsThreatIntelPage.test.tsx +199 -0
  150. package/Tests/App/Dashboard/SecurityEventsViewer.test.tsx +1203 -0
  151. package/Tests/App/Dashboard/SloOverviewHero.test.tsx +171 -36
  152. package/Tests/App/Dashboard/SloViewSideMenu.test.tsx +215 -0
  153. package/Tests/App/Dashboard/SloViewSideMenuCounts.test.tsx +40 -3
  154. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +11 -91
  155. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +12 -4
  156. package/Tests/App/PublicDashboard/PublicDashboardWidgetListAuth.test.tsx +623 -0
  157. package/Tests/App/StatusPage/StatusPageSsoEmptyState.test.tsx +319 -0
  158. package/Tests/Helpers/RefreshAwareApiScan.ts +757 -0
  159. package/Tests/Helpers/TenantScopedRequestScan.ts +515 -0
  160. package/Tests/Models/OnCallCalendarFeedModels.test.ts +23 -1
  161. package/Tests/Scripts/GenerateServiceWorker.test.ts +931 -0
  162. package/Tests/Server/API/AIChatRouteAuthorization.test.ts +138 -5
  163. package/Tests/Server/API/AIGenerationProjectAIToggle.test.ts +187 -0
  164. package/Tests/Server/API/AIInvestigationAvailabilityAuthorization.test.ts +51 -3
  165. package/Tests/Server/API/AIInvestigationEvidenceAPI.test.ts +55 -2
  166. package/Tests/Server/API/BaseAPICreateItemDoesNotMutateModel.test.ts +130 -0
  167. package/Tests/Server/API/BaseAnalyticsAPIAggregateCache.test.ts +729 -0
  168. package/Tests/Server/API/BillingPayAsYouGoStatus.test.ts +93 -7
  169. package/Tests/Server/API/BillingPaymentMethodAPI.test.ts +36 -3
  170. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +1003 -24
  171. package/Tests/Server/API/DashboardPublicRateLimit.test.ts +6 -3
  172. package/Tests/Server/API/ExpiredSessionAIRoutes.test.ts +708 -0
  173. package/Tests/Server/API/ExpiredSessionCustomRoutes.test.ts +928 -0
  174. package/Tests/Server/API/ExpiredSessionMonitorTemplate.test.ts +319 -0
  175. package/Tests/Server/API/ExpiredSessionOnCallDutyPolicy.test.ts +336 -0
  176. package/Tests/Server/API/ExpiredSessionRouteGuards.test.ts +820 -0
  177. package/Tests/Server/API/GlobalConfigLicense.test.ts +766 -381
  178. package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +115 -12
  179. package/Tests/Server/API/OnCallCalendarAPI.test.ts +375 -8
  180. package/Tests/Server/API/OnCallReadinessAPI.test.ts +196 -912
  181. package/Tests/Server/API/OnCallScheduleTimelineAPI.test.ts +1513 -0
  182. package/Tests/Server/API/OnCallSetupReminderAPI.test.ts +119 -7
  183. package/Tests/Server/API/ProjectScopedCustomRouteAuth.test.ts +111 -4
  184. package/Tests/Server/API/PublicRouteInvalidAccessToken.test.ts +615 -0
  185. package/Tests/Server/API/SecurityEventAttributesAPI.test.ts +89 -2
  186. package/Tests/Server/API/SessionReplayAPI.test.ts +121 -0
  187. package/Tests/Server/API/SsoProviderListingsEdition.test.ts +432 -0
  188. package/Tests/Server/API/StatusPageMasterPasswordRateLimit.test.ts +4 -2
  189. package/Tests/Server/API/StatusPagePostmortemVisibility.test.ts +441 -0
  190. package/Tests/Server/API/StatusPageTestEmailReportAuth.test.ts +664 -0
  191. package/Tests/Server/API/TeamMemberLeaveScimLockEdition.test.ts +252 -0
  192. package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +7 -0
  193. package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +6 -0
  194. package/Tests/Server/API/UserNotificationSettingAPI.test.ts +65 -4
  195. package/Tests/Server/API/UserProjectsAPI.test.ts +63 -3
  196. package/Tests/Server/API/UserTotpAuthAPI.test.ts +48 -2
  197. package/Tests/Server/API/UserWebAuthnAuthenticationOptionsRoute.test.ts +11 -2
  198. package/Tests/Server/API/UserWebAuthnBackupCodeMinting.test.ts +46 -3
  199. package/Tests/Server/API/UserWebAuthnPasskeyAPI.test.ts +18 -2
  200. package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +24 -7
  201. package/Tests/Server/Enterprise/EnterpriseEdition.test.ts +824 -0
  202. package/Tests/Server/Enterprise/EnterpriseFeatureActive.test.ts +643 -0
  203. package/Tests/Server/Enterprise/EnterpriseLicenseSnapshot.test.ts +294 -0
  204. package/Tests/Server/Enterprise/EnterpriseModelFeatureMap.test.ts +93 -0
  205. package/Tests/Server/Enterprise/EnterpriseOnlyModulesLiveInEe.test.ts +582 -0
  206. package/Tests/Server/Enterprise/EnterpriseServerModule.test.ts +247 -0
  207. package/Tests/Server/Enterprise/FakeEnterpriseModule.ts +502 -0
  208. package/Tests/Server/Enterprise/TestBillingFlag.ts +70 -0
  209. package/Tests/Server/EnvironmentConfigEnterpriseEdition.test.ts +314 -0
  210. package/Tests/Server/Infrastructure/ContainerBootConfiguration.test.ts +289 -0
  211. package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +5 -1
  212. package/Tests/Server/Middleware/ExpiredSessionUserMiddleware.test.ts +563 -0
  213. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +299 -19
  214. package/Tests/Server/Middleware/PublicRouteUserMiddleware.test.ts +453 -0
  215. package/Tests/Server/Middleware/UserAuthorization.test.ts +37 -2
  216. package/Tests/Server/Middleware/UserAuthorizationEditionEnforcement.test.ts +516 -0
  217. package/Tests/Server/Services/AIChatPrivacyPin.test.ts +24 -0
  218. package/Tests/Server/Services/ApiKeyPermissionSecurity.test.ts +2 -0
  219. package/Tests/Server/Services/ApiKeyPermissionService.test.ts +3 -3
  220. package/Tests/Server/Services/AuditLogService.test.ts +460 -990
  221. package/Tests/Server/Services/CreateAndReadResponsesKeepIds.test.ts +263 -0
  222. package/Tests/Server/Services/DatabaseServiceCreateSuppliedId.test.ts +147 -0
  223. package/Tests/Server/Services/DatabaseServiceLoginBeforeHooks.test.ts +1078 -0
  224. package/Tests/Server/Services/DatabaseServiceTenantRelationGuard.test.ts +259 -0
  225. package/Tests/Server/Services/DatabaseServiceWriteOperatorValues.test.ts +147 -0
  226. package/Tests/Server/Services/EnterpriseLicenseServiceKeyGeneration.test.ts +151 -0
  227. package/Tests/Server/Services/GlobalConfigServiceLicenseColumns.test.ts +357 -0
  228. package/Tests/Server/Services/MonitorDestinationInfoIPv6.test.ts +101 -0
  229. package/Tests/Server/Services/MonitorPayAsYouGoBilling.test.ts +115 -1
  230. package/Tests/Server/Services/MonitorProbeServiceIntervalScheduling.test.ts +333 -0
  231. package/Tests/Server/Services/MonitorStatusTimelineDailyAggregates.test.ts +303 -0
  232. package/Tests/Server/Services/OnCallChildCreateAuthorizationPostgres.test.ts +711 -0
  233. package/Tests/Server/Services/OnCallDutyPolicyChildService.test.ts +733 -0
  234. package/Tests/Server/Services/PayAsYouGoBillingService.test.ts +190 -140
  235. package/Tests/Server/Services/PayAsYouGoStripeRateLimitBudget.test.ts +4 -9
  236. package/Tests/Server/Services/PermissionBlockDeletion.test.ts +780 -0
  237. package/Tests/Server/Services/PermissionBlockDeletionPostgres.test.ts +628 -0
  238. package/Tests/Server/Services/ProjectServiceAuditLogLicense.test.ts +701 -0
  239. package/Tests/Server/Services/ProjectServiceAuditLogSettingsCache.test.ts +31 -35
  240. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +77 -1
  241. package/Tests/Server/Services/RumSessionPinCreatePermission.test.ts +715 -0
  242. package/Tests/Server/Services/ScimPushGroupsTeamLocksEdition.test.ts +431 -0
  243. package/Tests/Server/Services/SsoRequirementReadMasking.test.ts +419 -0
  244. package/Tests/Server/Services/SsoRequirementWriteGuard.test.ts +769 -0
  245. package/Tests/Server/Services/StatusPagePrivateUserInvitationPolicy.test.ts +94 -0
  246. package/Tests/Server/Services/TeamMemberMultiTenantReadPostgres.test.ts +394 -0
  247. package/Tests/Server/Services/TeamPrivilegeEscalation.test.ts +6 -0
  248. package/Tests/Server/Services/TelemetryPayAsYouGoBilling.test.ts +131 -0
  249. package/Tests/Server/Services/UserAuthenticationService.test.ts +35 -0
  250. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +13 -2
  251. package/Tests/Server/Services/UserServiceSeatLimit.test.ts +94 -56
  252. package/Tests/Server/Types/Database/Permissions/EditionPermission.test.ts +1823 -0
  253. package/Tests/Server/Types/Database/Permissions/ModelPermissionAnonymousRefusal.test.ts +518 -0
  254. package/Tests/Server/Types/Database/Permissions/MultiTenantPermissions.test.ts +350 -0
  255. package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +282 -25
  256. package/Tests/Server/Types/Database/Permissions/TableLevelBlockPermission.test.ts +459 -0
  257. package/Tests/Server/Types/Markdown.test.ts +181 -0
  258. package/Tests/Server/Utils/AI/AIRunPrivacyFilter.test.ts +101 -9
  259. package/Tests/Server/Utils/AI/ChatAgentHelpers.test.ts +88 -4
  260. package/Tests/Server/Utils/AI/Toolbox/ExceptionTools.test.ts +459 -0
  261. package/Tests/Server/Utils/AI/Toolbox/LogTools.test.ts +794 -0
  262. package/Tests/Server/Utils/AI/Toolbox/RecentChangesTools.test.ts +574 -0
  263. package/Tests/Server/Utils/AI/Toolbox/SecurityEventTools.test.ts +596 -0
  264. package/Tests/Server/Utils/CaptchaRequestAndLogging.test.ts +395 -0
  265. package/Tests/Server/Utils/CommunityEditionSsoReport.test.ts +565 -0
  266. package/Tests/Server/Utils/EditionEnforcement.test.ts +303 -0
  267. package/Tests/Server/Utils/FrontendEnvironment.test.ts +367 -0
  268. package/Tests/Server/Utils/InstanceHealth/RedisHealth.test.ts +38 -418
  269. package/Tests/Server/Utils/MasterAdminPageAccess.test.ts +251 -0
  270. package/Tests/Server/Utils/Monitor/AffectedResourceList.test.ts +518 -0
  271. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +24 -4
  272. package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +616 -0
  273. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +934 -96
  274. package/Tests/Server/Utils/Monitor/MonitoringIntervalValidator.test.ts +188 -0
  275. package/Tests/Server/Utils/Realtime.test.ts +502 -0
  276. package/Tests/Server/Utils/SecurityEvent/Connectors/GoogleSecOps/GoogleSecOpsConnectorSeverity.test.ts +216 -0
  277. package/Tests/Server/Utils/SecurityEvent/DetectionRuleEvaluator.test.ts +142 -0
  278. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.test.ts +140 -0
  279. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepairClickhouseIntegration.test.ts +542 -0
  280. package/Tests/Server/Utils/Telemetry/ChangeEventRow.test.ts +743 -0
  281. package/Tests/Server/Utils/Telemetry/RuntimeMetrics.test.ts +465 -0
  282. package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/Alert.test.ts +202 -0
  283. package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/Incident.test.ts +194 -0
  284. package/Tests/Server/Utils/Workspace/MicrosoftTeams/Messages/ScheduledMaintenance.test.ts +231 -0
  285. package/Tests/Server/Utils/Workspace/Slack/Messages/Alert.test.ts +194 -0
  286. package/Tests/Server/Utils/Workspace/Slack/Messages/AlertEpisode.test.ts +194 -0
  287. package/Tests/Server/Utils/Workspace/Slack/Messages/Incident.test.ts +194 -0
  288. package/Tests/Server/Utils/Workspace/Slack/Messages/IncidentEpisode.test.ts +194 -0
  289. package/Tests/Server/Utils/Workspace/Slack/Messages/ScheduledMaintenance.test.ts +247 -0
  290. package/Tests/Server/Utils/Workspace/Slack/SlackMarkdownSectionSplit.test.ts +1209 -0
  291. package/Tests/Types/AI/AIInsightEvidence.test.ts +206 -0
  292. package/Tests/Types/API/HostnameFromAuthority.test.ts +20 -10
  293. package/Tests/Types/API/HostnameIPv6.test.ts +149 -0
  294. package/Tests/Types/BaseDatabase/DatabaseCommonInteractionPropsUtilAnonymous.test.ts +144 -0
  295. package/Tests/Types/Call/CallProvider.test.ts +528 -0
  296. package/Tests/Types/Database/CanonicalModelInstance.test.ts +190 -0
  297. package/Tests/Types/Kubernetes/KubernetesCostIngest.test.ts +236 -0
  298. package/Tests/Types/OnCallDutyPolicy/ScheduleTimeline.test.ts +736 -0
  299. package/Tests/Types/OnCallDutyPolicy/ScheduleTimelineLayout.test.ts +996 -0
  300. package/Tests/Types/SecurityEvent/SigmaRule.test.ts +142 -0
  301. package/Tests/Types/Workflow/Components/JSON.test.ts +182 -0
  302. package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +116 -1
  303. package/Tests/UI/Components/Dropdown/DropdownAsyncOptions.test.tsx +81 -0
  304. package/Tests/UI/Components/EditionLabelLapseCopy.ts +104 -0
  305. package/Tests/UI/Components/EditionLabelLicenseManagerWiring.test.tsx +254 -0
  306. package/Tests/UI/Components/EditionLabelLicenseRefresh.test.tsx +165 -171
  307. package/Tests/UI/Components/EditionLabelLicenseStatus.test.tsx +1586 -0
  308. package/Tests/UI/Components/EditionLabelLicenseWritesGuard.test.tsx +157 -0
  309. package/Tests/UI/Components/EventAttachmentList.test.tsx +174 -0
  310. package/Tests/UI/Components/Forms/BasicFormBurnRateTemplateDefaults.test.tsx +395 -0
  311. package/Tests/UI/Components/Forms/BasicFormCollapsibleSections.test.tsx +286 -0
  312. package/Tests/UI/Components/Forms/FormSummary.test.tsx +128 -0
  313. package/Tests/UI/Components/Graphs/DayUptimeGraph.test.tsx +49 -2
  314. package/Tests/UI/Components/MarkdownConverters.test.ts +684 -0
  315. package/Tests/UI/Components/ModelListEmptyState.test.tsx +289 -0
  316. package/Tests/UI/Components/ModelTable/BaseModelTableQueryChange.test.tsx +365 -0
  317. package/Tests/UI/Components/OpenAuthenticatedUrl.test.ts +358 -0
  318. package/Tests/UI/Components/SessionAwareImage.test.tsx +496 -0
  319. package/Tests/UI/Components/TelemetryHistogramZeroBuckets.test.tsx +190 -0
  320. package/Tests/UI/Components/TelemetryViewer/TelemetryViewerEmptyContent.test.tsx +131 -0
  321. package/Tests/UI/Components/TooltipInteractive.test.tsx +97 -0
  322. package/Tests/UI/EsbuildEnterpriseAlias.test.ts +1127 -0
  323. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsChatsCardInstallGuidance.test.tsx +1 -1
  324. package/Tests/UI/Monitor/EvaluationLogListTimestamps.test.tsx +367 -0
  325. package/Tests/UI/Utils/API/BaseAPIRefreshSession.test.ts +1087 -0
  326. package/Tests/UI/Utils/API/DashboardHost.ts +12 -0
  327. package/Tests/UI/Utils/API/FakeAxiosServer.ts +183 -0
  328. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  329. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  330. package/Tests/UI/Utils/Cookie.test.ts +440 -0
  331. package/Tests/UI/Utils/ModelAPIRequestClient.test.ts +278 -0
  332. package/Tests/UI/Utils/Navigation.test.ts +33 -0
  333. package/Tests/UI/Utils/PricingPlan.test.ts +295 -0
  334. package/Tests/UI/Utils/Realtime.test.ts +759 -0
  335. package/Tests/UI/Utils/Telemetry/Telemetry.test.ts +500 -0
  336. package/Tests/Utils/APIInstanceDispatch.test.ts +311 -0
  337. package/Tests/Utils/HostAddressUtil.test.ts +138 -0
  338. package/Tests/Utils/Monitor/MonitorDestinationUtil.test.ts +313 -0
  339. package/Tests/Utils/Monitor/MonitoringIntervalUtil.test.ts +304 -0
  340. package/Tests/Utils/SecurityEvent/GoogleSecOpsAlertNormalizerSeverity.test.ts +565 -0
  341. package/Tests/Utils/StatusPage/UptimeDailyAggregateUtil.test.ts +248 -0
  342. package/Types/API/Hostname.ts +63 -16
  343. package/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.ts +40 -0
  344. package/Types/Database/AccessControl/ColumnAccessControl.ts +19 -7
  345. package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +19 -7
  346. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +15 -6
  347. package/Types/Database/CanonicalModelInstance.ts +102 -0
  348. package/Types/Database/TableColumn.ts +20 -6
  349. package/Types/EnterpriseLicense/EnterpriseLicensePeriods.ts +29 -0
  350. package/Types/IP/IPv6.ts +6 -1
  351. package/Types/Monitor/MonitorEvaluationSummary.ts +3 -0
  352. package/Types/Monitor/MonitorStep.ts +6 -1
  353. package/Types/Monitor/ProxmoxAlertTemplates.ts +1 -1
  354. package/Types/OnCallDutyPolicy/ScheduleTimeline.ts +507 -0
  355. package/Types/OnCallDutyPolicy/ScheduleTimelineLayout.ts +589 -0
  356. package/Types/Realtime/EventName.ts +11 -0
  357. package/Types/SecurityEvent/DetectionFindingConstants.ts +14 -0
  358. package/Types/SecurityEvent/OcsfSeverity.ts +31 -26
  359. package/Types/SecurityEvent/SigmaRule.ts +51 -0
  360. package/Types/SecurityEvent/ThreatIntelConstants.ts +18 -0
  361. package/Types/StatusPage/UptimeDailyAggregate.ts +110 -0
  362. package/UI/Components/AttachmentList/EventAttachmentList.tsx +30 -2
  363. package/UI/Components/Dashboard/DashboardVariableControl.tsx +12 -2
  364. package/UI/Components/Dropdown/Dropdown.tsx +6 -1
  365. package/UI/Components/EditionLabel/EditionLabel.tsx +808 -1195
  366. package/UI/Components/EditionLabel/LicenseManager.ts +119 -0
  367. package/UI/Components/EventItem/EventItem.tsx +12 -1
  368. package/UI/Components/Forms/BasicForm.tsx +151 -126
  369. package/UI/Components/Forms/CollapsibleFormSection.tsx +63 -0
  370. package/UI/Components/Forms/Fields/FormField.tsx +10 -4
  371. package/UI/Components/Forms/FormSummary.tsx +7 -0
  372. package/UI/Components/Forms/Types/Field.ts +10 -0
  373. package/UI/Components/Graphs/DayUptimeGraph.tsx +123 -15
  374. package/UI/Components/Markdown.tsx/MarkdownConverters.ts +0 -0
  375. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +2 -1
  376. package/UI/Components/Markdown.tsx/SessionAwareImage.tsx +122 -0
  377. package/UI/Components/ModelList/ModelList.tsx +11 -1
  378. package/UI/Components/ModelTable/BaseModelTable.tsx +34 -5
  379. package/UI/Components/ModelTable/TableView.tsx +1 -1
  380. package/UI/Components/MonitorGraphs/Uptime.tsx +69 -0
  381. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +28 -12
  382. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +8 -1
  383. package/UI/Components/Tooltip/Tooltip.tsx +8 -1
  384. package/UI/Utils/API/API.ts +173 -12
  385. package/UI/Utils/API/RequestOptions.ts +11 -0
  386. package/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.ts +31 -7
  387. package/UI/Utils/ModelAPI/ModelAPI.ts +30 -9
  388. package/UI/Utils/Navigation.ts +9 -0
  389. package/UI/Utils/OpenAuthenticatedUrl.ts +154 -0
  390. package/UI/Utils/Realtime.ts +176 -30
  391. package/UI/esbuild-enterprise.js +263 -0
  392. package/Utils/API.ts +14 -5
  393. package/Utils/EnterpriseLicense/EnterpriseLicenseUsage.ts +7 -2
  394. package/Utils/HostAddressUtil.ts +94 -0
  395. package/Utils/Monitor/MonitorDestinationUtil.ts +157 -0
  396. package/Utils/Monitor/MonitoringIntervalUtil.ts +436 -0
  397. package/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.ts +157 -7
  398. package/Utils/SecurityEvent/Sigma/SigmaRuleParser.ts +9 -17
  399. package/Utils/Slo/SloBurnRateTemplate.ts +2 -2
  400. package/Utils/StatusPage/UptimeDailyAggregateUtil.ts +192 -0
  401. package/build/dist/Models/AnalyticsModels/AuditLog.js +23 -15
  402. package/build/dist/Models/AnalyticsModels/AuditLog.js.map +1 -1
  403. package/build/dist/Models/DatabaseModels/GlobalConfig.js +25 -0
  404. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  405. package/build/dist/Models/DatabaseModels/Monitor.js +2 -2
  406. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  407. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +2 -1
  408. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  409. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +1 -1
  410. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -1
  411. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +1 -1
  412. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -1
  413. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +1 -1
  414. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -1
  415. package/build/dist/Server/API/AIBillingAPI.js +1 -1
  416. package/build/dist/Server/API/AIBillingAPI.js.map +1 -1
  417. package/build/dist/Server/API/AIInsightAPI.js +1 -4
  418. package/build/dist/Server/API/AIInsightAPI.js.map +1 -1
  419. package/build/dist/Server/API/AIInvestigationAPI.js +1 -4
  420. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  421. package/build/dist/Server/API/AIReadinessAPI.js +1 -3
  422. package/build/dist/Server/API/AIReadinessAPI.js.map +1 -1
  423. package/build/dist/Server/API/AlertAPI.js +1 -1
  424. package/build/dist/Server/API/AlertAPI.js.map +1 -1
  425. package/build/dist/Server/API/AutoRemediationAPI.js +1 -4
  426. package/build/dist/Server/API/AutoRemediationAPI.js.map +1 -1
  427. package/build/dist/Server/API/BaseAPI.js +33 -0
  428. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  429. package/build/dist/Server/API/BaseAnalyticsAPI.js +55 -10
  430. package/build/dist/Server/API/BaseAnalyticsAPI.js.map +1 -1
  431. package/build/dist/Server/API/BillingAPI.js +2 -2
  432. package/build/dist/Server/API/BillingAPI.js.map +1 -1
  433. package/build/dist/Server/API/BillingInvoiceAPI.js +1 -1
  434. package/build/dist/Server/API/BillingInvoiceAPI.js.map +1 -1
  435. package/build/dist/Server/API/BillingPaymentMethodAPI.js +2 -2
  436. package/build/dist/Server/API/BillingPaymentMethodAPI.js.map +1 -1
  437. package/build/dist/Server/API/CodeFixRunAPI.js +1 -3
  438. package/build/dist/Server/API/CodeFixRunAPI.js.map +1 -1
  439. package/build/dist/Server/API/CommonAPI.js +50 -5
  440. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  441. package/build/dist/Server/API/DashboardAPI.js +12 -12
  442. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  443. package/build/dist/Server/API/GlobalConfigAPI.js +158 -342
  444. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  445. package/build/dist/Server/API/IncidentAPI.js +2 -2
  446. package/build/dist/Server/API/IncidentAPI.js.map +1 -1
  447. package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
  448. package/build/dist/Server/API/IncidentEpisodeAPI.js.map +1 -1
  449. package/build/dist/Server/API/NotificationAPI.js +1 -1
  450. package/build/dist/Server/API/NotificationAPI.js.map +1 -1
  451. package/build/dist/Server/API/OnCallCalendarAPI.js +1 -4
  452. package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -1
  453. package/build/dist/Server/API/OnCallDutyPolicyAPI.js +6 -0
  454. package/build/dist/Server/API/OnCallDutyPolicyAPI.js.map +1 -1
  455. package/build/dist/Server/API/OnCallReadinessAPI.js +4 -1
  456. package/build/dist/Server/API/OnCallReadinessAPI.js.map +1 -1
  457. package/build/dist/Server/API/OnCallScheduleTimelineAPI.js +494 -0
  458. package/build/dist/Server/API/OnCallScheduleTimelineAPI.js.map +1 -0
  459. package/build/dist/Server/API/ProjectAPI.js +1 -1
  460. package/build/dist/Server/API/ProjectAPI.js.map +1 -1
  461. package/build/dist/Server/API/ProjectOIDC.js +24 -16
  462. package/build/dist/Server/API/ProjectOIDC.js.map +1 -1
  463. package/build/dist/Server/API/ProjectSSO.js +24 -16
  464. package/build/dist/Server/API/ProjectSSO.js.map +1 -1
  465. package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
  466. package/build/dist/Server/API/ScheduledMaintenanceAPI.js.map +1 -1
  467. package/build/dist/Server/API/StatusPageAPI.js +218 -76
  468. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  469. package/build/dist/Server/API/TeamMemberAPI.js +11 -4
  470. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  471. package/build/dist/Server/API/UserCallAPI.js +2 -2
  472. package/build/dist/Server/API/UserCallAPI.js.map +1 -1
  473. package/build/dist/Server/API/UserEmailAPI.js +2 -2
  474. package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
  475. package/build/dist/Server/API/UserIncomingCallNumberAPI.js +2 -2
  476. package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
  477. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +1 -1
  478. package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -1
  479. package/build/dist/Server/API/UserSlackAPI.js +1 -1
  480. package/build/dist/Server/API/UserSlackAPI.js.map +1 -1
  481. package/build/dist/Server/API/UserSmsAPI.js +2 -2
  482. package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
  483. package/build/dist/Server/API/UserTotpAuthAPI.js +1 -1
  484. package/build/dist/Server/API/UserTotpAuthAPI.js.map +1 -1
  485. package/build/dist/Server/API/UserWebAuthnAPI.js +2 -2
  486. package/build/dist/Server/API/UserWebAuthnAPI.js.map +1 -1
  487. package/build/dist/Server/API/UserWebhookAPI.js +1 -1
  488. package/build/dist/Server/API/UserWebhookAPI.js.map +1 -1
  489. package/build/dist/Server/API/UserWhatsAppAPI.js +2 -2
  490. package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
  491. package/build/dist/Server/Enterprise/EnterpriseEdition.js +466 -0
  492. package/build/dist/Server/Enterprise/EnterpriseEdition.js.map +1 -0
  493. package/build/dist/Server/Enterprise/EnterpriseFeature.js +39 -0
  494. package/build/dist/Server/Enterprise/EnterpriseFeature.js.map +1 -0
  495. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js +70 -0
  496. package/build/dist/Server/Enterprise/EnterpriseLicenseSnapshot.js.map +1 -0
  497. package/build/dist/Server/Enterprise/EnterpriseServerModule.js +84 -0
  498. package/build/dist/Server/Enterprise/EnterpriseServerModule.js.map +1 -0
  499. package/build/dist/Server/EnvironmentConfig.js +84 -2
  500. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  501. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js +18 -0
  502. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1793900000000-AddEnterpriseEditionFirstSeenAt.js.map +1 -0
  503. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  504. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  505. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -2
  506. package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
  507. package/build/dist/Server/Middleware/UserAuthorization.js +94 -15
  508. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  509. package/build/dist/Server/Services/ApiKeyPermissionService.js +80 -4
  510. package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
  511. package/build/dist/Server/Services/AuditLogService.js +71 -740
  512. package/build/dist/Server/Services/AuditLogService.js.map +1 -1
  513. package/build/dist/Server/Services/DatabaseService.js +171 -0
  514. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  515. package/build/dist/Server/Services/DetectionRuleService.js +4 -4
  516. package/build/dist/Server/Services/DetectionRuleService.js.map +1 -1
  517. package/build/dist/Server/Services/EnterpriseLicenseService.js +34 -0
  518. package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
  519. package/build/dist/Server/Services/GlobalConfigService.js +63 -0
  520. package/build/dist/Server/Services/GlobalConfigService.js.map +1 -1
  521. package/build/dist/Server/Services/MonitorProbeService.js +90 -17
  522. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  523. package/build/dist/Server/Services/MonitorService.js +45 -4
  524. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  525. package/build/dist/Server/Services/MonitorStatusTimelineService.js +242 -0
  526. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  527. package/build/dist/Server/Services/MonitorTemplateService.js +16 -0
  528. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  529. package/build/dist/Server/Services/OnCallDutyPolicyChildService.js +170 -0
  530. package/build/dist/Server/Services/OnCallDutyPolicyChildService.js.map +1 -0
  531. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +2 -2
  532. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  533. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +2 -2
  534. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
  535. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js +2 -2
  536. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleTeamService.js.map +1 -1
  537. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +2 -2
  538. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  539. package/build/dist/Server/Services/PayAsYouGoBillingService.js +20 -27
  540. package/build/dist/Server/Services/PayAsYouGoBillingService.js.map +1 -1
  541. package/build/dist/Server/Services/ProjectService.js +231 -0
  542. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  543. package/build/dist/Server/Services/RumSessionPinService.js +198 -16
  544. package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
  545. package/build/dist/Server/Services/StatusPagePrivateUserService.js +7 -3
  546. package/build/dist/Server/Services/StatusPagePrivateUserService.js.map +1 -1
  547. package/build/dist/Server/Services/StatusPageService.js +71 -0
  548. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  549. package/build/dist/Server/Services/TeamMemberService.js +10 -0
  550. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  551. package/build/dist/Server/Services/TeamPermissionService.js +89 -27
  552. package/build/dist/Server/Services/TeamPermissionService.js.map +1 -1
  553. package/build/dist/Server/Services/TeamService.js +9 -0
  554. package/build/dist/Server/Services/TeamService.js.map +1 -1
  555. package/build/dist/Server/Services/ThreatIntelFeedService.js +5 -4
  556. package/build/dist/Server/Services/ThreatIntelFeedService.js.map +1 -1
  557. package/build/dist/Server/Services/UserService.js +10 -16
  558. package/build/dist/Server/Services/UserService.js.map +1 -1
  559. package/build/dist/Server/Types/Database/Permissions/BasePermission.js +9 -6
  560. package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
  561. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +10 -0
  562. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  563. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js +154 -14
  564. package/build/dist/Server/Types/Database/Permissions/EditionPermission.js.map +1 -1
  565. package/build/dist/Server/Types/Database/Permissions/Index.js +60 -6
  566. package/build/dist/Server/Types/Database/Permissions/Index.js.map +1 -1
  567. package/build/dist/Server/Types/Database/Permissions/TablePermission.js +24 -16
  568. package/build/dist/Server/Types/Database/Permissions/TablePermission.js.map +1 -1
  569. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js +17 -0
  570. package/build/dist/Server/Types/Database/Permissions/UpdatePermission.js.map +1 -1
  571. package/build/dist/Server/Types/Markdown.js +31 -0
  572. package/build/dist/Server/Types/Markdown.js.map +1 -1
  573. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js +7 -0
  574. package/build/dist/Server/Utils/AI/AIChatPrivacyFilter.js.map +1 -1
  575. package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js +7 -0
  576. package/build/dist/Server/Utils/AI/AIRunPrivacyFilter.js.map +1 -1
  577. package/build/dist/Server/Utils/CommunityEditionSsoReport.js +223 -0
  578. package/build/dist/Server/Utils/CommunityEditionSsoReport.js.map +1 -0
  579. package/build/dist/Server/Utils/EditionEnforcement.js +187 -0
  580. package/build/dist/Server/Utils/EditionEnforcement.js.map +1 -0
  581. package/build/dist/Server/Utils/FrontendEnvironment.js +31 -2
  582. package/build/dist/Server/Utils/FrontendEnvironment.js.map +1 -1
  583. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js +6 -193
  584. package/build/dist/Server/Utils/InstanceHealth/RedisHealth.js.map +1 -1
  585. package/build/dist/Server/Utils/MasterAdminPageAccess.js +39 -0
  586. package/build/dist/Server/Utils/MasterAdminPageAccess.js.map +1 -0
  587. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +96 -0
  588. package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -0
  589. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +8 -1
  590. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  591. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +3 -1
  592. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  593. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +449 -261
  594. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  595. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +3 -1
  596. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  597. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js +88 -0
  598. package/build/dist/Server/Utils/Monitor/MonitoringIntervalValidator.js.map +1 -0
  599. package/build/dist/Server/Utils/Realtime.js +126 -37
  600. package/build/dist/Server/Utils/Realtime.js.map +1 -1
  601. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js +7 -25
  602. package/build/dist/Server/Utils/SecurityEvent/DetectionRuleEvaluator.js.map +1 -1
  603. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.js +127 -0
  604. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOpsSeverityRepair.js.map +1 -0
  605. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js +1 -1
  606. package/build/dist/Server/Utils/SecurityEvent/Sigma/SigmaClickhouseCompiler.js.map +1 -1
  607. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.js +3 -2
  608. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelFeedPoller.js.map +1 -1
  609. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.js +4 -8
  610. package/build/dist/Server/Utils/SecurityEvent/ThreatIntel/ThreatIntelMatcher.js.map +1 -1
  611. package/build/dist/Server/Utils/Workspace/Slack/Slack.js +331 -11
  612. package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
  613. package/build/dist/Types/API/Hostname.js +59 -11
  614. package/build/dist/Types/API/Hostname.js.map +1 -1
  615. package/build/dist/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.js +31 -1
  616. package/build/dist/Types/BaseDatabase/DatabaseCommonInteractionPropsUtil.js.map +1 -1
  617. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +19 -7
  618. package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
  619. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +19 -7
  620. package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
  621. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +13 -6
  622. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -1
  623. package/build/dist/Types/Database/CanonicalModelInstance.js +88 -0
  624. package/build/dist/Types/Database/CanonicalModelInstance.js.map +1 -0
  625. package/build/dist/Types/Database/TableColumn.js +19 -6
  626. package/build/dist/Types/Database/TableColumn.js.map +1 -1
  627. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js +28 -0
  628. package/build/dist/Types/EnterpriseLicense/EnterpriseLicensePeriods.js.map +1 -0
  629. package/build/dist/Types/IP/IPv6.js +6 -1
  630. package/build/dist/Types/IP/IPv6.js.map +1 -1
  631. package/build/dist/Types/Monitor/MonitorStep.js +5 -0
  632. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  633. package/build/dist/Types/Monitor/ProxmoxAlertTemplates.js +1 -1
  634. package/build/dist/Types/OnCallDutyPolicy/ScheduleTimeline.js +321 -0
  635. package/build/dist/Types/OnCallDutyPolicy/ScheduleTimeline.js.map +1 -0
  636. package/build/dist/Types/OnCallDutyPolicy/ScheduleTimelineLayout.js +372 -0
  637. package/build/dist/Types/OnCallDutyPolicy/ScheduleTimelineLayout.js.map +1 -0
  638. package/build/dist/Types/Realtime/EventName.js +10 -0
  639. package/build/dist/Types/Realtime/EventName.js.map +1 -1
  640. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js +11 -0
  641. package/build/dist/Types/SecurityEvent/DetectionFindingConstants.js.map +1 -1
  642. package/build/dist/Types/SecurityEvent/OcsfSeverity.js +30 -25
  643. package/build/dist/Types/SecurityEvent/OcsfSeverity.js.map +1 -1
  644. package/build/dist/Types/SecurityEvent/SigmaRule.js +47 -0
  645. package/build/dist/Types/SecurityEvent/SigmaRule.js.map +1 -1
  646. package/build/dist/Types/SecurityEvent/ThreatIntelConstants.js +14 -0
  647. package/build/dist/Types/SecurityEvent/ThreatIntelConstants.js.map +1 -1
  648. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js +32 -0
  649. package/build/dist/Types/StatusPage/UptimeDailyAggregate.js.map +1 -0
  650. package/build/dist/UI/Components/AttachmentList/EventAttachmentList.js +9 -4
  651. package/build/dist/UI/Components/AttachmentList/EventAttachmentList.js.map +1 -1
  652. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +10 -2
  653. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -1
  654. package/build/dist/UI/Components/Dropdown/Dropdown.js +5 -1
  655. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  656. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +425 -585
  657. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  658. package/build/dist/UI/Components/EditionLabel/LicenseManager.js +2 -0
  659. package/build/dist/UI/Components/EditionLabel/LicenseManager.js.map +1 -0
  660. package/build/dist/UI/Components/EventItem/EventItem.js +2 -2
  661. package/build/dist/UI/Components/EventItem/EventItem.js.map +1 -1
  662. package/build/dist/UI/Components/Forms/BasicForm.js +70 -64
  663. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  664. package/build/dist/UI/Components/Forms/CollapsibleFormSection.js +27 -0
  665. package/build/dist/UI/Components/Forms/CollapsibleFormSection.js.map +1 -0
  666. package/build/dist/UI/Components/Forms/Fields/FormField.js +10 -7
  667. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  668. package/build/dist/UI/Components/Forms/FormSummary.js +7 -0
  669. package/build/dist/UI/Components/Forms/FormSummary.js.map +1 -1
  670. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js +90 -17
  671. package/build/dist/UI/Components/Graphs/DayUptimeGraph.js.map +1 -1
  672. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js +0 -0
  673. package/build/dist/UI/Components/Markdown.tsx/MarkdownConverters.js.map +1 -1
  674. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +2 -1
  675. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  676. package/build/dist/UI/Components/Markdown.tsx/SessionAwareImage.js +84 -0
  677. package/build/dist/UI/Components/Markdown.tsx/SessionAwareImage.js.map +1 -0
  678. package/build/dist/UI/Components/ModelList/ModelList.js +4 -1
  679. package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
  680. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +14 -1
  681. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  682. package/build/dist/UI/Components/ModelTable/TableView.js +1 -1
  683. package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
  684. package/build/dist/UI/Components/MonitorGraphs/Uptime.js +42 -1
  685. package/build/dist/UI/Components/MonitorGraphs/Uptime.js.map +1 -1
  686. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +2 -2
  687. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  688. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +8 -1
  689. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  690. package/build/dist/UI/Components/Tooltip/Tooltip.js +2 -1
  691. package/build/dist/UI/Components/Tooltip/Tooltip.js.map +1 -1
  692. package/build/dist/UI/Utils/API/API.js +143 -19
  693. package/build/dist/UI/Utils/API/API.js.map +1 -1
  694. package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js +21 -7
  695. package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js.map +1 -1
  696. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +26 -9
  697. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  698. package/build/dist/UI/Utils/Navigation.js +8 -0
  699. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  700. package/build/dist/UI/Utils/OpenAuthenticatedUrl.js +92 -0
  701. package/build/dist/UI/Utils/OpenAuthenticatedUrl.js.map +1 -0
  702. package/build/dist/UI/Utils/Realtime.js +120 -15
  703. package/build/dist/UI/Utils/Realtime.js.map +1 -1
  704. package/build/dist/Utils/API.js +13 -5
  705. package/build/dist/Utils/API.js.map +1 -1
  706. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js +6 -2
  707. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseUsage.js.map +1 -1
  708. package/build/dist/Utils/HostAddressUtil.js +80 -0
  709. package/build/dist/Utils/HostAddressUtil.js.map +1 -0
  710. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js +127 -0
  711. package/build/dist/Utils/Monitor/MonitorDestinationUtil.js.map +1 -0
  712. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js +374 -0
  713. package/build/dist/Utils/Monitor/MonitoringIntervalUtil.js.map +1 -0
  714. package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js +123 -6
  715. package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js.map +1 -1
  716. package/build/dist/Utils/SecurityEvent/Sigma/SigmaRuleParser.js +4 -18
  717. package/build/dist/Utils/SecurityEvent/Sigma/SigmaRuleParser.js.map +1 -1
  718. package/build/dist/Utils/Slo/SloBurnRateTemplate.js +2 -2
  719. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js +148 -0
  720. package/build/dist/Utils/StatusPage/UptimeDailyAggregateUtil.js.map +1 -0
  721. package/jest.config.json +3 -1
  722. package/package.json +1 -1
  723. package/test-setup.sh +103 -34
  724. package/tsconfig.json +13 -0
  725. package/Server/API/EnterpriseLicenseAPI.ts +0 -925
  726. package/Server/API/TeamComplianceAPI.ts +0 -158
  727. package/Server/Middleware/SCIMAuthorization.ts +0 -141
  728. package/Server/Services/TeamComplianceService.ts +0 -733
  729. package/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.ts +0 -186
  730. package/Server/Utils/InstanceHealth/PostgresHealth.ts +0 -286
  731. package/Tests/App/AdminDashboard/EnterpriseLicenseInstanceActivity.test.tsx +0 -451
  732. package/Tests/App/Dashboard/AuditLogsTable.test.tsx +0 -598
  733. package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +0 -247
  734. package/Tests/App/Dashboard/SecurityEventsTableColumns.test.tsx +0 -382
  735. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +0 -1851
  736. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +0 -525
  737. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +0 -1845
  738. package/Tests/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.test.ts +0 -495
  739. package/Tests/Server/Utils/InstanceHealth/PostgresHealth.test.ts +0 -337
  740. package/Tests/Utils/EnterpriseLicenseSeats.test.ts +0 -576
  741. package/Tests/Utils/EnterpriseLicenseSync.test.ts +0 -568
  742. package/Utils/EnterpriseLicense/EnterpriseLicenseSeats.ts +0 -287
  743. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +0 -244
  744. package/build/dist/Server/API/EnterpriseLicenseAPI.js +0 -646
  745. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +0 -1
  746. package/build/dist/Server/API/TeamComplianceAPI.js +0 -111
  747. package/build/dist/Server/API/TeamComplianceAPI.js.map +0 -1
  748. package/build/dist/Server/Middleware/SCIMAuthorization.js +0 -124
  749. package/build/dist/Server/Middleware/SCIMAuthorization.js.map +0 -1
  750. package/build/dist/Server/Services/TeamComplianceService.js +0 -537
  751. package/build/dist/Server/Services/TeamComplianceService.js.map +0 -1
  752. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js +0 -145
  753. package/build/dist/Server/Utils/EnterpriseLicense/EnterpriseLicenseSeatUtil.js.map +0 -1
  754. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js +0 -194
  755. package/build/dist/Server/Utils/InstanceHealth/PostgresHealth.js.map +0 -1
  756. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js +0 -168
  757. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSeats.js.map +0 -1
  758. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +0 -174
  759. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +0 -1
@@ -0,0 +1,1580 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import {
11
+ act,
12
+ cleanup,
13
+ fireEvent,
14
+ render,
15
+ screen,
16
+ waitFor,
17
+ within,
18
+ } from "@testing-library/react";
19
+ import React, { ReactElement } from "react";
20
+ import { MemoryRouter } from "react-router-dom";
21
+ import getJestMockFunction, { MockFunction } from "../../MockType";
22
+
23
+ /*
24
+ * The schedule timeline page as a reader uses it: what loads, what each row
25
+ * says about right now, how the week / month / today controls move the
26
+ * window the server is asked for, what every filter and toggle does to the
27
+ * rows, and the empty, error and "answer arrived late" states.
28
+ *
29
+ * The API is stubbed at API.get and answers from a table keyed on the
30
+ * requested window, so each test states exactly what the server would send.
31
+ */
32
+
33
+ const getMock: MockFunction = getJestMockFunction();
34
+
35
+ jest.mock("../../../UI/Utils/API/API", () => {
36
+ return {
37
+ __esModule: true,
38
+ default: {
39
+ get: (...args: Array<any>) => {
40
+ return getMock(...args);
41
+ },
42
+ getFriendlyMessage: (error: unknown): string => {
43
+ if (error && typeof error === "object" && "message" in error) {
44
+ return String((error as { message: unknown }).message);
45
+ }
46
+
47
+ return "Something went wrong";
48
+ },
49
+ },
50
+ };
51
+ });
52
+
53
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
54
+ return {
55
+ __esModule: true,
56
+ default: {
57
+ getCommonHeaders: (): Record<string, string> => {
58
+ return {};
59
+ },
60
+ },
61
+ };
62
+ });
63
+
64
+ jest.mock("react-i18next", () => {
65
+ return {
66
+ useTranslation: () => {
67
+ return {
68
+ t: (key: string, options?: { defaultValue?: string }): string => {
69
+ return options?.defaultValue ?? key;
70
+ },
71
+ };
72
+ },
73
+ };
74
+ });
75
+
76
+ /*
77
+ * The real picker opens a modal with a searchable dropdown; the page only
78
+ * cares about the zone it hands back, so each test clicks straight to one.
79
+ */
80
+ jest.mock(
81
+ "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/OnCallScheduleLayer/TimezoneSelectButton",
82
+ () => {
83
+ return {
84
+ __esModule: true,
85
+ default: (props: {
86
+ value?: string;
87
+ onChange: (value: string | undefined) => void;
88
+ }) => {
89
+ return (
90
+ <span data-testid="timezone-picker" data-value={props.value}>
91
+ {["Asia/Tokyo", "America/New_York", "UTC"].map((zone: string) => {
92
+ return (
93
+ <button
94
+ key={zone}
95
+ type="button"
96
+ data-testid={`timezone-to-${zone}`}
97
+ onClick={() => {
98
+ props.onChange(zone);
99
+ }}
100
+ >
101
+ {zone}
102
+ </button>
103
+ );
104
+ })}
105
+ </span>
106
+ );
107
+ },
108
+ };
109
+ },
110
+ );
111
+
112
+ import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
113
+ import HTTPResponse from "../../../Types/API/HTTPResponse";
114
+ import URL from "../../../Types/API/URL";
115
+ import OneUptimeDate from "../../../Types/Date";
116
+ import { JSONObject } from "../../../Types/JSON";
117
+ import ObjectID from "../../../Types/ObjectID";
118
+ import {
119
+ ScheduleTimelineResponse,
120
+ ScheduleTimelineScheduleJson,
121
+ } from "../../../Types/OnCallDutyPolicy/ScheduleTimeline";
122
+ import ScheduleTimeline, {
123
+ GROUP_BY_TEAM_STORAGE_KEY,
124
+ VIEW_MODE_STORAGE_KEY,
125
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/ScheduleTimeline/ScheduleTimeline";
126
+ import TimelineRow from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/ScheduleTimeline/TimelineRow";
127
+ import TimelineGrid from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/ScheduleTimeline/TimelineGrid";
128
+ import TimelineModel, {
129
+ TimelineData,
130
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/ScheduleTimeline/TimelineModel";
131
+ import ScheduleTimelineLayout, {
132
+ TimelineRange,
133
+ TimelineViewMode,
134
+ } from "../../../Types/OnCallDutyPolicy/ScheduleTimelineLayout";
135
+
136
+ // Thursday. The week on screen is Mon 14 - Sun 20 September 2026 (UTC).
137
+ const NOW: Date = new Date("2026-09-17T12:00:00.000Z");
138
+ const WEEK_FROM: string = "2026-09-14T00:00:00.000Z";
139
+ const WEEK_TO: string = "2026-09-21T00:00:00.000Z";
140
+
141
+ const PAYMENTS: ScheduleTimelineScheduleJson = {
142
+ scheduleId: "s-pay",
143
+ scheduleName: "Payments primary",
144
+ scheduleTimezone: "Europe/Stockholm",
145
+ ownerTeamIds: ["t-pay"],
146
+ isCurrentUserOnRoster: false,
147
+ truncated: false,
148
+ shifts: [
149
+ {
150
+ shiftKey: "pay-1",
151
+ userId: "u-alice",
152
+ userName: "Alice Andersson",
153
+ start: "2026-09-10T09:00:00.000Z",
154
+ end: "2026-09-17T09:00:00.000Z",
155
+ layerId: "l-1",
156
+ layerName: "Weekly",
157
+ override: null,
158
+ },
159
+ {
160
+ shiftKey: "pay-2",
161
+ userId: "u-bob",
162
+ userName: "Bob Berg",
163
+ start: "2026-09-17T09:00:00.000Z",
164
+ end: "2026-09-24T09:00:00.000Z",
165
+ layerId: "l-1",
166
+ layerName: "Weekly",
167
+ override: null,
168
+ },
169
+ ],
170
+ };
171
+
172
+ // Uncovered from Wednesday 00:00 to Friday 00:00, so nobody is on call now.
173
+ const SRE: ScheduleTimelineScheduleJson = {
174
+ scheduleId: "s-sre",
175
+ scheduleName: "SRE primary",
176
+ scheduleTimezone: null,
177
+ ownerTeamIds: ["t-sre", "t-pay"],
178
+ isCurrentUserOnRoster: true,
179
+ truncated: false,
180
+ shifts: [
181
+ {
182
+ shiftKey: "sre-1",
183
+ userId: "u-carol",
184
+ userName: "Carol Chen",
185
+ start: "2026-09-14T00:00:00.000Z",
186
+ end: "2026-09-16T00:00:00.000Z",
187
+ layerId: null,
188
+ layerName: null,
189
+ override: null,
190
+ },
191
+ {
192
+ shiftKey: "sre-2",
193
+ userId: "u-dan",
194
+ userName: "Dan Diaz",
195
+ start: "2026-09-18T00:00:00.000Z",
196
+ end: "2026-09-21T00:00:00.000Z",
197
+ layerId: null,
198
+ layerName: null,
199
+ override: {
200
+ originalUserId: "u-carol",
201
+ originalUserName: "Carol Chen",
202
+ overrideStartsAt: "2026-09-18T00:00:00.000Z",
203
+ overrideEndsAt: "2026-09-21T00:00:00.000Z",
204
+ onCallDutyPolicyId: null,
205
+ },
206
+ },
207
+ ],
208
+ };
209
+
210
+ const LEGACY: ScheduleTimelineScheduleJson = {
211
+ scheduleId: "s-legacy",
212
+ scheduleName: "Legacy pager",
213
+ scheduleTimezone: null,
214
+ ownerTeamIds: [],
215
+ isCurrentUserOnRoster: false,
216
+ truncated: false,
217
+ shifts: [],
218
+ };
219
+
220
+ function timeline(
221
+ overrides?: Partial<ScheduleTimelineResponse>,
222
+ ): ScheduleTimelineResponse {
223
+ return {
224
+ from: WEEK_FROM,
225
+ to: WEEK_TO,
226
+ generatedAt: NOW.toISOString(),
227
+ truncated: false,
228
+ totalScheduleCount: 3,
229
+ schedulesTruncated: false,
230
+ schedules: [PAYMENTS, SRE, LEGACY],
231
+ teams: [
232
+ { teamId: "t-pay", teamName: "Payments", isCurrentUserMember: false },
233
+ { teamId: "t-sre", teamName: "SRE", isCurrentUserMember: true },
234
+ ],
235
+ ...(overrides || {}),
236
+ };
237
+ }
238
+
239
+ function ok(body: ScheduleTimelineResponse): HTTPResponse<JSONObject> {
240
+ return new HTTPResponse<JSONObject>(
241
+ 200,
242
+ JSON.parse(JSON.stringify(body)) as JSONObject,
243
+ {},
244
+ );
245
+ }
246
+
247
+ interface RequestedWindow {
248
+ from: string;
249
+ to: string;
250
+ teamId: string | null;
251
+ }
252
+
253
+ function requested(call: number): RequestedWindow {
254
+ const options: { url: URL } = getMock.mock.calls[call]?.[0] as { url: URL };
255
+ const parsed: globalThis.URL = new globalThis.URL(options.url.toString());
256
+
257
+ return {
258
+ from: parsed.searchParams.get("from") || "",
259
+ to: parsed.searchParams.get("to") || "",
260
+ teamId: parsed.searchParams.get("teamId"),
261
+ };
262
+ }
263
+
264
+ function lastRequested(): RequestedWindow {
265
+ return requested(getMock.mock.calls.length - 1);
266
+ }
267
+
268
+ /*
269
+ * Answers every request with `body`, but with `from` / `to` set to the
270
+ * window that was asked for, as the server would.
271
+ */
272
+ function answerWith(body: ScheduleTimelineResponse): void {
273
+ getMock.mockImplementation(async (options: { url: URL }) => {
274
+ const parsed: globalThis.URL = new globalThis.URL(options.url.toString());
275
+
276
+ return ok({
277
+ ...body,
278
+ from: parsed.searchParams.get("from") || body.from,
279
+ to: parsed.searchParams.get("to") || body.to,
280
+ });
281
+ });
282
+ }
283
+
284
+ function renderTimeline(
285
+ props?: React.ComponentProps<typeof ScheduleTimeline>,
286
+ ): void {
287
+ render(
288
+ <MemoryRouter>
289
+ <ScheduleTimeline {...(props || {})} />
290
+ </MemoryRouter>,
291
+ );
292
+ }
293
+
294
+ function rowIds(): Array<string> {
295
+ return screen
296
+ .queryAllByTestId("timeline-schedule-row")
297
+ .map((row: HTMLElement) => {
298
+ return row.getAttribute("data-schedule-id") || "";
299
+ });
300
+ }
301
+
302
+ function row(scheduleId: string, index: number = 0): HTMLElement {
303
+ const rows: Array<HTMLElement> = screen
304
+ .queryAllByTestId("timeline-schedule-row")
305
+ .filter((element: HTMLElement) => {
306
+ return element.getAttribute("data-schedule-id") === scheduleId;
307
+ });
308
+
309
+ const found: HTMLElement | undefined = rows[index];
310
+
311
+ if (!found) {
312
+ throw new Error(`No row for ${scheduleId}`);
313
+ }
314
+
315
+ return found;
316
+ }
317
+
318
+ async function loaded(): Promise<void> {
319
+ await waitFor(() => {
320
+ expect(screen.queryAllByTestId("timeline-schedule-row").length).toBe(4);
321
+ });
322
+ }
323
+
324
+ beforeEach(() => {
325
+ getMock.mockReset();
326
+ window.localStorage.clear();
327
+
328
+ jest.spyOn(OneUptimeDate, "getCurrentDate").mockReturnValue(NOW);
329
+ jest
330
+ .spyOn(OneUptimeDate, "getCurrentTimezone")
331
+ .mockReturnValue(
332
+ "UTC" as ReturnType<typeof OneUptimeDate.getCurrentTimezone>,
333
+ );
334
+ jest
335
+ .spyOn(OneUptimeDate, "getUserPrefers12HourFormat")
336
+ .mockReturnValue(false);
337
+
338
+ answerWith(timeline());
339
+ });
340
+
341
+ afterEach(() => {
342
+ cleanup();
343
+ jest.restoreAllMocks();
344
+ });
345
+
346
+ describe("first load", () => {
347
+ test("asks for the current ISO week in the viewer's zone", async () => {
348
+ renderTimeline();
349
+ await loaded();
350
+
351
+ expect(getMock).toHaveBeenCalledTimes(1);
352
+ expect(requested(0)).toEqual({
353
+ from: WEEK_FROM,
354
+ to: WEEK_TO,
355
+ teamId: null,
356
+ });
357
+ expect(screen.getByTestId("timeline-range-label")).toHaveTextContent(
358
+ "September 14 – 20, 2026",
359
+ );
360
+ });
361
+
362
+ test("shows skeleton rows until the answer arrives", async () => {
363
+ let resolve: (value: HTTPResponse<JSONObject>) => void = () => {};
364
+
365
+ getMock.mockImplementation(() => {
366
+ return new Promise<HTTPResponse<JSONObject>>(
367
+ (done: (value: HTTPResponse<JSONObject>) => void) => {
368
+ resolve = done;
369
+ },
370
+ );
371
+ });
372
+
373
+ renderTimeline();
374
+
375
+ expect(screen.getByTestId("timeline-skeleton")).toBeInTheDocument();
376
+ expect(screen.queryByTestId("timeline-summary")).not.toBeInTheDocument();
377
+
378
+ await act(async () => {
379
+ resolve(ok(timeline()));
380
+ });
381
+
382
+ await loaded();
383
+ expect(screen.queryByTestId("timeline-skeleton")).not.toBeInTheDocument();
384
+ });
385
+
386
+ test("seven day columns, today marked", async () => {
387
+ renderTimeline();
388
+ await loaded();
389
+
390
+ const headers: Array<HTMLElement> = screen.getAllByTestId(
391
+ "timeline-day-header",
392
+ );
393
+
394
+ expect(headers).toHaveLength(7);
395
+ expect(
396
+ headers.map((header: HTMLElement) => {
397
+ return header.getAttribute("data-today");
398
+ }),
399
+ ).toEqual(["false", "false", "false", "true", "false", "false", "false"]);
400
+ expect(screen.getByTestId("timeline-now-line")).toBeInTheDocument();
401
+ });
402
+
403
+ test("rows are grouped by owner team; a shared schedule appears under each team", async () => {
404
+ renderTimeline();
405
+ await loaded();
406
+
407
+ expect(
408
+ screen
409
+ .getAllByTestId("timeline-group-header")
410
+ .map((header: HTMLElement) => {
411
+ return header.textContent;
412
+ }),
413
+ ).toEqual(["Payments2", "SRE1Your team", "No owner team1"]);
414
+
415
+ expect(rowIds()).toEqual(["s-pay", "s-sre", "s-sre", "s-legacy"]);
416
+ });
417
+
418
+ test("schedule names link to the schedule's page", async () => {
419
+ renderTimeline();
420
+ await loaded();
421
+
422
+ const link: HTMLElement = within(row("s-pay")).getByRole("link", {
423
+ name: "Payments primary",
424
+ });
425
+
426
+ expect(link.getAttribute("href")).toContain(
427
+ "/on-call-duty/schedules/s-pay",
428
+ );
429
+ });
430
+
431
+ test("each row says who is on call now, or that nobody is", async () => {
432
+ renderTimeline();
433
+ await loaded();
434
+
435
+ expect(
436
+ within(row("s-pay")).getByTestId("timeline-on-call-now"),
437
+ ).toHaveTextContent("Bob Bergon call now");
438
+ expect(
439
+ within(row("s-sre")).getByTestId("timeline-uncovered-now"),
440
+ ).toHaveTextContent("No one on call now");
441
+ expect(
442
+ within(row("s-legacy")).getByTestId("timeline-uncovered-now"),
443
+ ).toBeInTheDocument();
444
+ });
445
+
446
+ test("the caller's own schedules are tagged", async () => {
447
+ renderTimeline();
448
+ await loaded();
449
+
450
+ expect(within(row("s-sre")).getByText("You")).toBeInTheDocument();
451
+ expect(within(row("s-pay")).queryByText("You")).not.toBeInTheDocument();
452
+ });
453
+
454
+ test("bars for shifts, hatched gaps, and the overridden person underneath", async () => {
455
+ renderTimeline();
456
+ await loaded();
457
+
458
+ const payBars: Array<HTMLElement> = within(row("s-pay")).getAllByTestId(
459
+ "timeline-shift-bar",
460
+ );
461
+
462
+ expect(
463
+ payBars.map((bar: HTMLElement) => {
464
+ return [
465
+ bar.getAttribute("data-user-id"),
466
+ bar.getAttribute("data-active"),
467
+ ];
468
+ }),
469
+ ).toEqual([
470
+ ["u-alice", "false"],
471
+ ["u-bob", "true"],
472
+ ]);
473
+ expect(within(row("s-pay")).queryAllByTestId("timeline-gap")).toHaveLength(
474
+ 0,
475
+ );
476
+
477
+ const sreRow: HTMLElement = row("s-sre");
478
+
479
+ expect(within(sreRow).getAllByTestId("timeline-gap")).toHaveLength(1);
480
+ expect(
481
+ within(sreRow)
482
+ .getAllByTestId("timeline-gap")[0]
483
+ ?.getAttribute("aria-label"),
484
+ ).toBe("No one on call, Wed, Sep 16, 00:00 → Fri, Sep 18, 00:00");
485
+
486
+ const overrideBar: HTMLElement | undefined = within(sreRow)
487
+ .getAllByTestId("timeline-shift-bar")
488
+ .find((bar: HTMLElement) => {
489
+ return bar.getAttribute("data-override") === "true";
490
+ });
491
+
492
+ expect(overrideBar).toHaveTextContent("⇄ Dan Diaz");
493
+ expect(overrideBar?.getAttribute("aria-label")).toContain(
494
+ "covering for Carol Chen",
495
+ );
496
+
497
+ const overridden: HTMLElement = within(sreRow).getByTestId(
498
+ "timeline-overridden-segment",
499
+ );
500
+
501
+ expect(overridden).toHaveTextContent("Carol Chen (overridden)");
502
+
503
+ // Nobody at all: the whole week is one gap.
504
+ expect(within(row("s-legacy")).getAllByTestId("timeline-gap")).toHaveLength(
505
+ 1,
506
+ );
507
+ });
508
+
509
+ test("the summary counts schedules, coverage now, and gaps", async () => {
510
+ renderTimeline();
511
+ await loaded();
512
+
513
+ const summary: HTMLElement = screen.getByTestId("timeline-summary");
514
+
515
+ expect(summary).toHaveTextContent("3schedules");
516
+ expect(
517
+ screen.getByTestId("timeline-summary-covered-now"),
518
+ ).toHaveTextContent("1covered now");
519
+ expect(
520
+ screen.getByTestId("timeline-summary-uncovered-now"),
521
+ ).toHaveTextContent("2with no one on call now");
522
+ expect(screen.getByTestId("timeline-summary-gaps")).toHaveTextContent(
523
+ "2with coverage gaps this week",
524
+ );
525
+ });
526
+
527
+ test("the legend lists everyone on call this week, heaviest load first", async () => {
528
+ renderTimeline();
529
+ await loaded();
530
+
531
+ const people: Array<string> = screen
532
+ .getAllByTestId("timeline-person")
533
+ .map((chip: HTMLElement) => {
534
+ return chip.getAttribute("data-user-id") || "";
535
+ });
536
+
537
+ // Bob 3d15h, Alice 3d9h, Dan 3d, Carol 2d.
538
+ expect(people).toEqual(["u-bob", "u-alice", "u-dan", "u-carol"]);
539
+ expect(screen.getByTestId("timeline-legend")).toHaveTextContent(
540
+ "Times in UTC.",
541
+ );
542
+ });
543
+ });
544
+
545
+ describe("navigation", () => {
546
+ test("next, previous and today move the requested window a week at a time", async () => {
547
+ renderTimeline();
548
+ await loaded();
549
+
550
+ fireEvent.click(screen.getByTestId("timeline-next-button"));
551
+
552
+ await waitFor(() => {
553
+ expect(lastRequested().from).toBe("2026-09-21T00:00:00.000Z");
554
+ });
555
+ expect(lastRequested().to).toBe("2026-09-28T00:00:00.000Z");
556
+ expect(screen.getByTestId("timeline-range-label")).toHaveTextContent(
557
+ "September 21 – 27, 2026",
558
+ );
559
+
560
+ // Not this week any more: no red line, no "now" statements.
561
+ await waitFor(() => {
562
+ expect(screen.queryByTestId("timeline-now-line")).not.toBeInTheDocument();
563
+ });
564
+ expect(
565
+ screen.queryByTestId("timeline-summary-covered-now"),
566
+ ).not.toBeInTheDocument();
567
+
568
+ fireEvent.click(screen.getByTestId("timeline-previous-button"));
569
+ fireEvent.click(screen.getByTestId("timeline-previous-button"));
570
+
571
+ await waitFor(() => {
572
+ expect(lastRequested().from).toBe("2026-09-07T00:00:00.000Z");
573
+ });
574
+
575
+ fireEvent.click(screen.getByTestId("timeline-today-button"));
576
+
577
+ await waitFor(() => {
578
+ expect(lastRequested().from).toBe(WEEK_FROM);
579
+ });
580
+ });
581
+
582
+ test("outside the current week the rows describe the period instead of 'now'", async () => {
583
+ renderTimeline();
584
+ await loaded();
585
+
586
+ fireEvent.click(screen.getByTestId("timeline-next-button"));
587
+
588
+ await waitFor(() => {
589
+ expect(
590
+ within(row("s-legacy")).getByText("No one on call this week"),
591
+ ).toBeInTheDocument();
592
+ });
593
+ });
594
+
595
+ test("the month view asks for the calendar month and is remembered", async () => {
596
+ renderTimeline();
597
+ await loaded();
598
+
599
+ fireEvent.click(screen.getByTestId("timeline-mode-month"));
600
+
601
+ await waitFor(() => {
602
+ expect(lastRequested()).toEqual({
603
+ from: "2026-09-01T00:00:00.000Z",
604
+ to: "2026-10-01T00:00:00.000Z",
605
+ teamId: null,
606
+ });
607
+ });
608
+
609
+ expect(screen.getByTestId("timeline-range-label")).toHaveTextContent(
610
+ "September 2026",
611
+ );
612
+ expect(screen.getByTestId("timeline-mode-month")).toHaveAttribute(
613
+ "aria-checked",
614
+ "true",
615
+ );
616
+
617
+ await waitFor(() => {
618
+ expect(screen.getAllByTestId("timeline-day-header")).toHaveLength(30);
619
+ });
620
+
621
+ expect(window.localStorage.getItem(VIEW_MODE_STORAGE_KEY)).toBe("month");
622
+ });
623
+
624
+ test("a remembered month view is used on the next visit", async () => {
625
+ window.localStorage.setItem(VIEW_MODE_STORAGE_KEY, "month");
626
+
627
+ renderTimeline();
628
+
629
+ await waitFor(() => {
630
+ expect(requested(0).from).toBe("2026-09-01T00:00:00.000Z");
631
+ });
632
+ });
633
+
634
+ test("the back button stops where the server stops computing", async () => {
635
+ renderTimeline();
636
+ await loaded();
637
+
638
+ fireEvent.click(screen.getByTestId("timeline-mode-month"));
639
+
640
+ const back: HTMLElement = screen.getByTestId("timeline-previous-button");
641
+
642
+ /*
643
+ * 180 days before 17 Sep 2026 is 21 Mar 2026: March is reachable,
644
+ * February is not.
645
+ */
646
+ for (let step: number = 0; step < 6; step++) {
647
+ expect(back).not.toBeDisabled();
648
+ fireEvent.click(back);
649
+ }
650
+
651
+ await waitFor(() => {
652
+ expect(screen.getByTestId("timeline-range-label")).toHaveTextContent(
653
+ "March 2026",
654
+ );
655
+ });
656
+ expect(back).toBeDisabled();
657
+ expect(screen.getByTestId("timeline-next-button")).not.toBeDisabled();
658
+ });
659
+
660
+ test("a slower answer for a window the reader already left is ignored", async () => {
661
+ const pending: Array<(value: HTTPResponse<JSONObject>) => void> = [];
662
+
663
+ getMock.mockImplementation(() => {
664
+ return new Promise<HTTPResponse<JSONObject>>(
665
+ (done: (value: HTTPResponse<JSONObject>) => void) => {
666
+ pending.push(done);
667
+ },
668
+ );
669
+ });
670
+
671
+ renderTimeline();
672
+
673
+ await waitFor(() => {
674
+ expect(pending).toHaveLength(1);
675
+ });
676
+
677
+ fireEvent.click(screen.getByTestId("timeline-next-button"));
678
+
679
+ await waitFor(() => {
680
+ expect(pending).toHaveLength(2);
681
+ });
682
+
683
+ // The next week answers first...
684
+ await act(async () => {
685
+ pending[1]!(
686
+ ok(
687
+ timeline({
688
+ from: "2026-09-21T00:00:00.000Z",
689
+ to: "2026-09-28T00:00:00.000Z",
690
+ schedules: [LEGACY],
691
+ teams: [],
692
+ }),
693
+ ),
694
+ );
695
+ });
696
+
697
+ // ...then the stale current week.
698
+ await act(async () => {
699
+ pending[0]!(ok(timeline()));
700
+ });
701
+
702
+ await waitFor(() => {
703
+ expect(rowIds()).toEqual(["s-legacy"]);
704
+ });
705
+ });
706
+ });
707
+
708
+ describe("filters", () => {
709
+ test("search matches schedules, teams and people", async () => {
710
+ renderTimeline();
711
+ await loaded();
712
+
713
+ fireEvent.change(screen.getByTestId("timeline-search"), {
714
+ target: { value: "dan" },
715
+ });
716
+
717
+ expect(rowIds()).toEqual(["s-sre", "s-sre"]);
718
+
719
+ fireEvent.change(screen.getByTestId("timeline-search"), {
720
+ target: { value: "legacy" },
721
+ });
722
+
723
+ expect(rowIds()).toEqual(["s-legacy"]);
724
+ });
725
+
726
+ test("the team picker offers My teams and every owner team", async () => {
727
+ renderTimeline();
728
+ await loaded();
729
+
730
+ const select: HTMLSelectElement = screen.getByTestId(
731
+ "timeline-team-filter",
732
+ ) as HTMLSelectElement;
733
+
734
+ expect(
735
+ Array.from(select.options).map((option: HTMLOptionElement) => {
736
+ return option.textContent;
737
+ }),
738
+ ).toEqual(["All teams", "My teams", "Payments", "SRE"]);
739
+
740
+ fireEvent.change(select, { target: { value: "t-sre" } });
741
+
742
+ // Only the chosen team's group, even though SRE primary is co-owned.
743
+ expect(rowIds()).toEqual(["s-sre"]);
744
+ expect(
745
+ screen
746
+ .getAllByTestId("timeline-group-header")
747
+ .map((header: HTMLElement) => {
748
+ return header.textContent;
749
+ }),
750
+ ).toEqual(["SRE1Your team"]);
751
+
752
+ fireEvent.change(select, { target: { value: "t-pay" } });
753
+
754
+ expect(rowIds()).toEqual(["s-pay", "s-sre"]);
755
+ });
756
+
757
+ test("My teams keeps the schedules a team the caller is in owns", async () => {
758
+ renderTimeline();
759
+ await loaded();
760
+
761
+ fireEvent.change(screen.getByTestId("timeline-team-filter"), {
762
+ target: { value: "__my_teams__" },
763
+ });
764
+
765
+ expect(rowIds()).toEqual(["s-sre"]);
766
+ expect(
767
+ screen
768
+ .getAllByTestId("timeline-group-header")
769
+ .map((header: HTMLElement) => {
770
+ return header.textContent;
771
+ }),
772
+ ).toEqual(["SRE1Your team"]);
773
+ });
774
+
775
+ test("Schedules I'm on", async () => {
776
+ renderTimeline();
777
+ await loaded();
778
+
779
+ const toggle: HTMLElement = screen.getByTestId("timeline-only-mine");
780
+
781
+ fireEvent.click(toggle);
782
+
783
+ expect(toggle).toHaveAttribute("aria-pressed", "true");
784
+ expect(new Set(rowIds())).toEqual(new Set(["s-sre"]));
785
+
786
+ fireEvent.click(toggle);
787
+
788
+ expect(rowIds()).toHaveLength(4);
789
+ });
790
+
791
+ test("the 'no one on call now' count filters to those schedules and back", async () => {
792
+ renderTimeline();
793
+ await loaded();
794
+
795
+ const chip: HTMLElement = screen.getByTestId(
796
+ "timeline-summary-uncovered-now",
797
+ );
798
+
799
+ fireEvent.click(chip);
800
+
801
+ expect(chip).toHaveAttribute("aria-pressed", "true");
802
+ expect(rowIds()).toEqual(["s-sre", "s-sre", "s-legacy"]);
803
+
804
+ // The counts do not move under the reader's feet.
805
+ expect(
806
+ screen.getByTestId("timeline-summary-covered-now"),
807
+ ).toHaveTextContent("1covered now");
808
+
809
+ fireEvent.click(chip);
810
+
811
+ expect(rowIds()).toHaveLength(4);
812
+ });
813
+
814
+ test("the 'coverage gaps' count filters too", async () => {
815
+ renderTimeline();
816
+ await loaded();
817
+
818
+ fireEvent.click(screen.getByTestId("timeline-summary-gaps"));
819
+
820
+ expect(new Set(rowIds())).toEqual(new Set(["s-sre", "s-legacy"]));
821
+ });
822
+
823
+ test("no match: a message and a way back", async () => {
824
+ renderTimeline();
825
+ await loaded();
826
+
827
+ fireEvent.change(screen.getByTestId("timeline-search"), {
828
+ target: { value: "zzz" },
829
+ });
830
+
831
+ expect(screen.getByTestId("timeline-no-matches")).toHaveTextContent(
832
+ "No schedules match these filters",
833
+ );
834
+
835
+ fireEvent.click(
836
+ within(screen.getByTestId("timeline-no-matches")).getByText(
837
+ "Clear filters",
838
+ ),
839
+ );
840
+
841
+ expect(rowIds()).toHaveLength(4);
842
+ expect(screen.getByTestId("timeline-search")).toHaveValue("");
843
+ });
844
+
845
+ test("Clear filters resets every filter", async () => {
846
+ renderTimeline();
847
+ await loaded();
848
+
849
+ expect(
850
+ screen.queryByTestId("timeline-clear-filters"),
851
+ ).not.toBeInTheDocument();
852
+
853
+ fireEvent.click(screen.getByTestId("timeline-only-mine"));
854
+ fireEvent.change(screen.getByTestId("timeline-search"), {
855
+ target: { value: "sre" },
856
+ });
857
+
858
+ fireEvent.click(screen.getByTestId("timeline-clear-filters"));
859
+
860
+ expect(rowIds()).toHaveLength(4);
861
+ expect(screen.getByTestId("timeline-only-mine")).toHaveAttribute(
862
+ "aria-pressed",
863
+ "false",
864
+ );
865
+ });
866
+
867
+ test("grouping can be turned off, and that is remembered", async () => {
868
+ renderTimeline();
869
+ await loaded();
870
+
871
+ fireEvent.click(screen.getByTestId("timeline-group-by-team"));
872
+
873
+ expect(screen.queryAllByTestId("timeline-group-header")).toHaveLength(0);
874
+ expect(rowIds()).toEqual(["s-pay", "s-sre", "s-legacy"]);
875
+ expect(
876
+ JSON.parse(
877
+ window.localStorage.getItem(GROUP_BY_TEAM_STORAGE_KEY) || "null",
878
+ ),
879
+ ).toBe(false);
880
+ });
881
+
882
+ test("a group collapses and expands", async () => {
883
+ renderTimeline();
884
+ await loaded();
885
+
886
+ const header: HTMLElement = screen.getAllByTestId(
887
+ "timeline-group-header",
888
+ )[0] as HTMLElement;
889
+ const toggle: HTMLElement = within(header).getByRole("button");
890
+
891
+ fireEvent.click(toggle);
892
+
893
+ expect(toggle).toHaveAttribute("aria-expanded", "false");
894
+ expect(rowIds()).toEqual(["s-sre", "s-legacy"]);
895
+
896
+ fireEvent.click(toggle);
897
+
898
+ expect(rowIds()).toHaveLength(4);
899
+ });
900
+ });
901
+
902
+ describe("highlighting a person", () => {
903
+ test("from the legend: their bars are marked everywhere, and it toggles off", async () => {
904
+ renderTimeline();
905
+ await loaded();
906
+
907
+ const chip: HTMLElement = screen
908
+ .getAllByTestId("timeline-person")
909
+ .find((element: HTMLElement) => {
910
+ return element.getAttribute("data-user-id") === "u-dan";
911
+ }) as HTMLElement;
912
+
913
+ fireEvent.click(chip);
914
+
915
+ expect(chip).toHaveAttribute("aria-pressed", "true");
916
+
917
+ const pressed: Array<string> = screen
918
+ .getAllByTestId("timeline-shift-bar")
919
+ .filter((bar: HTMLElement) => {
920
+ return bar.getAttribute("aria-pressed") === "true";
921
+ })
922
+ .map((bar: HTMLElement) => {
923
+ return bar.getAttribute("data-user-id") || "";
924
+ });
925
+
926
+ // Dan's one shift, drawn under both groups SRE primary appears in.
927
+ expect(pressed).toEqual(["u-dan", "u-dan"]);
928
+
929
+ fireEvent.click(screen.getByTestId("timeline-clear-highlight"));
930
+
931
+ expect(chip).toHaveAttribute("aria-pressed", "false");
932
+ });
933
+
934
+ test("from a bar", async () => {
935
+ renderTimeline();
936
+ await loaded();
937
+
938
+ const bar: HTMLElement = within(row("s-pay"))
939
+ .getAllByTestId("timeline-shift-bar")
940
+ .find((element: HTMLElement) => {
941
+ return element.getAttribute("data-user-id") === "u-bob";
942
+ }) as HTMLElement;
943
+
944
+ fireEvent.click(bar);
945
+
946
+ expect(bar).toHaveAttribute("aria-pressed", "true");
947
+ expect(
948
+ screen.getAllByTestId("timeline-person").find((element: HTMLElement) => {
949
+ return element.getAttribute("data-user-id") === "u-bob";
950
+ }),
951
+ ).toHaveAttribute("aria-pressed", "true");
952
+
953
+ fireEvent.click(bar);
954
+
955
+ expect(bar).toHaveAttribute("aria-pressed", "false");
956
+ });
957
+ });
958
+
959
+ describe("locked to a team", () => {
960
+ test("asks for that team, and hides the team picker and grouping", async () => {
961
+ const teamId: ObjectID = ObjectID.generate();
962
+
963
+ renderTimeline({ teamId });
964
+
965
+ // Ungrouped: each schedule once.
966
+ await waitFor(() => {
967
+ expect(rowIds()).toEqual(["s-pay", "s-sre", "s-legacy"]);
968
+ });
969
+
970
+ expect(requested(0).teamId).toBe(teamId.toString());
971
+ expect(
972
+ screen.queryByTestId("timeline-team-filter"),
973
+ ).not.toBeInTheDocument();
974
+ expect(
975
+ screen.queryByTestId("timeline-group-by-team"),
976
+ ).not.toBeInTheDocument();
977
+ expect(screen.queryAllByTestId("timeline-group-header")).toHaveLength(0);
978
+ expect(screen.getByText("On-Call Schedules")).toBeInTheDocument();
979
+ });
980
+
981
+ test("a team with no schedules is told how to add one", async () => {
982
+ answerWith(timeline({ schedules: [], teams: [], totalScheduleCount: 0 }));
983
+
984
+ renderTimeline({ teamId: ObjectID.generate() });
985
+
986
+ await waitFor(() => {
987
+ expect(
988
+ screen.getByText("This team does not own any on-call schedules yet"),
989
+ ).toBeInTheDocument();
990
+ });
991
+ });
992
+ });
993
+
994
+ describe("empty, error and partial answers", () => {
995
+ test("a project without schedules gets an empty state with a way forward", async () => {
996
+ answerWith(timeline({ schedules: [], teams: [], totalScheduleCount: 0 }));
997
+
998
+ renderTimeline();
999
+
1000
+ await waitFor(() => {
1001
+ expect(screen.getByText("No on-call schedules yet")).toBeInTheDocument();
1002
+ });
1003
+ expect(screen.getByText("Go to On-Call Schedules")).toBeInTheDocument();
1004
+ expect(screen.queryByTestId("timeline-summary")).not.toBeInTheDocument();
1005
+ });
1006
+
1007
+ test("an error shows the server's message, and retry asks again", async () => {
1008
+ getMock.mockResolvedValueOnce(
1009
+ new HTTPErrorResponse(
1010
+ 422,
1011
+ { message: "You do not have permission to read schedules." },
1012
+ {},
1013
+ ),
1014
+ );
1015
+
1016
+ renderTimeline();
1017
+
1018
+ await waitFor(() => {
1019
+ expect(
1020
+ screen.getByText("You do not have permission to read schedules."),
1021
+ ).toBeInTheDocument();
1022
+ });
1023
+
1024
+ fireEvent.click(screen.getByTestId("refresh-button"));
1025
+
1026
+ await loaded();
1027
+ expect(getMock).toHaveBeenCalledTimes(2);
1028
+ });
1029
+
1030
+ test("a capped schedule list says so", async () => {
1031
+ answerWith(timeline({ totalScheduleCount: 412, schedulesTruncated: true }));
1032
+
1033
+ renderTimeline();
1034
+ await loaded();
1035
+
1036
+ expect(
1037
+ screen.getByTestId("timeline-schedules-truncated"),
1038
+ ).toHaveTextContent("Showing the first 3 of 412 schedules");
1039
+ });
1040
+
1041
+ test("an incomplete rotation says so", async () => {
1042
+ answerWith(timeline({ truncated: true }));
1043
+
1044
+ renderTimeline();
1045
+ await loaded();
1046
+
1047
+ expect(screen.getByTestId("timeline-engine-truncated")).toBeInTheDocument();
1048
+ });
1049
+
1050
+ test("a partly served window is shaded and explained, not drawn as 'nobody on call'", async () => {
1051
+ getMock.mockResolvedValue(
1052
+ ok(timeline({ from: "2026-09-16T00:00:00.000Z", to: WEEK_TO })),
1053
+ );
1054
+
1055
+ renderTimeline();
1056
+ await loaded();
1057
+
1058
+ expect(screen.getByTestId("timeline-outside-range")).toBeInTheDocument();
1059
+ expect(screen.getAllByTestId("timeline-unavailable")).toHaveLength(1);
1060
+
1061
+ // Legacy has no shifts: its gap starts where the served window starts.
1062
+ expect(
1063
+ within(row("s-legacy"))
1064
+ .getByTestId("timeline-gap")
1065
+ .getAttribute("aria-label"),
1066
+ ).toBe("No one on call, Wed, Sep 16, 00:00 → Mon, Sep 21, 00:00");
1067
+ });
1068
+ });
1069
+
1070
+ // -- Behaviour found in review -------------------------------------------------
1071
+
1072
+ function pendingAnswers(): Array<(value: HTTPResponse<JSONObject>) => void> {
1073
+ const pending: Array<(value: HTTPResponse<JSONObject>) => void> = [];
1074
+
1075
+ getMock.mockImplementation(() => {
1076
+ return new Promise<HTTPResponse<JSONObject>>(
1077
+ (done: (value: HTTPResponse<JSONObject>) => void) => {
1078
+ pending.push(done);
1079
+ },
1080
+ );
1081
+ });
1082
+
1083
+ return pending;
1084
+ }
1085
+
1086
+ function dayKeys(): Array<string> {
1087
+ return screen
1088
+ .getAllByTestId("timeline-day-header")
1089
+ .map((header: HTMLElement) => {
1090
+ return header.getAttribute("title") || "";
1091
+ });
1092
+ }
1093
+
1094
+ describe("while the next range loads", () => {
1095
+ test("the previous week stays drawn, dimmed, against its own days", async () => {
1096
+ const pending: Array<(value: HTTPResponse<JSONObject>) => void> =
1097
+ pendingAnswers();
1098
+
1099
+ renderTimeline();
1100
+
1101
+ await waitFor(() => {
1102
+ expect(pending).toHaveLength(1);
1103
+ });
1104
+
1105
+ await act(async () => {
1106
+ pending[0]!(ok(timeline()));
1107
+ });
1108
+
1109
+ await loaded();
1110
+
1111
+ const rowsBefore: Array<string> = rowIds();
1112
+ const gapsBefore: number = screen.getAllByTestId("timeline-gap").length;
1113
+
1114
+ fireEvent.click(screen.getByTestId("timeline-next-button"));
1115
+
1116
+ await waitFor(() => {
1117
+ expect(pending).toHaveLength(2);
1118
+ });
1119
+
1120
+ // The label follows the request...
1121
+ expect(screen.getByTestId("timeline-range-label")).toHaveTextContent(
1122
+ "September 21 – 27, 2026",
1123
+ );
1124
+ expect(screen.getByTestId("timeline-refreshing")).toBeInTheDocument();
1125
+
1126
+ // ...the grid stays on the week its data belongs to, gaps and all.
1127
+ expect(dayKeys()[0]).toBe("2026-09-14");
1128
+ expect(rowIds()).toEqual(rowsBefore);
1129
+ expect(screen.getAllByTestId("timeline-gap")).toHaveLength(gapsBefore);
1130
+ expect(screen.getByTestId("timeline-now-line")).toBeInTheDocument();
1131
+ expect(
1132
+ screen.queryByTestId("timeline-unavailable"),
1133
+ ).not.toBeInTheDocument();
1134
+ expect(
1135
+ screen.queryByTestId("timeline-outside-range"),
1136
+ ).not.toBeInTheDocument();
1137
+
1138
+ await act(async () => {
1139
+ pending[1]!(
1140
+ ok(
1141
+ timeline({
1142
+ from: "2026-09-21T00:00:00.000Z",
1143
+ to: "2026-09-28T00:00:00.000Z",
1144
+ }),
1145
+ ),
1146
+ );
1147
+ });
1148
+
1149
+ await waitFor(() => {
1150
+ expect(dayKeys()[0]).toBe("2026-09-21");
1151
+ });
1152
+ expect(screen.queryByTestId("timeline-refreshing")).not.toBeInTheDocument();
1153
+ expect(screen.queryByTestId("timeline-now-line")).not.toBeInTheDocument();
1154
+ });
1155
+
1156
+ test("an empty team page stays on its empty state", async () => {
1157
+ const pending: Array<(value: HTTPResponse<JSONObject>) => void> =
1158
+ pendingAnswers();
1159
+
1160
+ renderTimeline({ teamId: ObjectID.generate() });
1161
+
1162
+ await waitFor(() => {
1163
+ expect(pending).toHaveLength(1);
1164
+ });
1165
+
1166
+ await act(async () => {
1167
+ pending[0]!(ok(timeline({ schedules: [], teams: [] })));
1168
+ });
1169
+
1170
+ await waitFor(() => {
1171
+ expect(
1172
+ screen.getByText("This team does not own any on-call schedules yet"),
1173
+ ).toBeInTheDocument();
1174
+ });
1175
+
1176
+ fireEvent.click(screen.getByTestId("timeline-next-button"));
1177
+
1178
+ await waitFor(() => {
1179
+ expect(pending).toHaveLength(2);
1180
+ });
1181
+
1182
+ expect(
1183
+ screen.getByText("This team does not own any on-call schedules yet"),
1184
+ ).toBeInTheDocument();
1185
+ expect(screen.queryByTestId("timeline-summary")).not.toBeInTheDocument();
1186
+ expect(screen.queryByTestId("timeline-no-matches")).not.toBeInTheDocument();
1187
+ });
1188
+
1189
+ test("leaving the page stops a request that is waiting to retry", async () => {
1190
+ getMock.mockResolvedValue(
1191
+ new HTTPErrorResponse(503, { message: "busy" }, { "retry-after": "1" }),
1192
+ );
1193
+
1194
+ jest.useFakeTimers();
1195
+
1196
+ try {
1197
+ renderTimeline();
1198
+
1199
+ await act(async () => {
1200
+ await Promise.resolve();
1201
+ });
1202
+
1203
+ expect(getMock).toHaveBeenCalledTimes(1);
1204
+
1205
+ cleanup();
1206
+
1207
+ await act(async () => {
1208
+ jest.advanceTimersByTime(5000);
1209
+ await Promise.resolve();
1210
+ });
1211
+
1212
+ expect(getMock).toHaveBeenCalledTimes(1);
1213
+ } finally {
1214
+ jest.useRealTimers();
1215
+ }
1216
+ });
1217
+ });
1218
+
1219
+ describe("changing the time zone", () => {
1220
+ test("keeps the same dates on screen, even across the date line", async () => {
1221
+ // Sunday 22:00 UTC: already Monday in Tokyo.
1222
+ jest
1223
+ .spyOn(OneUptimeDate, "getCurrentDate")
1224
+ .mockReturnValue(new Date("2026-09-20T22:00:00.000Z"));
1225
+
1226
+ renderTimeline();
1227
+
1228
+ await loaded();
1229
+
1230
+ expect(dayKeys()[0]).toBe("2026-09-14");
1231
+
1232
+ fireEvent.click(screen.getByTestId("timezone-to-Asia/Tokyo"));
1233
+
1234
+ // The week of 14 September, now laid out in Tokyo.
1235
+ await waitFor(() => {
1236
+ expect(lastRequested().from).toBe("2026-09-13T15:00:00.000Z");
1237
+ });
1238
+ expect(lastRequested().to).toBe("2026-09-20T15:00:00.000Z");
1239
+ expect(screen.getByTestId("timeline-range-label")).toHaveTextContent(
1240
+ "September 14 – 20, 2026",
1241
+ );
1242
+ });
1243
+
1244
+ test("after paging forward, a zone to the west does not page back", async () => {
1245
+ renderTimeline();
1246
+
1247
+ await loaded();
1248
+
1249
+ fireEvent.click(screen.getByTestId("timeline-next-button"));
1250
+
1251
+ await waitFor(() => {
1252
+ expect(lastRequested().from).toBe("2026-09-21T00:00:00.000Z");
1253
+ });
1254
+
1255
+ fireEvent.click(screen.getByTestId("timezone-to-America/New_York"));
1256
+
1257
+ await waitFor(() => {
1258
+ expect(lastRequested().from).toBe("2026-09-21T04:00:00.000Z");
1259
+ });
1260
+ expect(screen.getByTestId("timeline-range-label")).toHaveTextContent(
1261
+ "September 21 – 27, 2026",
1262
+ );
1263
+ });
1264
+ });
1265
+
1266
+ describe("the Week / Month switch", () => {
1267
+ test("is one tab stop, and the arrow keys move the selection", async () => {
1268
+ renderTimeline();
1269
+
1270
+ await loaded();
1271
+
1272
+ const weekButton: HTMLElement = screen.getByTestId("timeline-mode-week");
1273
+ const monthButton: HTMLElement = screen.getByTestId("timeline-mode-month");
1274
+
1275
+ expect(weekButton).toHaveAttribute("tabindex", "0");
1276
+ expect(monthButton).toHaveAttribute("tabindex", "-1");
1277
+
1278
+ weekButton.focus();
1279
+ fireEvent.keyDown(weekButton, { key: "ArrowRight" });
1280
+
1281
+ await waitFor(() => {
1282
+ expect(lastRequested().from).toBe("2026-09-01T00:00:00.000Z");
1283
+ });
1284
+
1285
+ expect(monthButton).toHaveAttribute("aria-checked", "true");
1286
+ expect(monthButton).toHaveAttribute("tabindex", "0");
1287
+ expect(document.activeElement).toBe(monthButton);
1288
+
1289
+ fireEvent.keyDown(monthButton, { key: "Home" });
1290
+
1291
+ await waitFor(() => {
1292
+ expect(weekButton).toHaveAttribute("aria-checked", "true");
1293
+ });
1294
+ expect(document.activeElement).toBe(weekButton);
1295
+
1296
+ // Other keys are left alone.
1297
+ fireEvent.keyDown(weekButton, { key: "a" });
1298
+ expect(weekButton).toHaveAttribute("aria-checked", "true");
1299
+ });
1300
+ });
1301
+
1302
+ describe("attention filters", () => {
1303
+ test("'no one on call now' is dropped when the reader pages away from now", async () => {
1304
+ renderTimeline();
1305
+
1306
+ await loaded();
1307
+
1308
+ fireEvent.click(screen.getByTestId("timeline-summary-uncovered-now"));
1309
+
1310
+ expect(rowIds()).toEqual(["s-sre", "s-sre", "s-legacy"]);
1311
+
1312
+ fireEvent.click(screen.getByTestId("timeline-next-button"));
1313
+
1314
+ // Next week cannot say who is uncovered "now": every schedule is back.
1315
+ await waitFor(() => {
1316
+ expect(dayKeys()[0]).toBe("2026-09-21");
1317
+ });
1318
+ await waitFor(() => {
1319
+ expect(rowIds()).toHaveLength(4);
1320
+ });
1321
+ expect(screen.queryByTestId("timeline-no-matches")).not.toBeInTheDocument();
1322
+ });
1323
+
1324
+ test("an active 'coverage gaps' chip stays visible even at zero, so it can be turned off", async () => {
1325
+ const covered: ScheduleTimelineScheduleJson = {
1326
+ ...PAYMENTS,
1327
+ shifts: [
1328
+ {
1329
+ ...PAYMENTS.shifts[0]!,
1330
+ start: "2026-09-01T00:00:00.000Z",
1331
+ end: "2026-10-15T00:00:00.000Z",
1332
+ },
1333
+ ],
1334
+ };
1335
+
1336
+ getMock.mockImplementation(async (options: { url: URL }) => {
1337
+ const parsed: globalThis.URL = new globalThis.URL(options.url.toString());
1338
+ const from: string = parsed.searchParams.get("from") || WEEK_FROM;
1339
+ const to: string = parsed.searchParams.get("to") || WEEK_TO;
1340
+
1341
+ return ok(
1342
+ from === WEEK_FROM
1343
+ ? timeline()
1344
+ : timeline({ from, to, schedules: [covered] }),
1345
+ );
1346
+ });
1347
+
1348
+ renderTimeline();
1349
+
1350
+ await loaded();
1351
+
1352
+ fireEvent.click(screen.getByTestId("timeline-summary-gaps"));
1353
+ fireEvent.click(screen.getByTestId("timeline-next-button"));
1354
+
1355
+ // Next week has no gaps at all; the chip must stay, at zero and pressed.
1356
+ await waitFor(() => {
1357
+ expect(screen.getByTestId("timeline-summary-gaps")).toHaveTextContent(
1358
+ "0with coverage gaps this week",
1359
+ );
1360
+ });
1361
+
1362
+ const chip: HTMLElement = screen.getByTestId("timeline-summary-gaps");
1363
+
1364
+ expect(chip).toHaveTextContent("0with coverage gaps this week");
1365
+ expect(chip).toHaveAttribute("aria-pressed", "true");
1366
+ expect(screen.getByTestId("timeline-no-matches")).toBeInTheDocument();
1367
+
1368
+ fireEvent.click(chip);
1369
+
1370
+ expect(rowIds()).toEqual(["s-pay"]);
1371
+ expect(
1372
+ screen.queryByTestId("timeline-summary-gaps"),
1373
+ ).not.toBeInTheDocument();
1374
+ });
1375
+ });
1376
+
1377
+ describe("layout details", () => {
1378
+ test("'no schedules match' is drawn outside the scrolling grid", async () => {
1379
+ renderTimeline();
1380
+
1381
+ await loaded();
1382
+
1383
+ fireEvent.change(screen.getByTestId("timeline-search"), {
1384
+ target: { value: "zzz" },
1385
+ });
1386
+
1387
+ expect(screen.getByTestId("timeline-no-matches")).toBeInTheDocument();
1388
+ expect(
1389
+ screen.queryByTestId("schedule-timeline-grid"),
1390
+ ).not.toBeInTheDocument();
1391
+ });
1392
+
1393
+ test("a row reads in time order, each override lane right after its shift", async () => {
1394
+ renderTimeline();
1395
+
1396
+ await loaded();
1397
+
1398
+ const order: Array<string> = within(row("s-sre"))
1399
+ .getAllByTestId(/timeline-(shift-bar|gap|overridden-segment)/)
1400
+ .map((element: HTMLElement) => {
1401
+ const kind: string = element.getAttribute("data-testid") || "";
1402
+ return kind === "timeline-shift-bar"
1403
+ ? `shift:${element.getAttribute("data-user-id")}`
1404
+ : kind.replace("timeline-", "");
1405
+ });
1406
+
1407
+ expect(order).toEqual([
1408
+ "shift:u-carol",
1409
+ "gap",
1410
+ "shift:u-dan",
1411
+ "overridden-segment",
1412
+ ]);
1413
+ });
1414
+
1415
+ test("a served window wholly outside the range shades the whole range", async () => {
1416
+ getMock.mockResolvedValue(
1417
+ ok(
1418
+ timeline({
1419
+ from: "2027-09-17T12:00:00.000Z",
1420
+ to: "2027-09-18T12:00:00.000Z",
1421
+ }),
1422
+ ),
1423
+ );
1424
+
1425
+ renderTimeline();
1426
+
1427
+ await loaded();
1428
+
1429
+ const bands: Array<HTMLElement> = screen.getAllByTestId(
1430
+ "timeline-unavailable",
1431
+ );
1432
+
1433
+ expect(bands).toHaveLength(1);
1434
+ expect(bands[0]?.style.left).toBe("0%");
1435
+ expect(bands[0]?.style.width).toBe("100%");
1436
+ expect(screen.getByTestId("timeline-outside-range")).toBeInTheDocument();
1437
+ expect(screen.queryAllByTestId("timeline-gap")).toHaveLength(0);
1438
+ });
1439
+
1440
+ test("keyboard focus uses an outline, which inline shadows and opacity cannot hide", async () => {
1441
+ renderTimeline();
1442
+
1443
+ await loaded();
1444
+
1445
+ for (const element of [
1446
+ ...screen.getAllByTestId("timeline-shift-bar"),
1447
+ ...screen.getAllByTestId("timeline-gap"),
1448
+ ...screen.getAllByTestId("timeline-overridden-segment"),
1449
+ ]) {
1450
+ expect(element.className).toContain("focus-visible:outline-2");
1451
+ expect(element.className).toContain("focus-visible:!opacity-100");
1452
+ expect(element.className).not.toContain("focus-visible:ring");
1453
+ }
1454
+ });
1455
+
1456
+ test("rows are memoized", () => {
1457
+ expect((TimelineRow as unknown as { $$typeof: symbol }).$$typeof).toBe(
1458
+ Symbol.for("react.memo"),
1459
+ );
1460
+ });
1461
+ });
1462
+
1463
+ describe("review round two", () => {
1464
+ test("paging away with 'no one on call now' on never flashes 'No schedules match'", async () => {
1465
+ const pending: Array<(value: HTTPResponse<JSONObject>) => void> =
1466
+ pendingAnswers();
1467
+
1468
+ renderTimeline();
1469
+
1470
+ await waitFor(() => {
1471
+ expect(pending).toHaveLength(1);
1472
+ });
1473
+ await act(async () => {
1474
+ pending[0]!(ok(timeline()));
1475
+ });
1476
+ await loaded();
1477
+
1478
+ fireEvent.click(screen.getByTestId("timeline-summary-uncovered-now"));
1479
+ fireEvent.click(screen.getByTestId("timeline-next-button"));
1480
+
1481
+ await waitFor(() => {
1482
+ expect(pending).toHaveLength(2);
1483
+ });
1484
+
1485
+ await act(async () => {
1486
+ pending[1]!(
1487
+ ok(
1488
+ timeline({
1489
+ from: "2026-09-21T00:00:00.000Z",
1490
+ to: "2026-09-28T00:00:00.000Z",
1491
+ }),
1492
+ ),
1493
+ );
1494
+ });
1495
+
1496
+ // The very first render of next week already shows every schedule.
1497
+ expect(screen.queryByTestId("timeline-no-matches")).not.toBeInTheDocument();
1498
+ expect(dayKeys()[0]).toBe("2026-09-21");
1499
+ expect(rowIds()).toHaveLength(4);
1500
+ });
1501
+
1502
+ test("a 'now' frozen on an earlier visit never marks a week the clock has left", () => {
1503
+ const data: TimelineData = TimelineModel.fromResponse(timeline());
1504
+ const groups: ReturnType<typeof TimelineModel.groupSchedules> =
1505
+ TimelineModel.groupSchedules({
1506
+ schedules: data.schedules,
1507
+ teams: data.teams,
1508
+ groupByTeam: false,
1509
+ });
1510
+
1511
+ const weekOf: (anchor: string) => TimelineRange = (
1512
+ anchor: string,
1513
+ ): TimelineRange => {
1514
+ return ScheduleTimelineLayout.getRange({
1515
+ mode: TimelineViewMode.Week,
1516
+ anchor: new Date(anchor),
1517
+ timezone: "UTC",
1518
+ });
1519
+ };
1520
+
1521
+ const thisWeek: TimelineRange = weekOf("2026-09-17T12:00:00.000Z");
1522
+ const lastWeek: TimelineRange = weekOf("2026-09-10T12:00:00.000Z");
1523
+
1524
+ const renderGrid: (range: TimelineRange, now: Date) => ReactElement = (
1525
+ range: TimelineRange,
1526
+ now: Date,
1527
+ ): ReactElement => {
1528
+ return (
1529
+ <MemoryRouter>
1530
+ <TimelineGrid
1531
+ range={range}
1532
+ groups={groups}
1533
+ now={now}
1534
+ computedWindow={{ start: range.start, end: range.end }}
1535
+ servedWindow={{ start: range.start, end: range.end }}
1536
+ showGroupHeaders={false}
1537
+ collapsedGroupKeys={new Set<string>()}
1538
+ onToggleGroup={() => {}}
1539
+ highlightedUserId={null}
1540
+ onToggleHighlight={() => {}}
1541
+ />
1542
+ </MemoryRouter>
1543
+ );
1544
+ };
1545
+
1546
+ // Thursday of this week: someone is on call now.
1547
+ const { rerender } = render(
1548
+ renderGrid(thisWeek, new Date("2026-09-17T12:00:00.000Z")),
1549
+ );
1550
+
1551
+ expect(
1552
+ screen.getAllByTestId("timeline-on-call-now").length,
1553
+ ).toBeGreaterThan(0);
1554
+
1555
+ // Page back; the clock is still Thursday.
1556
+ rerender(renderGrid(lastWeek, new Date("2026-09-17T12:00:00.000Z")));
1557
+
1558
+ // Come back to "this" week after the clock has moved on to the next one.
1559
+ rerender(renderGrid(thisWeek, new Date("2026-09-22T09:00:00.000Z")));
1560
+
1561
+ // The rows now describe the week, not a "now" inside it...
1562
+ expect(screen.queryAllByTestId("timeline-on-call-now")).toHaveLength(0);
1563
+ expect(screen.queryAllByTestId("timeline-uncovered-now")).toHaveLength(0);
1564
+
1565
+ /*
1566
+ * ...and the only active bar is the one that really contains the new
1567
+ * now: Bob's shift runs from Thursday into next week.
1568
+ */
1569
+ expect(
1570
+ screen
1571
+ .getAllByTestId("timeline-shift-bar")
1572
+ .filter((bar: HTMLElement) => {
1573
+ return bar.getAttribute("data-active") === "true";
1574
+ })
1575
+ .map((bar: HTMLElement) => {
1576
+ return bar.getAttribute("data-user-id");
1577
+ }),
1578
+ ).toEqual(["u-bob"]);
1579
+ });
1580
+ });