@oneuptime/common 11.3.26 → 11.4.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 (298) hide show
  1. package/Models/DatabaseModels/AIRun.ts +50 -0
  2. package/Models/DatabaseModels/IncidentEpisodeRoleMember.ts +1 -1
  3. package/Models/DatabaseModels/IncidentMember.ts +1 -1
  4. package/Models/DatabaseModels/IncomingCallPolicy.ts +13 -0
  5. package/Models/DatabaseModels/Index.ts +3 -0
  6. package/Models/DatabaseModels/MigrationFailure.ts +170 -0
  7. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +51 -2
  8. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +44 -0
  9. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +6 -0
  10. package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.ts +6 -0
  11. package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +1 -1
  12. package/Models/DatabaseModels/OnCallDutyPolicyUserOverride.ts +2 -2
  13. package/Models/DatabaseModels/Project.ts +60 -0
  14. package/Server/API/AIInvestigationAPI.ts +148 -0
  15. package/Server/API/AlertAPI.ts +1 -1
  16. package/Server/API/IncidentAPI.ts +2 -2
  17. package/Server/API/IncidentEpisodeAPI.ts +1 -1
  18. package/Server/API/LlmProviderAPI.ts +1 -0
  19. package/Server/API/ScheduledMaintenanceAPI.ts +1 -1
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.ts +25 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.ts +81 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.ts +25 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.ts +35 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.ts +19 -0
  25. package/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.ts +33 -0
  26. package/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.ts +19 -0
  27. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -0
  28. package/Server/Infrastructure/PostgresDatabase.ts +11 -0
  29. package/Server/Services/AIService.ts +74 -2
  30. package/Server/Services/AlertService.ts +25 -0
  31. package/Server/Services/AnalyticsDatabaseService.ts +10 -1
  32. package/Server/Services/DatabaseService.ts +10 -1
  33. package/Server/Services/IncidentService.ts +26 -1
  34. package/Server/Services/IncidentStateTimelineService.ts +19 -0
  35. package/Server/Services/IncomingCallPolicyEscalationRuleService.ts +75 -2
  36. package/Server/Services/IncomingCallPolicyService.ts +42 -1
  37. package/Server/Services/Index.ts +2 -0
  38. package/Server/Services/MigrationFailureService.ts +9 -0
  39. package/Server/Services/MonitorProbeService.ts +33 -7
  40. package/Server/Services/MonitorService.ts +19 -7
  41. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
  42. package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +185 -41
  43. package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +23 -15
  44. package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +39 -0
  45. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +148 -0
  46. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +11 -2
  47. package/Server/Services/OnCallDutyPolicyScheduleService.ts +502 -26
  48. package/Server/Services/OnCallDutyPolicyTimeLogService.ts +49 -7
  49. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +218 -4
  50. package/Server/Services/ProjectCallSMSConfigService.ts +82 -1
  51. package/Server/Services/TeamMemberService.ts +6 -0
  52. package/Server/Services/UserNotificationRuleService.ts +13 -42
  53. package/Server/Services/UserOnCallLogService.ts +90 -0
  54. package/Server/Services/UserOnCallLogTimelineService.ts +43 -1
  55. package/Server/Types/Database/QueryUtil.ts +57 -0
  56. package/Server/Utils/AI/AlertAIContextBuilder.ts +6 -1
  57. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +14 -2
  58. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +119 -11
  59. package/Server/Utils/AI/IncidentAIContextBuilder.ts +18 -3
  60. package/Server/Utils/AI/IncidentEpisodeAIContextBuilder.ts +6 -1
  61. package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +12 -2
  62. package/Server/Utils/AI/Sentinel/AlertInvestigationRunner.ts +141 -0
  63. package/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.ts +178 -0
  64. package/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.ts +105 -0
  65. package/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.ts +405 -0
  66. package/Server/Utils/AI/Sentinel/SentinelMemory.ts +167 -0
  67. package/Server/Utils/AI/Toolbox/IncidentTools.ts +183 -1
  68. package/Server/Utils/AI/Toolbox/Index.ts +15 -1
  69. package/Server/Utils/AI/Toolbox/RecentChangesTools.ts +208 -0
  70. package/Server/Utils/AI/Toolbox/SentinelActionTools.ts +577 -0
  71. package/Server/Utils/AI/Toolbox/Serializer.ts +42 -0
  72. package/Server/Utils/Database/MigrationFailureLog.ts +240 -0
  73. package/Server/Utils/IncomingCallPhoneNumber.ts +41 -0
  74. package/Server/Utils/LLM/LLMService.ts +110 -11
  75. package/Server/Utils/Telemetry/IoTSnapshotScan.ts +12 -9
  76. package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +12 -9
  77. package/Tests/Server/API/BaseAPI.test.ts +1 -0
  78. package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +0 -8
  79. package/Tests/Server/Services/OnCallDutyPolicyScheduleLayerUserReorder.test.ts +233 -0
  80. package/Tests/Server/Services/OnCallDutyPolicyTimeLogServiceScoping.test.ts +242 -0
  81. package/Tests/Server/Services/OnCallDutyPolicyUserOverrideEdit.test.ts +198 -0
  82. package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +495 -0
  83. package/Tests/Server/Types/Database/QueryUtil.test.ts +74 -0
  84. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +78 -0
  85. package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +68 -0
  86. package/Tests/Types/DateExhaustiveTimezone.test.ts +700 -0
  87. package/Tests/Types/DateTimezoneWallClock.test.ts +99 -0
  88. package/Tests/Types/Events/Recurring.test.ts +22 -9
  89. package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixes.test.ts +291 -0
  90. package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixesRound2.test.ts +398 -0
  91. package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeekendGap.test.ts +114 -0
  92. package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeeklyDayShift.test.ts +76 -0
  93. package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyMutation.test.ts +128 -0
  94. package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyProbe.test.ts +203 -0
  95. package/Tests/Types/OnCallDutyPolicy/LayerUtilDiffFuzz.test.ts +322 -0
  96. package/Tests/Types/OnCallDutyPolicy/LayerUtilEdgeCases.test.ts +280 -0
  97. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveCurrentUser.test.ts +863 -0
  98. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveDaily.test.ts +900 -0
  99. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveMultiLayer.test.ts +1104 -0
  100. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +957 -0
  101. package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveWeekly.test.ts +1095 -0
  102. package/Tests/Types/OnCallDutyPolicy/LayerUtilInvariants.test.ts +232 -0
  103. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +227 -0
  104. package/Tests/Types/OnCallDutyPolicy/LayerUtilMonthYearAudit.test.ts +255 -0
  105. package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerAudit.test.ts +388 -0
  106. package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerEdge.test.ts +300 -0
  107. package/Tests/Types/OnCallDutyPolicy/LayerUtilOvernightDSTAudit.test.ts +158 -0
  108. package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapFix.test.ts +253 -0
  109. package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapRepro.test.ts +157 -0
  110. package/Tests/Types/OnCallDutyPolicy/LayerUtilRotationFixes.test.ts +414 -0
  111. package/Tests/Types/OnCallDutyPolicy/LayerUtilTimezone.test.ts +243 -0
  112. package/Tests/Types/OnCallDutyPolicy/LayerUtilWeekendGapRepro.test.ts +112 -0
  113. package/Tests/Types/OnCallDutyPolicy/OverrideLensAudit.test.ts +117 -0
  114. package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +8 -6
  115. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesDefaultDayAudit.test.ts +112 -0
  116. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesExhaustive.test.ts +821 -0
  117. package/Tests/Types/OnCallDutyPolicy/RestrictionTimesMutationInvariant.test.ts +886 -0
  118. package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtil.test.ts +260 -0
  119. package/Tests/Types/OnCallDutyPolicy/UserOverrideUtil.test.ts +255 -0
  120. package/Tests/Types/OnCallDutyPolicy/UserOverrideUtilExhaustive.test.ts +1126 -0
  121. package/Types/Date.ts +246 -15
  122. package/Types/OnCallDutyPolicy/Layer.ts +790 -260
  123. package/Types/OnCallDutyPolicy/RestrictionTimes.ts +39 -13
  124. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +257 -0
  125. package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +21 -5
  126. package/UI/Components/Calendar/Calendar.tsx +8 -2
  127. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +5 -5
  128. package/UI/Components/Events/RecurringFieldElement.tsx +16 -0
  129. package/UI/Components/ModelFormModal/ModelFormModal.tsx +8 -0
  130. package/UI/Components/RadioButtons/BasicRadioButtons.tsx +1 -1
  131. package/build/dist/Models/DatabaseModels/AIRun.js +52 -0
  132. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
  133. package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js +1 -1
  134. package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js.map +1 -1
  135. package/build/dist/Models/DatabaseModels/IncidentMember.js +1 -1
  136. package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
  137. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js +10 -0
  138. package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js.map +1 -1
  139. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  140. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  141. package/build/dist/Models/DatabaseModels/MigrationFailure.js +196 -0
  142. package/build/dist/Models/DatabaseModels/MigrationFailure.js.map +1 -0
  143. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +52 -2
  144. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  145. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +45 -0
  146. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  147. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +6 -0
  148. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
  149. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js +6 -0
  150. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +1 -1
  152. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js +2 -2
  154. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js.map +1 -1
  155. package/build/dist/Models/DatabaseModels/Project.js +62 -0
  156. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  157. package/build/dist/Server/API/AIInvestigationAPI.js +107 -0
  158. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -0
  159. package/build/dist/Server/API/AlertAPI.js +1 -1
  160. package/build/dist/Server/API/IncidentAPI.js +2 -2
  161. package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
  162. package/build/dist/Server/API/LlmProviderAPI.js +1 -0
  163. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  164. package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
  165. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js +18 -0
  166. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js.map +1 -0
  167. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js +38 -0
  168. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js.map +1 -0
  169. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js +16 -0
  170. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js.map +1 -0
  171. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js +18 -0
  172. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js.map +1 -0
  173. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.js +12 -0
  174. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.js.map +1 -0
  175. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.js +18 -0
  176. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.js.map +1 -0
  177. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.js +12 -0
  178. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.js.map +1 -0
  179. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -0
  180. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  181. package/build/dist/Server/Infrastructure/PostgresDatabase.js +9 -0
  182. package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
  183. package/build/dist/Server/Services/AIService.js +53 -2
  184. package/build/dist/Server/Services/AIService.js.map +1 -1
  185. package/build/dist/Server/Services/AlertService.js +24 -0
  186. package/build/dist/Server/Services/AlertService.js.map +1 -1
  187. package/build/dist/Server/Services/AnalyticsDatabaseService.js +10 -1
  188. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  189. package/build/dist/Server/Services/DatabaseService.js +10 -1
  190. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  191. package/build/dist/Server/Services/IncidentService.js +25 -1
  192. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  193. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  194. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  195. package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js +55 -2
  196. package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js.map +1 -1
  197. package/build/dist/Server/Services/IncomingCallPolicyService.js +40 -0
  198. package/build/dist/Server/Services/IncomingCallPolicyService.js.map +1 -1
  199. package/build/dist/Server/Services/Index.js +2 -0
  200. package/build/dist/Server/Services/Index.js.map +1 -1
  201. package/build/dist/Server/Services/MigrationFailureService.js +9 -0
  202. package/build/dist/Server/Services/MigrationFailureService.js.map +1 -0
  203. package/build/dist/Server/Services/MonitorProbeService.js +33 -7
  204. package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
  205. package/build/dist/Server/Services/MonitorService.js +12 -5
  206. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  207. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +81 -22
  208. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  209. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +134 -31
  210. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
  211. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +21 -13
  212. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
  213. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +36 -0
  214. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
  215. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +121 -0
  216. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  217. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +11 -2
  218. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  219. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +409 -36
  220. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  221. package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js +52 -8
  222. package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js.map +1 -1
  223. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +170 -3
  224. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  225. package/build/dist/Server/Services/ProjectCallSMSConfigService.js +72 -0
  226. package/build/dist/Server/Services/ProjectCallSMSConfigService.js.map +1 -1
  227. package/build/dist/Server/Services/TeamMemberService.js +6 -0
  228. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  229. package/build/dist/Server/Services/UserNotificationRuleService.js +12 -30
  230. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  231. package/build/dist/Server/Services/UserOnCallLogService.js +75 -0
  232. package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
  233. package/build/dist/Server/Services/UserOnCallLogTimelineService.js +31 -1
  234. package/build/dist/Server/Services/UserOnCallLogTimelineService.js.map +1 -1
  235. package/build/dist/Server/Types/Database/QueryUtil.js +45 -0
  236. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  237. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +6 -1
  238. package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -1
  239. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +13 -2
  240. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  241. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +59 -11
  242. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
  243. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +18 -3
  244. package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -1
  245. package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js +6 -1
  246. package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js.map +1 -1
  247. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js +12 -2
  248. package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js.map +1 -1
  249. package/build/dist/Server/Utils/AI/Sentinel/AlertInvestigationRunner.js +119 -0
  250. package/build/dist/Server/Utils/AI/Sentinel/AlertInvestigationRunner.js.map +1 -0
  251. package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js +147 -0
  252. package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js.map +1 -0
  253. package/build/dist/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.js +93 -0
  254. package/build/dist/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.js.map +1 -0
  255. package/build/dist/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.js +311 -0
  256. package/build/dist/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.js.map +1 -0
  257. package/build/dist/Server/Utils/AI/Sentinel/SentinelMemory.js +143 -0
  258. package/build/dist/Server/Utils/AI/Sentinel/SentinelMemory.js.map +1 -0
  259. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +152 -1
  260. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
  261. package/build/dist/Server/Utils/AI/Toolbox/Index.js +10 -1
  262. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  263. package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js +165 -0
  264. package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js.map +1 -0
  265. package/build/dist/Server/Utils/AI/Toolbox/SentinelActionTools.js +451 -0
  266. package/build/dist/Server/Utils/AI/Toolbox/SentinelActionTools.js.map +1 -0
  267. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +40 -0
  268. package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -1
  269. package/build/dist/Server/Utils/Database/MigrationFailureLog.js +174 -0
  270. package/build/dist/Server/Utils/Database/MigrationFailureLog.js.map +1 -0
  271. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js +30 -0
  272. package/build/dist/Server/Utils/IncomingCallPhoneNumber.js.map +1 -0
  273. package/build/dist/Server/Utils/LLM/LLMService.js +81 -5
  274. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  275. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +11 -8
  276. package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
  277. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +11 -8
  278. package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
  279. package/build/dist/Types/Date.js +185 -15
  280. package/build/dist/Types/Date.js.map +1 -1
  281. package/build/dist/Types/OnCallDutyPolicy/Layer.js +602 -171
  282. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  283. package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js +27 -13
  284. package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js.map +1 -1
  285. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +159 -0
  286. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -0
  287. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +20 -5
  288. package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
  289. package/build/dist/UI/Components/Calendar/Calendar.js +2 -2
  290. package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
  291. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
  292. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  293. package/build/dist/UI/Components/Events/RecurringFieldElement.js +13 -0
  294. package/build/dist/UI/Components/Events/RecurringFieldElement.js.map +1 -1
  295. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  296. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js +1 -1
  297. package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js.map +1 -1
  298. package/package.json +1 -1
@@ -8,6 +8,8 @@ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
8
8
  import OneUptimeDate from "../../Types/Date";
9
9
  import ObjectID from "../../Types/ObjectID";
10
10
  import LayerUtil, { LayerProps } from "../../Types/OnCallDutyPolicy/Layer";
11
+ import { RestrictionType } from "../../Types/OnCallDutyPolicy/RestrictionTimes";
12
+ import Recurring from "../../Types/Events/Recurring";
11
13
  import UserOverrideUtil, {
12
14
  UserOverrideRecord,
13
15
  } from "../../Types/OnCallDutyPolicy/UserOverrideUtil";
@@ -34,6 +36,7 @@ import NotificationSettingEventType from "../../Types/NotificationSetting/Notifi
34
36
  import BadDataException from "../../Types/Exception/BadDataException";
35
37
  import Timezone from "../../Types/Timezone";
36
38
  import logger, { LogAttributes } from "../Utils/Logger";
39
+ import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
37
40
  import OnCallDutyPolicyFeedService from "./OnCallDutyPolicyFeedService";
38
41
  import { OnCallDutyPolicyFeedEventType } from "../../Models/DatabaseModels/OnCallDutyPolicyFeed";
39
42
  import { Green500 } from "../../Types/BrandColors";
@@ -384,7 +387,19 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
384
387
  });
385
388
  }
386
389
 
387
- if (newInformation.currentUserIdOnRoster?.toString()) {
390
+ /*
391
+ * Only notify "you are now on-call" when the current user ACTUALLY
392
+ * changed — not merely when the handoff time changed. The enclosing
393
+ * guard also fires on a rosterHandoffAt change, which happens every
394
+ * rotation period; without this user-changed check a continuing (e.g.
395
+ * single-user) roster was re-sent the full email + SMS + phone call +
396
+ * push + WhatsApp bundle every period. Mirrors the removal branch above.
397
+ */
398
+ if (
399
+ previousInformation.currentUserIdOnRoster?.toString() !==
400
+ newInformation.currentUserIdOnRoster?.toString() &&
401
+ newInformation.currentUserIdOnRoster?.toString()
402
+ ) {
388
403
  // send email to the new current user.
389
404
  const sendEmailToUserId: ObjectID =
390
405
  newInformation.currentUserIdOnRoster;
@@ -544,7 +559,20 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
544
559
  previousInformation.nextRosterStartAt?.toString() !==
545
560
  newInformation.nextRosterStartAt?.toString()
546
561
  ) {
547
- if (newInformation.nextUserIdOnRoster?.toString()) {
562
+ /*
563
+ * Only notify "you are next on-call" when the next user ACTUALLY
564
+ * changed — not merely when the next handoff/start time advanced. The
565
+ * enclosing guard also fires when nextHandOffTimeAt / nextRosterStartAt
566
+ * change, which happens every rotation period; without this
567
+ * user-changed check a continuing (e.g. single-user) roster re-sent the
568
+ * full email + SMS + phone call + push + WhatsApp bundle every period.
569
+ * Mirrors the current-user "now on-call" branch above.
570
+ */
571
+ if (
572
+ previousInformation.nextUserIdOnRoster?.toString() !==
573
+ newInformation.nextUserIdOnRoster?.toString() &&
574
+ newInformation.nextUserIdOnRoster?.toString()
575
+ ) {
548
576
  // send email to the next user.
549
577
  const sendEmailToUserId: ObjectID = newInformation.nextUserIdOnRoster;
550
578
  const userTimezone: Timezone | null =
@@ -680,6 +708,62 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
680
708
  nextHandOffTimeAt: Date | null;
681
709
  rosterStartAt: Date | null;
682
710
  nextRosterStartAt: Date | null;
711
+ }> {
712
+ /*
713
+ * Serialize per-schedule refreshes across processes (audit L2). This method
714
+ * reads the persisted roster, diffs it, SENDS the handoff notification
715
+ * bundle + feed items, and only THEN persists — notifying before persisting
716
+ * is deliberate (audit F11). Without a lock the override-triggered refresh
717
+ * (API process) and the EVERY_MINUTE RefreshHandoffTime cron (Workers
718
+ * process) can both read the same stale roster and both send the full page
719
+ * before either persists, double-paging the incoming user. A blocking
720
+ * per-schedule mutex makes the second caller wait, then read the
721
+ * already-persisted new roster and diff to "no change" -> no duplicate page.
722
+ * Best-effort: if the lock cannot be acquired (Redis unavailable, or the
723
+ * acquire window is exceeded) we proceed unlocked — no worse than before.
724
+ */
725
+ let mutex: SemaphoreMutex | null = null;
726
+ try {
727
+ mutex = await Semaphore.lock({
728
+ key: scheduleId.toString(),
729
+ namespace: "OnCallDutyPolicyScheduleService.refreshRoster",
730
+ lockTimeout: 60000,
731
+ acquireTimeout: 15000,
732
+ retryInterval: 200,
733
+ acquireAttemptsLimit: 100,
734
+ });
735
+ } catch (err) {
736
+ logger.debug(
737
+ "Proceeding with roster refresh without a per-schedule lock (could not acquire) for scheduleId: " +
738
+ scheduleId.toString(),
739
+ );
740
+ logger.debug(err);
741
+ }
742
+
743
+ try {
744
+ return await this.refreshCurrentUserIdAndHandoffTimeInScheduleInternal(
745
+ scheduleId,
746
+ );
747
+ } finally {
748
+ if (mutex) {
749
+ try {
750
+ await Semaphore.release(mutex);
751
+ } catch (err) {
752
+ logger.error(err);
753
+ }
754
+ }
755
+ }
756
+ }
757
+
758
+ private async refreshCurrentUserIdAndHandoffTimeInScheduleInternal(
759
+ scheduleId: ObjectID,
760
+ ): Promise<{
761
+ currentUserId: ObjectID | null;
762
+ handOffTimeAt: Date | null;
763
+ nextUserId: ObjectID | null;
764
+ nextHandOffTimeAt: Date | null;
765
+ rosterStartAt: Date | null;
766
+ nextRosterStartAt: Date | null;
683
767
  }> {
684
768
  logger.debug(
685
769
  "refreshCurrentUserIdAndHandoffTimeInSchedule called with scheduleId: " +
@@ -749,6 +833,18 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
749
833
  { onCallDutyPolicyScheduleId: scheduleId.toString() } as LogAttributes,
750
834
  );
751
835
 
836
+ /*
837
+ * When this schedule is attached to exactly one on-call policy, resolve the
838
+ * roster in that policy's context so its policy-scoped overrides are
839
+ * reflected in the persisted roster, handoff notifications and time logs —
840
+ * matching the live paging path, which already passes the policy id (audit
841
+ * F10). Attached to zero or multiple policies, the roster stays
842
+ * policy-agnostic (global overrides only), because a single schedule-level
843
+ * roster cannot represent divergent per-policy overrides.
844
+ */
845
+ const singleAttachedPolicyId: ObjectID | undefined =
846
+ await this.getSingleAttachedPolicyId(scheduleId);
847
+
752
848
  const newInformation: {
753
849
  currentUserId: ObjectID | null;
754
850
  handOffTimeAt: Date | null;
@@ -756,7 +852,9 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
756
852
  nextHandOffTimeAt: Date | null;
757
853
  rosterStartAt: Date | null;
758
854
  nextRosterStartAt: Date | null;
759
- } = await this.getCurrrentUserIdAndHandoffTimeInSchedule(scheduleId);
855
+ } = await this.getCurrrentUserIdAndHandoffTimeInSchedule(scheduleId, {
856
+ onCallDutyPolicyId: singleAttachedPolicyId,
857
+ });
760
858
 
761
859
  logger.debug(newInformation, {
762
860
  onCallDutyPolicyScheduleId: scheduleId.toString(),
@@ -768,29 +866,25 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
768
866
  { onCallDutyPolicyScheduleId: scheduleId.toString() } as LogAttributes,
769
867
  );
770
868
 
771
- await this.updateOneById({
772
- id: scheduleId!,
773
- data: {
774
- currentUserIdOnRoster: newInformation.currentUserId,
775
- rosterHandoffAt: newInformation.handOffTimeAt,
776
- nextUserIdOnRoster: newInformation.nextUserId,
777
- rosterNextHandoffAt: newInformation.nextHandOffTimeAt,
778
- rosterStartAt: newInformation.rosterStartAt,
779
- rosterNextStartAt: newInformation.nextRosterStartAt,
780
- },
781
- props: {
782
- isRoot: true,
783
- ignoreHooks: true,
784
- },
785
- });
786
-
787
869
  logger.debug(
788
870
  "Sending notifications for schedule handoff for scheduleId: " +
789
871
  scheduleId.toString(),
790
872
  { onCallDutyPolicyScheduleId: scheduleId.toString() } as LogAttributes,
791
873
  );
792
874
 
793
- // send notification to the users.
875
+ /*
876
+ * Send the handoff notification BEFORE persisting the new roster. The
877
+ * decision to notify is a diff of the persisted-previous vs new roster; if
878
+ * we persisted first and the notification then threw (a transient DB error
879
+ * in the notification-settings / timezone lookup, or a partial send), the
880
+ * row would already show the new user, so the next tick's diff would show
881
+ * "no change" and the newly-on-call user would NEVER be told they are on
882
+ * call — the handoff page would be silently and permanently lost (audit
883
+ * F11). By notifying first, a failed send leaves the roster un-advanced, so
884
+ * the schedule is re-selected next tick and the notification is retried
885
+ * (at worst a duplicate send, which is far better than a missed page for an
886
+ * on-call system).
887
+ */
794
888
  await this.sendNotificationToUserOnScheduleHandoff({
795
889
  scheduleId: scheduleId,
796
890
  previousInformation: previousInformation,
@@ -804,6 +898,28 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
804
898
  },
805
899
  });
806
900
 
901
+ logger.debug(
902
+ "Updating schedule with new information for scheduleId: " +
903
+ scheduleId.toString(),
904
+ { onCallDutyPolicyScheduleId: scheduleId.toString() } as LogAttributes,
905
+ );
906
+
907
+ await this.updateOneById({
908
+ id: scheduleId!,
909
+ data: {
910
+ currentUserIdOnRoster: newInformation.currentUserId,
911
+ rosterHandoffAt: newInformation.handOffTimeAt,
912
+ nextUserIdOnRoster: newInformation.nextUserId,
913
+ rosterNextHandoffAt: newInformation.nextHandOffTimeAt,
914
+ rosterStartAt: newInformation.rosterStartAt,
915
+ rosterNextStartAt: newInformation.nextRosterStartAt,
916
+ },
917
+ props: {
918
+ isRoot: true,
919
+ ignoreHooks: true,
920
+ },
921
+ });
922
+
807
923
  logger.debug(
808
924
  "Returning new schedule information for scheduleId: " +
809
925
  scheduleId.toString(),
@@ -815,6 +931,7 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
815
931
 
816
932
  public async getCurrrentUserIdAndHandoffTimeInSchedule(
817
933
  scheduleId: ObjectID,
934
+ options?: { onCallDutyPolicyId?: ObjectID | undefined } | undefined,
818
935
  ): Promise<{
819
936
  rosterStartAt: Date | null;
820
937
  currentUserId: ObjectID | null;
@@ -852,6 +969,7 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
852
969
  await this.getEventByIndexInSchedule({
853
970
  scheduleId: scheduleId,
854
971
  getNumberOfEvents: 2,
972
+ onCallDutyPolicyId: options?.onCallDutyPolicyId,
855
973
  });
856
974
 
857
975
  logger.debug("Events fetched: " + JSON.stringify(events), {
@@ -904,10 +1022,31 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
904
1022
  // get start time
905
1023
  const startTime: Date | undefined = currentEvent?.start; // this is user id in string.
906
1024
  if (startTime) {
907
- logger.debug("Current rosterStartAt: " + startTime.toISOString(), {
908
- onCallDutyPolicyScheduleId: scheduleId.toString(),
909
- } as LogAttributes);
910
- resultReturn.rosterStartAt = startTime;
1025
+ logger.debug(
1026
+ "Current rosterStartAt (clamped): " + startTime.toISOString(),
1027
+ {
1028
+ onCallDutyPolicyScheduleId: scheduleId.toString(),
1029
+ } as LogAttributes,
1030
+ );
1031
+
1032
+ /*
1033
+ * currentEvent.start is clamped to the resolution window start (now).
1034
+ * Recover the true start of the current coverage window so the
1035
+ * persisted/displayed "on call since" reflects when the shift actually
1036
+ * began, not ~now (audit F12). Best-effort with a fallback to the
1037
+ * clamped value.
1038
+ */
1039
+ if (resultReturn.currentUserId) {
1040
+ resultReturn.rosterStartAt = await this.getTrueRosterStartAt({
1041
+ scheduleId: scheduleId,
1042
+ currentUserId: resultReturn.currentUserId,
1043
+ now: OneUptimeDate.getCurrentDate(),
1044
+ onCallDutyPolicyId: options?.onCallDutyPolicyId,
1045
+ fallbackStart: startTime,
1046
+ });
1047
+ } else {
1048
+ resultReturn.rosterStartAt = startTime;
1049
+ }
911
1050
  }
912
1051
  }
913
1052
 
@@ -951,6 +1090,104 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
951
1090
  return resultReturn;
952
1091
  }
953
1092
 
1093
+ /*
1094
+ * Returns the on-call policy this schedule is attached to IFF it is attached
1095
+ * to exactly one; otherwise undefined (audit F10). A lazy require avoids a
1096
+ * circular import — OnCallDutyPolicyEscalationRuleScheduleService imports this
1097
+ * service.
1098
+ */
1099
+ private async getSingleAttachedPolicyId(
1100
+ scheduleId: ObjectID,
1101
+ ): Promise<ObjectID | undefined> {
1102
+ try {
1103
+ const escalationRuleScheduleService: {
1104
+ findBy: (
1105
+ args: unknown,
1106
+ ) => Promise<Array<{ onCallDutyPolicyId?: ObjectID | undefined }>>;
1107
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
1108
+ } = require("./OnCallDutyPolicyEscalationRuleScheduleService").default;
1109
+
1110
+ const links: Array<{ onCallDutyPolicyId?: ObjectID | undefined }> =
1111
+ await escalationRuleScheduleService.findBy({
1112
+ query: { onCallDutyPolicyScheduleId: scheduleId },
1113
+ select: { onCallDutyPolicyId: true },
1114
+ limit: LIMIT_PER_PROJECT,
1115
+ skip: 0,
1116
+ props: { isRoot: true },
1117
+ });
1118
+
1119
+ const distinctPolicyIds: Set<string> = new Set<string>();
1120
+ for (const link of links) {
1121
+ const policyId: string | undefined =
1122
+ link.onCallDutyPolicyId?.toString();
1123
+ if (policyId) {
1124
+ distinctPolicyIds.add(policyId);
1125
+ }
1126
+ }
1127
+
1128
+ if (distinctPolicyIds.size === 1) {
1129
+ return new ObjectID(Array.from(distinctPolicyIds)[0]!);
1130
+ }
1131
+ } catch (err) {
1132
+ logger.error(err);
1133
+ }
1134
+
1135
+ return undefined;
1136
+ }
1137
+
1138
+ /**
1139
+ * Re-resolve and persist the roster for every schedule in the project whose
1140
+ * layers include `userId`. Called when a user override is created / updated /
1141
+ * deleted so the persisted roster, handoff notifications and on-call time logs
1142
+ * reflect the override mid-period instead of waiting for the next natural
1143
+ * handoff or the per-minute cron (which does not re-select an established
1144
+ * roster) — audit F4. Best-effort and idempotent: refreshing an unaffected
1145
+ * schedule simply recomputes the same roster; per-schedule errors are logged
1146
+ * and never abort the rest.
1147
+ */
1148
+ public async refreshRostersForUserInProject(data: {
1149
+ projectId: ObjectID;
1150
+ userId: ObjectID;
1151
+ }): Promise<void> {
1152
+ const layerUsers: Array<OnCallDutyPolicyScheduleLayerUser> =
1153
+ await OnCallDutyPolicyScheduleLayerUserService.findBy({
1154
+ query: {
1155
+ projectId: data.projectId,
1156
+ userId: data.userId,
1157
+ },
1158
+ select: {
1159
+ onCallDutyPolicyScheduleId: true,
1160
+ },
1161
+ limit: LIMIT_PER_PROJECT,
1162
+ skip: 0,
1163
+ props: {
1164
+ isRoot: true,
1165
+ },
1166
+ });
1167
+
1168
+ const scheduleIds: Set<string> = new Set<string>();
1169
+ for (const layerUser of layerUsers) {
1170
+ const scheduleId: string | undefined =
1171
+ layerUser.onCallDutyPolicyScheduleId?.toString();
1172
+ if (scheduleId) {
1173
+ scheduleIds.add(scheduleId);
1174
+ }
1175
+ }
1176
+
1177
+ for (const scheduleId of scheduleIds) {
1178
+ try {
1179
+ await this.refreshCurrentUserIdAndHandoffTimeInSchedule(
1180
+ new ObjectID(scheduleId),
1181
+ );
1182
+ } catch (err) {
1183
+ logger.error(
1184
+ `Error refreshing roster for schedule ${scheduleId} after a user override change.`,
1185
+ );
1186
+ logger.error(err);
1187
+ }
1188
+ }
1189
+ }
1190
+
954
1191
  private async getScheduleLayerProps(data: { scheduleId: ObjectID }): Promise<{
955
1192
  layerProps: Array<LayerProps>;
956
1193
  projectId: ObjectID | null;
@@ -1006,6 +1243,24 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1006
1243
  },
1007
1244
  });
1008
1245
 
1246
+ /*
1247
+ * The schedule's timezone (if set) is applied to every layer so restriction
1248
+ * wall-clock times resolve in that zone. When null (existing schedules), the
1249
+ * layer engine falls back to server-local time — unchanged legacy behavior.
1250
+ */
1251
+ const schedule: OnCallDutyPolicySchedule | null = await this.findOneById({
1252
+ id: scheduleId,
1253
+ select: {
1254
+ timezone: true,
1255
+ },
1256
+ props: {
1257
+ isRoot: true,
1258
+ },
1259
+ });
1260
+
1261
+ const scheduleTimezone: string | undefined =
1262
+ schedule?.timezone?.toString() || undefined;
1263
+
1009
1264
  const layerProps: Array<LayerProps> = [];
1010
1265
  const scheduleUserIds: Array<ObjectID> = [];
1011
1266
  const seenUserIds: Set<string> = new Set<string>();
@@ -1039,6 +1294,7 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1039
1294
  restrictionTimes: layer.restrictionTimes!,
1040
1295
  rotation: layer.rotation!,
1041
1296
  handOffTime: layer.handOffTime!,
1297
+ timezone: scheduleTimezone,
1042
1298
  });
1043
1299
  }
1044
1300
 
@@ -1114,6 +1370,217 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1114
1370
  });
1115
1371
  }
1116
1372
 
1373
+ /*
1374
+ * Compute how far out to resolve calendar events. Returns at least 1 year
1375
+ * from `from`, but extends far enough to contain (getNumberOfEvents + 1)
1376
+ * rotation periods for the slowest layer, so multi-year rotations still yield
1377
+ * a current and next event.
1378
+ */
1379
+ private computeResolutionWindowEnd(
1380
+ layerProps: Array<LayerProps>,
1381
+ from: Date,
1382
+ getNumberOfEvents: number,
1383
+ ): Date {
1384
+ const periodsNeeded: number = Math.max(2, getNumberOfEvents + 1);
1385
+
1386
+ let windowEnd: Date = from;
1387
+ let anyLayerRestricted: boolean = false;
1388
+
1389
+ for (const layer of layerProps) {
1390
+ if (this.isLayerRestricted(layer)) {
1391
+ anyLayerRestricted = true;
1392
+ }
1393
+
1394
+ if (!layer.rotation) {
1395
+ continue;
1396
+ }
1397
+
1398
+ let recurring: Recurring;
1399
+ try {
1400
+ recurring =
1401
+ layer.rotation instanceof Recurring
1402
+ ? layer.rotation
1403
+ : Recurring.fromJSON(layer.rotation as any);
1404
+ } catch {
1405
+ continue;
1406
+ }
1407
+
1408
+ let candidate: Date = from;
1409
+ for (let i: number = 0; i < periodsNeeded; i++) {
1410
+ candidate = Recurring.getNextDateInterval(candidate, recurring);
1411
+ }
1412
+
1413
+ if (OneUptimeDate.isAfter(candidate, windowEnd)) {
1414
+ windowEnd = candidate;
1415
+ }
1416
+ }
1417
+
1418
+ /*
1419
+ * Floor the window. Restricted layers can have long coverage gaps (a
1420
+ * weekend-only or business-hours restriction), so the next COVERED event may
1421
+ * be far past the naive (getNumberOfEvents + 1)-period horizon — keep a
1422
+ * generous 1-year floor for them so a current/next user is still found.
1423
+ * UNRESTRICTED layers produce exactly one event per rotation period, so the
1424
+ * per-layer candidate above already spans the events we need; flooring those
1425
+ * at a full year forced a fast (hourly/daily) rotation to materialize
1426
+ * thousands of events on every resolution, and getMultiLayerEvents then ran
1427
+ * an O(n^2) overlap merge over them — a real worker stall (audit H2). For
1428
+ * unrestricted layers a small 1-day floor is ample margin.
1429
+ */
1430
+ const floor: Date = anyLayerRestricted
1431
+ ? OneUptimeDate.addRemoveYears(from, 1)
1432
+ : OneUptimeDate.addRemoveDays(from, 1);
1433
+
1434
+ if (OneUptimeDate.isBefore(windowEnd, floor)) {
1435
+ windowEnd = floor;
1436
+ }
1437
+
1438
+ return windowEnd;
1439
+ }
1440
+
1441
+ /*
1442
+ * True when the layer carries an active (non-None) restriction. Handles both a
1443
+ * hydrated RestrictionTimes instance (exposes `restictionType`) and the raw
1444
+ * serialized JSON form ({ _type, value: { restictionType } }) so it is safe
1445
+ * regardless of where the LayerProps came from.
1446
+ */
1447
+ private isLayerRestricted(layer: LayerProps): boolean {
1448
+ const rt: unknown = layer.restrictionTimes;
1449
+ if (!rt) {
1450
+ return false;
1451
+ }
1452
+ const anyRt: {
1453
+ restictionType?: RestrictionType | string;
1454
+ value?: { restictionType?: RestrictionType | string };
1455
+ } = rt as {
1456
+ restictionType?: RestrictionType | string;
1457
+ value?: { restictionType?: RestrictionType | string };
1458
+ };
1459
+ const type: RestrictionType | string | undefined =
1460
+ anyRt.restictionType || anyRt.value?.restictionType;
1461
+ return Boolean(type) && type !== RestrictionType.None;
1462
+ }
1463
+
1464
+ /*
1465
+ * Mirror of computeResolutionWindowEnd, stepping BACKWARDS: the earliest of
1466
+ * `from` minus `periodsBack` rotation periods across the layers (at least a
1467
+ * day). Used to recover the true, un-clamped start of the current coverage
1468
+ * window for rosterStartAt (audit F12).
1469
+ */
1470
+ private computeResolutionWindowStart(
1471
+ layerProps: Array<LayerProps>,
1472
+ from: Date,
1473
+ periodsBack: number,
1474
+ ): Date {
1475
+ let windowStart: Date = OneUptimeDate.addRemoveDays(from, -1);
1476
+
1477
+ for (const layer of layerProps) {
1478
+ if (!layer.rotation) {
1479
+ continue;
1480
+ }
1481
+
1482
+ let recurring: Recurring;
1483
+ try {
1484
+ recurring =
1485
+ layer.rotation instanceof Recurring
1486
+ ? layer.rotation
1487
+ : Recurring.fromJSON(layer.rotation as any);
1488
+ } catch {
1489
+ continue;
1490
+ }
1491
+
1492
+ let candidate: Date = from;
1493
+ for (let i: number = 0; i < periodsBack; i++) {
1494
+ candidate = Recurring.getNextDateInterval(candidate, recurring, true);
1495
+ }
1496
+
1497
+ if (OneUptimeDate.isBefore(candidate, windowStart)) {
1498
+ windowStart = candidate;
1499
+ }
1500
+ }
1501
+
1502
+ return windowStart;
1503
+ }
1504
+
1505
+ /*
1506
+ * Recover the TRUE start of the current on-call coverage window for
1507
+ * `currentUserId`. getEventByIndexInSchedule always expands from `now`, and
1508
+ * LayerUtil clamps the first event's start to the window start, so the current
1509
+ * event's start reads ~now rather than when the shift actually began. We
1510
+ * re-expand over a bounded lookback window and return the start of the segment
1511
+ * that covers `now` for the current user. Best-effort: any failure (or no
1512
+ * covering segment found) falls back to the clamped value, so the who-is-paged
1513
+ * path is never affected (audit F12).
1514
+ */
1515
+ private async getTrueRosterStartAt(data: {
1516
+ scheduleId: ObjectID;
1517
+ currentUserId: ObjectID;
1518
+ now: Date;
1519
+ onCallDutyPolicyId?: ObjectID | undefined;
1520
+ fallbackStart: Date;
1521
+ }): Promise<Date> {
1522
+ try {
1523
+ const { layerProps, projectId, scheduleUserIds } =
1524
+ await this.getScheduleLayerProps({ scheduleId: data.scheduleId });
1525
+
1526
+ if (layerProps.length === 0) {
1527
+ return data.fallbackStart;
1528
+ }
1529
+
1530
+ const windowStart: Date = this.computeResolutionWindowStart(
1531
+ layerProps,
1532
+ data.now,
1533
+ 2,
1534
+ );
1535
+ const windowEnd: Date = OneUptimeDate.addRemoveSeconds(data.now, 1);
1536
+
1537
+ let events: Array<CalendarEvent> = this.layerUtil.getMultiLayerEvents({
1538
+ layers: layerProps,
1539
+ calendarStartDate: windowStart,
1540
+ calendarEndDate: windowEnd,
1541
+ });
1542
+
1543
+ if (projectId && events.length > 0) {
1544
+ const overrides: Array<UserOverrideRecord> =
1545
+ await this.fetchOverridesForSchedule({
1546
+ projectId,
1547
+ scheduleUserIds,
1548
+ windowStart,
1549
+ windowEnd,
1550
+ currentOnCallDutyPolicyId: data.onCallDutyPolicyId,
1551
+ });
1552
+
1553
+ if (overrides.length > 0) {
1554
+ events = UserOverrideUtil.applyOverridesToEvents({
1555
+ events,
1556
+ overrides,
1557
+ currentOnCallDutyPolicyId: data.onCallDutyPolicyId?.toString(),
1558
+ });
1559
+ }
1560
+ }
1561
+
1562
+ const currentUserIdStr: string = data.currentUserId.toString();
1563
+
1564
+ for (const event of events) {
1565
+ if (!event) {
1566
+ continue;
1567
+ }
1568
+
1569
+ if (
1570
+ event.title === currentUserIdStr &&
1571
+ OneUptimeDate.isOnOrBefore(event.start, data.now) &&
1572
+ OneUptimeDate.isOnOrAfter(event.end, data.now)
1573
+ ) {
1574
+ return event.start;
1575
+ }
1576
+ }
1577
+ } catch (err) {
1578
+ logger.error(err);
1579
+ }
1580
+
1581
+ return data.fallbackStart;
1582
+ }
1583
+
1117
1584
  public async getEventByIndexInSchedule(data: {
1118
1585
  scheduleId: ObjectID;
1119
1586
  getNumberOfEvents: number; // which event would you like to get. First event, second event, etc.
@@ -1150,9 +1617,18 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
1150
1617
  onCallDutyPolicyScheduleId: data.scheduleId.toString(),
1151
1618
  } as LogAttributes);
1152
1619
 
1153
- const currentEndTime: Date = OneUptimeDate.addRemoveYears(
1620
+ /*
1621
+ * Size the resolution window from the layers' rotations rather than a fixed
1622
+ * 1 year. A rotation period longer than a year (e.g. a 3-year rotation)
1623
+ * would otherwise clamp the current event's end to now+1yr and report a
1624
+ * bogus handoff time with no "next" user. The window is at least 1 year and
1625
+ * large enough to contain the requested number of events for the slowest
1626
+ * layer.
1627
+ */
1628
+ const currentEndTime: Date = this.computeResolutionWindowEnd(
1629
+ layerProps,
1154
1630
  currentStartTime,
1155
- 1,
1631
+ data.getNumberOfEvents,
1156
1632
  );
1157
1633
  logger.debug("Current end time: " + currentEndTime.toISOString(), {
1158
1634
  onCallDutyPolicyScheduleId: data.scheduleId.toString(),