@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.
- package/Models/DatabaseModels/AIRun.ts +50 -0
- package/Models/DatabaseModels/IncidentEpisodeRoleMember.ts +1 -1
- package/Models/DatabaseModels/IncidentMember.ts +1 -1
- package/Models/DatabaseModels/IncomingCallPolicy.ts +13 -0
- package/Models/DatabaseModels/Index.ts +3 -0
- package/Models/DatabaseModels/MigrationFailure.ts +170 -0
- package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +51 -2
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +44 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +6 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.ts +6 -0
- package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +1 -1
- package/Models/DatabaseModels/OnCallDutyPolicyUserOverride.ts +2 -2
- package/Models/DatabaseModels/Project.ts +60 -0
- package/Server/API/AIInvestigationAPI.ts +148 -0
- package/Server/API/AlertAPI.ts +1 -1
- package/Server/API/IncidentAPI.ts +2 -2
- package/Server/API/IncidentEpisodeAPI.ts +1 -1
- package/Server/API/LlmProviderAPI.ts +1 -0
- package/Server/API/ScheduledMaintenanceAPI.ts +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.ts +81 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +14 -0
- package/Server/Infrastructure/PostgresDatabase.ts +11 -0
- package/Server/Services/AIService.ts +74 -2
- package/Server/Services/AlertService.ts +25 -0
- package/Server/Services/AnalyticsDatabaseService.ts +10 -1
- package/Server/Services/DatabaseService.ts +10 -1
- package/Server/Services/IncidentService.ts +26 -1
- package/Server/Services/IncidentStateTimelineService.ts +19 -0
- package/Server/Services/IncomingCallPolicyEscalationRuleService.ts +75 -2
- package/Server/Services/IncomingCallPolicyService.ts +42 -1
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/MigrationFailureService.ts +9 -0
- package/Server/Services/MonitorProbeService.ts +33 -7
- package/Server/Services/MonitorService.ts +19 -7
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +185 -41
- package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +23 -15
- package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +39 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +148 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +11 -2
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +502 -26
- package/Server/Services/OnCallDutyPolicyTimeLogService.ts +49 -7
- package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +218 -4
- package/Server/Services/ProjectCallSMSConfigService.ts +82 -1
- package/Server/Services/TeamMemberService.ts +6 -0
- package/Server/Services/UserNotificationRuleService.ts +13 -42
- package/Server/Services/UserOnCallLogService.ts +90 -0
- package/Server/Services/UserOnCallLogTimelineService.ts +43 -1
- package/Server/Types/Database/QueryUtil.ts +57 -0
- package/Server/Utils/AI/AlertAIContextBuilder.ts +6 -1
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +14 -2
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +119 -11
- package/Server/Utils/AI/IncidentAIContextBuilder.ts +18 -3
- package/Server/Utils/AI/IncidentEpisodeAIContextBuilder.ts +6 -1
- package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +12 -2
- package/Server/Utils/AI/Sentinel/AlertInvestigationRunner.ts +141 -0
- package/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.ts +178 -0
- package/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.ts +105 -0
- package/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.ts +405 -0
- package/Server/Utils/AI/Sentinel/SentinelMemory.ts +167 -0
- package/Server/Utils/AI/Toolbox/IncidentTools.ts +183 -1
- package/Server/Utils/AI/Toolbox/Index.ts +15 -1
- package/Server/Utils/AI/Toolbox/RecentChangesTools.ts +208 -0
- package/Server/Utils/AI/Toolbox/SentinelActionTools.ts +577 -0
- package/Server/Utils/AI/Toolbox/Serializer.ts +42 -0
- package/Server/Utils/Database/MigrationFailureLog.ts +240 -0
- package/Server/Utils/IncomingCallPhoneNumber.ts +41 -0
- package/Server/Utils/LLM/LLMService.ts +110 -11
- package/Server/Utils/Telemetry/IoTSnapshotScan.ts +12 -9
- package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +12 -9
- package/Tests/Server/API/BaseAPI.test.ts +1 -0
- package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +0 -8
- package/Tests/Server/Services/OnCallDutyPolicyScheduleLayerUserReorder.test.ts +233 -0
- package/Tests/Server/Services/OnCallDutyPolicyTimeLogServiceScoping.test.ts +242 -0
- package/Tests/Server/Services/OnCallDutyPolicyUserOverrideEdit.test.ts +198 -0
- package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +495 -0
- package/Tests/Server/Types/Database/QueryUtil.test.ts +74 -0
- package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +78 -0
- package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +68 -0
- package/Tests/Types/DateExhaustiveTimezone.test.ts +700 -0
- package/Tests/Types/DateTimezoneWallClock.test.ts +99 -0
- package/Tests/Types/Events/Recurring.test.ts +22 -9
- package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixes.test.ts +291 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixesRound2.test.ts +398 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeekendGap.test.ts +114 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeeklyDayShift.test.ts +76 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyMutation.test.ts +128 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyProbe.test.ts +203 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDiffFuzz.test.ts +322 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilEdgeCases.test.ts +280 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveCurrentUser.test.ts +863 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveDaily.test.ts +900 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveMultiLayer.test.ts +1104 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +957 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveWeekly.test.ts +1095 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilInvariants.test.ts +232 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +227 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMonthYearAudit.test.ts +255 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerAudit.test.ts +388 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerEdge.test.ts +300 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilOvernightDSTAudit.test.ts +158 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapFix.test.ts +253 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapRepro.test.ts +157 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRotationFixes.test.ts +414 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilTimezone.test.ts +243 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilWeekendGapRepro.test.ts +112 -0
- package/Tests/Types/OnCallDutyPolicy/OverrideLensAudit.test.ts +117 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +8 -6
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesDefaultDayAudit.test.ts +112 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesExhaustive.test.ts +821 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesMutationInvariant.test.ts +886 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtil.test.ts +260 -0
- package/Tests/Types/OnCallDutyPolicy/UserOverrideUtil.test.ts +255 -0
- package/Tests/Types/OnCallDutyPolicy/UserOverrideUtilExhaustive.test.ts +1126 -0
- package/Types/Date.ts +246 -15
- package/Types/OnCallDutyPolicy/Layer.ts +790 -260
- package/Types/OnCallDutyPolicy/RestrictionTimes.ts +39 -13
- package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +257 -0
- package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +21 -5
- package/UI/Components/Calendar/Calendar.tsx +8 -2
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +5 -5
- package/UI/Components/Events/RecurringFieldElement.tsx +16 -0
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +8 -0
- package/UI/Components/RadioButtons/BasicRadioButtons.tsx +1 -1
- package/build/dist/Models/DatabaseModels/AIRun.js +52 -0
- package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentMember.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js +10 -0
- package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MigrationFailure.js +196 -0
- package/build/dist/Models/DatabaseModels/MigrationFailure.js.map +1 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +52 -2
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +45 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +6 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js +6 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js +2 -2
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +62 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +107 -0
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -0
- package/build/dist/Server/API/AlertAPI.js +1 -1
- package/build/dist/Server/API/IncidentAPI.js +2 -2
- package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
- package/build/dist/Server/API/LlmProviderAPI.js +1 -0
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js +38 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783598145111-AddEnableAutomaticIncidentInvestigationToProject.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783615884106-AddInvestigationSubjectToAIRun.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783632447975-AddEnableAutomaticAlertInvestigationToProject.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/PostgresDatabase.js +9 -0
- package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
- package/build/dist/Server/Services/AIService.js +53 -2
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +24 -0
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +10 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +10 -1
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +25 -1
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js +55 -2
- package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js.map +1 -1
- package/build/dist/Server/Services/IncomingCallPolicyService.js +40 -0
- package/build/dist/Server/Services/IncomingCallPolicyService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MigrationFailureService.js +9 -0
- package/build/dist/Server/Services/MigrationFailureService.js.map +1 -0
- package/build/dist/Server/Services/MonitorProbeService.js +33 -7
- package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +12 -5
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +81 -22
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +134 -31
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +21 -13
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +36 -0
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +121 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +11 -2
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +409 -36
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js +52 -8
- package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +170 -3
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
- package/build/dist/Server/Services/ProjectCallSMSConfigService.js +72 -0
- package/build/dist/Server/Services/ProjectCallSMSConfigService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +6 -0
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +12 -30
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallLogService.js +75 -0
- package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallLogTimelineService.js +31 -1
- package/build/dist/Server/Services/UserOnCallLogTimelineService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +45 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +6 -1
- package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +13 -2
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +59 -11
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
- package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +18 -3
- package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js +6 -1
- package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js +12 -2
- package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/Sentinel/AlertInvestigationRunner.js +119 -0
- package/build/dist/Server/Utils/AI/Sentinel/AlertInvestigationRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js +147 -0
- package/build/dist/Server/Utils/AI/Sentinel/IncidentInvestigationRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.js +93 -0
- package/build/dist/Server/Utils/AI/Sentinel/IncidentPostmortemRunner.js.map +1 -0
- package/build/dist/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.js +311 -0
- package/build/dist/Server/Utils/AI/Sentinel/SentinelInvestigationEngine.js.map +1 -0
- package/build/dist/Server/Utils/AI/Sentinel/SentinelMemory.js +143 -0
- package/build/dist/Server/Utils/AI/Sentinel/SentinelMemory.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +152 -1
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +10 -1
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js +165 -0
- package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/SentinelActionTools.js +451 -0
- package/build/dist/Server/Utils/AI/Toolbox/SentinelActionTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +40 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -1
- package/build/dist/Server/Utils/Database/MigrationFailureLog.js +174 -0
- package/build/dist/Server/Utils/Database/MigrationFailureLog.js.map +1 -0
- package/build/dist/Server/Utils/IncomingCallPhoneNumber.js +30 -0
- package/build/dist/Server/Utils/IncomingCallPhoneNumber.js.map +1 -0
- package/build/dist/Server/Utils/LLM/LLMService.js +81 -5
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +11 -8
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +11 -8
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
- package/build/dist/Types/Date.js +185 -15
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +602 -171
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js +27 -13
- package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +159 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +20 -5
- package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
- package/build/dist/UI/Components/Calendar/Calendar.js +2 -2
- package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Events/RecurringFieldElement.js +13 -0
- package/build/dist/UI/Components/Events/RecurringFieldElement.js.map +1 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js +1 -1
- package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js.map +1 -1
- package/package.json +1 -1
|
@@ -16,6 +16,13 @@ export interface LayerProps {
|
|
|
16
16
|
restrictionTimes: RestrictionTimes;
|
|
17
17
|
handOffTime: Date;
|
|
18
18
|
rotation: Recurring;
|
|
19
|
+
/*
|
|
20
|
+
* IANA timezone (e.g. "America/New_York") the schedule's wall-clock
|
|
21
|
+
* restriction/handoff times are authored in. When omitted (existing
|
|
22
|
+
* schedules), restriction windows are reconstructed in the server's local
|
|
23
|
+
* time exactly as before — fully backward compatible.
|
|
24
|
+
*/
|
|
25
|
+
timezone?: string | undefined;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
export interface EventProps extends LayerProps {
|
|
@@ -34,6 +41,15 @@ export interface PriorityCalendarEvents extends CalendarEvent {
|
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export default class LayerUtil {
|
|
44
|
+
/*
|
|
45
|
+
* The timezone of the layer currently being expanded. Set at the start of
|
|
46
|
+
* getEvents and read by the restriction-trimming helpers so wall-clock
|
|
47
|
+
* restriction windows resolve in the schedule's zone. undefined => local
|
|
48
|
+
* time (legacy behavior). getEvents runs synchronously, so this per-call
|
|
49
|
+
* field is not subject to interleaving.
|
|
50
|
+
*/
|
|
51
|
+
private timezone: string | undefined = undefined;
|
|
52
|
+
|
|
37
53
|
public getEvents(
|
|
38
54
|
data: EventProps,
|
|
39
55
|
options?:
|
|
@@ -50,6 +66,8 @@ export default class LayerUtil {
|
|
|
50
66
|
|
|
51
67
|
data = this.sanitizeData(data);
|
|
52
68
|
|
|
69
|
+
this.timezone = data.timezone;
|
|
70
|
+
|
|
53
71
|
let start: Date = data.calendarStartDate;
|
|
54
72
|
const end: Date = data.calendarEndDate;
|
|
55
73
|
|
|
@@ -78,7 +96,10 @@ export default class LayerUtil {
|
|
|
78
96
|
|
|
79
97
|
// before we do this, we need to update the user index.
|
|
80
98
|
|
|
81
|
-
|
|
99
|
+
const currentUserResolution: {
|
|
100
|
+
currentUserIndex: number;
|
|
101
|
+
currentPeriodStart: Date;
|
|
102
|
+
} = this.getCurrentUserIndexBasedOnHandoffTime({
|
|
82
103
|
rotation,
|
|
83
104
|
handOffTime,
|
|
84
105
|
currentUserIndex,
|
|
@@ -87,6 +108,17 @@ export default class LayerUtil {
|
|
|
87
108
|
currentEventStartTime,
|
|
88
109
|
restrictionTimes: data.restrictionTimes,
|
|
89
110
|
});
|
|
111
|
+
currentUserIndex = currentUserResolution.currentUserIndex;
|
|
112
|
+
|
|
113
|
+
/*
|
|
114
|
+
* True (un-clamped) start of the first rotation period we are about to
|
|
115
|
+
* expand. When the calendar window starts partway through a period (the
|
|
116
|
+
* live "who is on call now" path always starts its window at the current
|
|
117
|
+
* instant), currentEventStartTime is clamped to that instant. The advance
|
|
118
|
+
* guard in the loop below uses this to decide whether the first period
|
|
119
|
+
* consumed a rotation turn based on its FULL-span coverage (audit F2).
|
|
120
|
+
*/
|
|
121
|
+
const firstPeriodTrueStart: Date = currentUserResolution.currentPeriodStart;
|
|
90
122
|
|
|
91
123
|
// update handoff time to the same day as current start time
|
|
92
124
|
|
|
@@ -120,10 +152,42 @@ export default class LayerUtil {
|
|
|
120
152
|
return events;
|
|
121
153
|
}
|
|
122
154
|
|
|
123
|
-
|
|
124
|
-
|
|
155
|
+
/*
|
|
156
|
+
* Bound the loop by the actual calendar window instead of a fixed count.
|
|
157
|
+
* Each iteration advances currentEventStartTime by at least one rotation
|
|
158
|
+
* period (fully-restricted periods produce no event but still advance the
|
|
159
|
+
* handoff), so at most ~windowUnits/periodUnits periods can fall inside
|
|
160
|
+
* [start, end]. A fixed cap of 100 silently truncated long windows for short
|
|
161
|
+
* rotations (audit F1) and could even return ZERO events — the schedule
|
|
162
|
+
* reporting nobody on-call and no next user — when "now" sat in a restriction
|
|
163
|
+
* gap longer than 100 periods (audit F8, e.g. an hourly rotation with a
|
|
164
|
+
* weekend-only restriction). Scale the cap to the window with a generous
|
|
165
|
+
* margin, keeping a hard ceiling to bound pathological inputs.
|
|
166
|
+
*/
|
|
167
|
+
const rawRotationCount: number = rotation.intervalCount.toNumber();
|
|
168
|
+
const periodUnitsForBound: number =
|
|
169
|
+
Number.isFinite(rawRotationCount) && rawRotationCount >= 1
|
|
170
|
+
? Math.floor(rawRotationCount)
|
|
171
|
+
: 1;
|
|
172
|
+
const windowUnits: number = this.getUnitsBetweenDates(
|
|
173
|
+
start,
|
|
174
|
+
end,
|
|
175
|
+
rotation.intervalType,
|
|
176
|
+
);
|
|
177
|
+
const maxLoopCount: number = Math.min(
|
|
178
|
+
1000000,
|
|
179
|
+
Math.max(100, Math.ceil(windowUnits / periodUnitsForBound) + 10),
|
|
180
|
+
);
|
|
125
181
|
let loopCount: number = 0;
|
|
126
182
|
|
|
183
|
+
/*
|
|
184
|
+
* The first loop iteration expands the rotation period that CONTAINS the
|
|
185
|
+
* window start; its currentEventStartTime may be clamped to the window
|
|
186
|
+
* start (now) rather than the true period start. Tracked so the rotation
|
|
187
|
+
* advance can be decided against the period's full span (audit F2).
|
|
188
|
+
*/
|
|
189
|
+
let isFirstPeriod: boolean = true;
|
|
190
|
+
|
|
127
191
|
while (!hasReachedTheEndOfTheCalendar) {
|
|
128
192
|
loopCount++;
|
|
129
193
|
if (loopCount > maxLoopCount) {
|
|
@@ -131,6 +195,9 @@ export default class LayerUtil {
|
|
|
131
195
|
}
|
|
132
196
|
currentEventEndTime = handOffTime;
|
|
133
197
|
|
|
198
|
+
// The rotation boundary that ends this period, before any clamp to `end`.
|
|
199
|
+
const periodBoundaryEnd: Date = handOffTime;
|
|
200
|
+
|
|
134
201
|
// if current event start time and end time is the same then increase current event start time by 1 second.
|
|
135
202
|
|
|
136
203
|
if (OneUptimeDate.isSame(currentEventStartTime, currentEventEndTime)) {
|
|
@@ -162,14 +229,21 @@ export default class LayerUtil {
|
|
|
162
229
|
restrictionTimes: data.restrictionTimes,
|
|
163
230
|
});
|
|
164
231
|
|
|
165
|
-
|
|
166
|
-
|
|
232
|
+
/*
|
|
233
|
+
* push() instead of rebuilding the array with [...events, ...new] every
|
|
234
|
+
* iteration. The spread reallocated and copied the whole accumulated array
|
|
235
|
+
* each period — O(n^2) over the loop — which, combined with a window sized
|
|
236
|
+
* to a slow layer, made a fast (e.g. hourly) layer's expansion quadratic
|
|
237
|
+
* (audit H2). Each period contributes only a handful of segments, so the
|
|
238
|
+
* spread of the small per-period array as push args is safe.
|
|
239
|
+
*/
|
|
240
|
+
events.push(
|
|
167
241
|
...this.getCalendarEventsFromStartAndEndDates(
|
|
168
242
|
trimmedStartAndEndTimes,
|
|
169
243
|
data.users,
|
|
170
244
|
currentUserIndex,
|
|
171
245
|
),
|
|
172
|
-
|
|
246
|
+
);
|
|
173
247
|
|
|
174
248
|
if (options?.getNumberOfEvents !== undefined) {
|
|
175
249
|
if (events.length >= options.getNumberOfEvents) {
|
|
@@ -193,18 +267,50 @@ export default class LayerUtil {
|
|
|
193
267
|
});
|
|
194
268
|
|
|
195
269
|
/*
|
|
196
|
-
* Only advance the rotation if
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* issue #2413.
|
|
270
|
+
* Only advance the rotation if this rotation period actually produced
|
|
271
|
+
* coverage. Otherwise the user "lost" their turn to a fully restricted
|
|
272
|
+
* window (e.g. a weekend with Mon-Fri restrictions), which would skip
|
|
273
|
+
* rotations and break ordering across the gap. See issue #2413.
|
|
201
274
|
*/
|
|
202
|
-
|
|
275
|
+
let periodProducedCoverage: boolean = trimmedStartAndEndTimes.length > 0;
|
|
276
|
+
|
|
277
|
+
/*
|
|
278
|
+
* First-period correction (audit F2): when the window starts partway
|
|
279
|
+
* through the current period AND begins after that period's restriction
|
|
280
|
+
* window has already closed (the live roster refresh resolving in a
|
|
281
|
+
* daily/weekend off-hours gap), the clamped [now, periodEnd] slice trims
|
|
282
|
+
* to nothing even though the period DID have coverage earlier. Deciding
|
|
283
|
+
* the advance on that empty clamped slice carried the current user into
|
|
284
|
+
* the next period, so every subsequent shift resolved one user off from
|
|
285
|
+
* the calendar/full expansion — paging/notifying the wrong "next" user.
|
|
286
|
+
* Re-evaluate the advance against the period's FULL span so a
|
|
287
|
+
* partially-elapsed period still consumes its rotation turn, while a
|
|
288
|
+
* genuinely fully-restricted period (full-span trim also empty) still
|
|
289
|
+
* correctly skips its turn and preserves the #2413 behavior.
|
|
290
|
+
*/
|
|
291
|
+
if (
|
|
292
|
+
isFirstPeriod &&
|
|
293
|
+
!periodProducedCoverage &&
|
|
294
|
+
data.restrictionTimes &&
|
|
295
|
+
data.restrictionTimes.restictionType !== RestrictionType.None
|
|
296
|
+
) {
|
|
297
|
+
const fullSpanTrim: Array<StartAndEndTime> =
|
|
298
|
+
this.trimStartAndEndTimesBasedOnRestrictionTimes({
|
|
299
|
+
eventStartTime: firstPeriodTrueStart,
|
|
300
|
+
eventEndTime: periodBoundaryEnd,
|
|
301
|
+
restrictionTimes: data.restrictionTimes,
|
|
302
|
+
});
|
|
303
|
+
periodProducedCoverage = fullSpanTrim.length > 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (periodProducedCoverage) {
|
|
203
307
|
currentUserIndex = this.incrementUserIndex(
|
|
204
308
|
currentUserIndex,
|
|
205
309
|
data.users.length,
|
|
206
310
|
);
|
|
207
311
|
}
|
|
312
|
+
|
|
313
|
+
isFirstPeriod = false;
|
|
208
314
|
}
|
|
209
315
|
|
|
210
316
|
// increment ids of all the events and return them, to make sure they are unique
|
|
@@ -287,162 +393,127 @@ export default class LayerUtil {
|
|
|
287
393
|
return data.handOffTime;
|
|
288
394
|
}
|
|
289
395
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
if (intervalBetweenStartTimeAndHandoffTime < rotationInterval) {
|
|
303
|
-
intervalBetweenStartTimeAndHandoffTime = rotationInterval;
|
|
304
|
-
} else if (
|
|
305
|
-
intervalBetweenStartTimeAndHandoffTime % rotationInterval !==
|
|
306
|
-
0
|
|
307
|
-
) {
|
|
308
|
-
intervalBetweenStartTimeAndHandoffTime += rotationInterval;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// add intervalBetweenStartTimeAndHandoffTime to handoff time
|
|
312
|
-
|
|
313
|
-
handOffTime = OneUptimeDate.addRemoveDays(
|
|
314
|
-
handOffTime,
|
|
315
|
-
intervalBetweenStartTimeAndHandoffTime,
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
if (OneUptimeDate.isOnOrBefore(handOffTime, data.currentEventStartTime)) {
|
|
319
|
-
handOffTime = OneUptimeDate.addRemoveDays(handOffTime, 1);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return handOffTime;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (data.rotation.intervalType === EventInterval.Hour) {
|
|
326
|
-
intervalBetweenStartTimeAndHandoffTime =
|
|
327
|
-
OneUptimeDate.getHoursBetweenTwoDatesInclusive(
|
|
328
|
-
handOffTime,
|
|
329
|
-
data.currentEventStartTime,
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
if (intervalBetweenStartTimeAndHandoffTime < rotationInterval) {
|
|
333
|
-
intervalBetweenStartTimeAndHandoffTime = rotationInterval;
|
|
334
|
-
} else if (
|
|
335
|
-
intervalBetweenStartTimeAndHandoffTime % rotationInterval !==
|
|
336
|
-
0
|
|
337
|
-
) {
|
|
338
|
-
intervalBetweenStartTimeAndHandoffTime += rotationInterval;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// add intervalBetweenStartTimeAndHandoffTime to handoff time
|
|
342
|
-
|
|
343
|
-
handOffTime = OneUptimeDate.addRemoveHours(
|
|
344
|
-
handOffTime,
|
|
345
|
-
intervalBetweenStartTimeAndHandoffTime,
|
|
346
|
-
);
|
|
396
|
+
const rawRotationInterval: number = data.rotation.intervalCount.toNumber();
|
|
397
|
+
/*
|
|
398
|
+
* Defensive clamp: an invalid interval count (0, NaN, negative) would make
|
|
399
|
+
* the alignment below produce an Invalid Date and spin the main getEvents
|
|
400
|
+
* loop. Treat any invalid value as a single unit.
|
|
401
|
+
*/
|
|
402
|
+
const rotationInterval: number =
|
|
403
|
+
Number.isFinite(rawRotationInterval) && rawRotationInterval >= 1
|
|
404
|
+
? Math.floor(rawRotationInterval)
|
|
405
|
+
: 1;
|
|
347
406
|
|
|
348
|
-
|
|
349
|
-
handOffTime = OneUptimeDate.addRemoveHours(handOffTime, 1);
|
|
350
|
-
}
|
|
407
|
+
const intervalType: EventInterval = data.rotation.intervalType;
|
|
351
408
|
|
|
352
|
-
|
|
409
|
+
/*
|
|
410
|
+
* Rotation boundaries are exactly handOffTime + k * rotationInterval units
|
|
411
|
+
* (k a non-negative integer). Return the SMALLEST such boundary that is
|
|
412
|
+
* strictly after currentEventStartTime.
|
|
413
|
+
*
|
|
414
|
+
* We start from the floor of the whole-period distance and step UP one full
|
|
415
|
+
* rotation period at a time until strictly after the target. This:
|
|
416
|
+
* - stays on the interval grid (multiples of rotationInterval), so
|
|
417
|
+
* intervalCount >= 2 rotations never drift onto an off-grid boundary
|
|
418
|
+
* (audit HIGH-1); and
|
|
419
|
+
* - never OVERSHOOTS by a whole period. The previous
|
|
420
|
+
* ceil(getUnitsInclusive / interval) * interval formula used an
|
|
421
|
+
* INCLUSIVE unit count, which overshot by a full period for positions in
|
|
422
|
+
* the last partial period before a boundary — and a DST offset shift
|
|
423
|
+
* could push the inclusive count across an even/odd threshold — yielding
|
|
424
|
+
* a first period that spanned two rotations and resolved the wrong
|
|
425
|
+
* current/next on-call user for intervalCount >= 2 rotations.
|
|
426
|
+
* addRotationUnits carries the timezone per interval type (wall-clock across
|
|
427
|
+
* DST for Day/Week/Month/Year; absolute for Hour), matching the main loop.
|
|
428
|
+
*/
|
|
429
|
+
const unitsBetween: number = this.getUnitsBetweenDates(
|
|
430
|
+
data.handOffTime,
|
|
431
|
+
data.currentEventStartTime,
|
|
432
|
+
intervalType,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
let periods: number = Math.floor(unitsBetween / rotationInterval);
|
|
436
|
+
if (!Number.isFinite(periods) || periods < 0) {
|
|
437
|
+
periods = 0;
|
|
353
438
|
}
|
|
354
439
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
440
|
+
/*
|
|
441
|
+
* Compute the boundary `periods` rotation periods after the anchor. For
|
|
442
|
+
* Month/Year this ITERATES one period at a time (see addRotationPeriods),
|
|
443
|
+
* because moment end-of-month-clamps a single multiplied add differently
|
|
444
|
+
* than stepping period-by-period — e.g. Jan-31 + 11 months multiplied =
|
|
445
|
+
* Dec-31, but eleven iterated +1-month steps = Dec-28. countElapsedRotation
|
|
446
|
+
* Periods and the main getEvents loop both walk the ITERATED grid, so a
|
|
447
|
+
* multiplied step here produced handoff boundaries off that grid and paged
|
|
448
|
+
* the wrong on-call user for Month/Year rotations anchored on day 29-31 (or
|
|
449
|
+
* Feb-29 for Year) — audit H1.
|
|
450
|
+
*/
|
|
451
|
+
let handOffTime: Date = this.addRotationPeriods(
|
|
452
|
+
data.handOffTime,
|
|
453
|
+
periods,
|
|
454
|
+
rotationInterval,
|
|
455
|
+
intervalType,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
let safety: number = 0;
|
|
459
|
+
while (
|
|
460
|
+
OneUptimeDate.isOnOrBefore(handOffTime, data.currentEventStartTime) &&
|
|
461
|
+
safety < 1000000
|
|
462
|
+
) {
|
|
463
|
+
periods++;
|
|
464
|
+
/*
|
|
465
|
+
* Step exactly ONE rotation period from the PREVIOUS boundary (not a fresh
|
|
466
|
+
* multiplied add from the anchor), so Month/Year stay on the same iterated,
|
|
467
|
+
* calendar-clamped grid as the initial jump above.
|
|
468
|
+
*/
|
|
469
|
+
handOffTime = this.addRotationUnits(
|
|
374
470
|
handOffTime,
|
|
375
|
-
|
|
471
|
+
rotationInterval,
|
|
472
|
+
intervalType,
|
|
376
473
|
);
|
|
377
|
-
|
|
378
|
-
if (OneUptimeDate.isOnOrBefore(handOffTime, data.currentEventStartTime)) {
|
|
379
|
-
handOffTime = OneUptimeDate.addRemoveWeeks(handOffTime, 1);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return handOffTime;
|
|
474
|
+
safety++;
|
|
383
475
|
}
|
|
384
476
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
OneUptimeDate.getMonthsBetweenTwoDatesInclusive(
|
|
388
|
-
handOffTime,
|
|
389
|
-
data.currentEventStartTime,
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
if (intervalBetweenStartTimeAndHandoffTime < rotationInterval) {
|
|
393
|
-
intervalBetweenStartTimeAndHandoffTime = rotationInterval;
|
|
394
|
-
} else if (
|
|
395
|
-
intervalBetweenStartTimeAndHandoffTime % rotationInterval !==
|
|
396
|
-
0
|
|
397
|
-
) {
|
|
398
|
-
intervalBetweenStartTimeAndHandoffTime += rotationInterval;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// add intervalBetweenStartTimeAndHandoffTime to handoff time
|
|
402
|
-
|
|
403
|
-
handOffTime = OneUptimeDate.addRemoveMonths(
|
|
404
|
-
handOffTime,
|
|
405
|
-
intervalBetweenStartTimeAndHandoffTime,
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
if (OneUptimeDate.isOnOrBefore(handOffTime, data.currentEventStartTime)) {
|
|
409
|
-
handOffTime = OneUptimeDate.addRemoveMonths(handOffTime, 1);
|
|
410
|
-
}
|
|
477
|
+
return handOffTime;
|
|
478
|
+
}
|
|
411
479
|
|
|
412
|
-
|
|
480
|
+
/*
|
|
481
|
+
* Advance `anchor` by `numberOfPeriods` rotation periods (each period =
|
|
482
|
+
* rotationInterval units of intervalType). Hour/Day/Week have no calendar-
|
|
483
|
+
* length clamping, so a single multiplied add is exact and O(1). Month/Year
|
|
484
|
+
* DO clamp (moment: Jan-31 + 1mo = Feb-28, then Feb-28 + 1mo = Mar-28), so a
|
|
485
|
+
* multiplied add does NOT equal iterating; we step one period at a time to
|
|
486
|
+
* stay on the same grid the rest of the engine walks (audit H1). The period
|
|
487
|
+
* count for Month/Year is small even over decades, so iterating is cheap.
|
|
488
|
+
*/
|
|
489
|
+
private addRotationPeriods(
|
|
490
|
+
anchor: Date,
|
|
491
|
+
numberOfPeriods: number,
|
|
492
|
+
rotationInterval: number,
|
|
493
|
+
intervalType: EventInterval,
|
|
494
|
+
): Date {
|
|
495
|
+
if (numberOfPeriods <= 0) {
|
|
496
|
+
return anchor;
|
|
413
497
|
}
|
|
414
498
|
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
} else if (
|
|
425
|
-
intervalBetweenStartTimeAndHandoffTime % rotationInterval !==
|
|
426
|
-
0
|
|
427
|
-
) {
|
|
428
|
-
intervalBetweenStartTimeAndHandoffTime += rotationInterval;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// add intervalBetweenStartTimeAndHandoffTime to handoff time
|
|
432
|
-
|
|
433
|
-
handOffTime = OneUptimeDate.addRemoveYears(
|
|
434
|
-
handOffTime,
|
|
435
|
-
intervalBetweenStartTimeAndHandoffTime,
|
|
436
|
-
);
|
|
437
|
-
|
|
438
|
-
if (OneUptimeDate.isOnOrBefore(handOffTime, data.currentEventStartTime)) {
|
|
439
|
-
handOffTime = OneUptimeDate.addRemoveYears(handOffTime, 1);
|
|
499
|
+
if (
|
|
500
|
+
intervalType === EventInterval.Month ||
|
|
501
|
+
intervalType === EventInterval.Year
|
|
502
|
+
) {
|
|
503
|
+
let result: Date = anchor;
|
|
504
|
+
let safety: number = 0;
|
|
505
|
+
while (safety < numberOfPeriods && safety < 1000000) {
|
|
506
|
+
result = this.addRotationUnits(result, rotationInterval, intervalType);
|
|
507
|
+
safety++;
|
|
440
508
|
}
|
|
441
|
-
|
|
442
|
-
return handOffTime;
|
|
509
|
+
return result;
|
|
443
510
|
}
|
|
444
511
|
|
|
445
|
-
return
|
|
512
|
+
return this.addRotationUnits(
|
|
513
|
+
anchor,
|
|
514
|
+
numberOfPeriods * rotationInterval,
|
|
515
|
+
intervalType,
|
|
516
|
+
);
|
|
446
517
|
}
|
|
447
518
|
|
|
448
519
|
private getCurrentUserIndexBasedOnHandoffTime(data: {
|
|
@@ -453,7 +524,16 @@ export default class LayerUtil {
|
|
|
453
524
|
users: Array<UserModel>;
|
|
454
525
|
currentEventStartTime: Date;
|
|
455
526
|
restrictionTimes: RestrictionTimes;
|
|
456
|
-
}): number {
|
|
527
|
+
}): { currentUserIndex: number; currentPeriodStart: Date } {
|
|
528
|
+
/*
|
|
529
|
+
* Returns both the on-call user index for the rotation period that CONTAINS
|
|
530
|
+
* currentEventStartTime AND the true (un-clamped) start of that period.
|
|
531
|
+
* getEvents needs the true period start so it can decide, for the first
|
|
532
|
+
* (possibly clamped) period, whether that period consumed a rotation turn
|
|
533
|
+
* based on its FULL-span restriction coverage rather than the coverage in
|
|
534
|
+
* the clamped [now, periodEnd] slice (see the first-period advance guard in
|
|
535
|
+
* getEvents — audit F2).
|
|
536
|
+
*/
|
|
457
537
|
let currentUserIndex: number = data.currentUserIndex;
|
|
458
538
|
|
|
459
539
|
// if current event start time is before layer start, idx unchanged.
|
|
@@ -463,12 +543,63 @@ export default class LayerUtil {
|
|
|
463
543
|
data.startDateTimeOfLayer,
|
|
464
544
|
)
|
|
465
545
|
) {
|
|
466
|
-
return
|
|
546
|
+
return {
|
|
547
|
+
currentUserIndex,
|
|
548
|
+
currentPeriodStart: data.currentEventStartTime,
|
|
549
|
+
};
|
|
467
550
|
}
|
|
468
551
|
|
|
469
552
|
// if handoff is after current start, no rotation has occurred yet — idx unchanged.
|
|
470
553
|
if (OneUptimeDate.isAfter(data.handOffTime, data.currentEventStartTime)) {
|
|
471
|
-
|
|
554
|
+
/*
|
|
555
|
+
* No handoff has happened yet, so we are still inside the very first
|
|
556
|
+
* rotation period, which starts at the layer start.
|
|
557
|
+
*/
|
|
558
|
+
return {
|
|
559
|
+
currentUserIndex,
|
|
560
|
+
currentPeriodStart: data.startDateTimeOfLayer,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/*
|
|
565
|
+
* Fast path: with no restriction, every rotation period produces exactly
|
|
566
|
+
* one event, so the current user index is simply the initial index plus the
|
|
567
|
+
* number of whole rotation periods elapsed since the first handoff. We
|
|
568
|
+
* compute this analytically in O(1) instead of simulating one iteration per
|
|
569
|
+
* period. The simulation below capped at 10000 iterations and returned the
|
|
570
|
+
* WRONG user for long-lived schedules (e.g. an hourly rotation older than
|
|
571
|
+
* ~14 months). Restricted layers still use the period-by-period simulation
|
|
572
|
+
* because fully-restricted periods must not advance the rotation.
|
|
573
|
+
*/
|
|
574
|
+
if (
|
|
575
|
+
data.restrictionTimes &&
|
|
576
|
+
data.restrictionTimes.restictionType === RestrictionType.None &&
|
|
577
|
+
data.users.length > 0
|
|
578
|
+
) {
|
|
579
|
+
const firstBoundary: Date =
|
|
580
|
+
this.moveHandsOffTimeAfterCurrentEventStartTime({
|
|
581
|
+
handOffTime: data.handOffTime,
|
|
582
|
+
currentEventStartTime: data.startDateTimeOfLayer,
|
|
583
|
+
rotation: data.rotation,
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const periodsElapsed: number = this.countElapsedRotationPeriods(
|
|
587
|
+
firstBoundary,
|
|
588
|
+
data.currentEventStartTime,
|
|
589
|
+
data.rotation,
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
const length: number = data.users.length;
|
|
593
|
+
/*
|
|
594
|
+
* Unrestricted layers never have coverage gaps, so getEvents never needs
|
|
595
|
+
* the full-span first-period fallback for them; currentPeriodStart is
|
|
596
|
+
* returned for interface symmetry only and is not read on this path.
|
|
597
|
+
*/
|
|
598
|
+
return {
|
|
599
|
+
currentUserIndex:
|
|
600
|
+
(((currentUserIndex + periodsElapsed) % length) + length) % length,
|
|
601
|
+
currentPeriodStart: data.currentEventStartTime,
|
|
602
|
+
};
|
|
472
603
|
}
|
|
473
604
|
|
|
474
605
|
/*
|
|
@@ -487,12 +618,30 @@ export default class LayerUtil {
|
|
|
487
618
|
});
|
|
488
619
|
|
|
489
620
|
/*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
621
|
+
* Bound the simulation by the actual number of rotation periods between the
|
|
622
|
+
* layer start and the target, so the cap is always large enough to REACH the
|
|
623
|
+
* target for any realistic schedule age. A fixed 10000 cap (~14 months of
|
|
624
|
+
* hourly rotation) stopped early for long-lived sub-daily restricted
|
|
625
|
+
* schedules and returned the index at iteration 10000 instead of the index at
|
|
626
|
+
* "now" — paging the wrong current user (audit F9). We keep a very high
|
|
627
|
+
* ceiling to still bound pathological inputs. Restricted periods must be
|
|
628
|
+
* simulated one at a time (they must not advance the rotation), so this stays
|
|
629
|
+
* O(elapsed periods); for realistic ages that is small.
|
|
494
630
|
*/
|
|
495
|
-
const
|
|
631
|
+
const rawSimCount: number = data.rotation.intervalCount.toNumber();
|
|
632
|
+
const simPeriodUnits: number =
|
|
633
|
+
Number.isFinite(rawSimCount) && rawSimCount >= 1
|
|
634
|
+
? Math.floor(rawSimCount)
|
|
635
|
+
: 1;
|
|
636
|
+
const simUnitsBetween: number = this.getUnitsBetweenDates(
|
|
637
|
+
data.startDateTimeOfLayer,
|
|
638
|
+
data.currentEventStartTime,
|
|
639
|
+
data.rotation.intervalType,
|
|
640
|
+
);
|
|
641
|
+
const maxIterations: number = Math.min(
|
|
642
|
+
5000000,
|
|
643
|
+
Math.max(10000, Math.ceil(simUnitsBetween / simPeriodUnits) + 10),
|
|
644
|
+
);
|
|
496
645
|
let iterations: number = 0;
|
|
497
646
|
|
|
498
647
|
while (
|
|
@@ -530,7 +679,147 @@ export default class LayerUtil {
|
|
|
530
679
|
});
|
|
531
680
|
}
|
|
532
681
|
|
|
533
|
-
|
|
682
|
+
/*
|
|
683
|
+
* simulatedTime is now the true (un-clamped) start of the rotation period
|
|
684
|
+
* that contains data.currentEventStartTime — the loop advances it to the
|
|
685
|
+
* next period start each covered iteration and breaks once a period would
|
|
686
|
+
* extend past the target, so it holds the current period's real start.
|
|
687
|
+
*/
|
|
688
|
+
return { currentUserIndex, currentPeriodStart: simulatedTime };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/*
|
|
692
|
+
* Count the number of rotation boundaries that fall on-or-before `target`,
|
|
693
|
+
* starting from `firstBoundary` and stepping by one rotation period. This is
|
|
694
|
+
* the number of whole rotation periods elapsed, used for the O(1) unrestricted
|
|
695
|
+
* current-user computation. Uses calendar-aware unit stepping so it stays
|
|
696
|
+
* correct for Month/Year (variable length) and across DST for Day/Week.
|
|
697
|
+
*/
|
|
698
|
+
private countElapsedRotationPeriods(
|
|
699
|
+
firstBoundary: Date,
|
|
700
|
+
target: Date,
|
|
701
|
+
rotation: Recurring,
|
|
702
|
+
): number {
|
|
703
|
+
if (OneUptimeDate.isAfter(firstBoundary, target)) {
|
|
704
|
+
return 0;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const intervalType: EventInterval = rotation.intervalType;
|
|
708
|
+
const rawCount: number = rotation.intervalCount.toNumber();
|
|
709
|
+
const periodUnits: number =
|
|
710
|
+
Number.isFinite(rawCount) && rawCount >= 1 ? Math.floor(rawCount) : 1;
|
|
711
|
+
|
|
712
|
+
/*
|
|
713
|
+
* Month and Year have variable calendar length (moment clamps end-of-month:
|
|
714
|
+
* Jan 31 + 1mo = Feb 29, and Feb 29 + 1mo = Mar 29). Because of that,
|
|
715
|
+
* boundary_k computed as a SINGLE multiplied step (anchor + k*units) does
|
|
716
|
+
* NOT equal advancing one period at a time — which is exactly how the real
|
|
717
|
+
* rotation in getEvents (via moveHandsOffTimeAfterCurrentEventStartTime)
|
|
718
|
+
* steps. Using the multiplied form here under-counted elapsed periods by one
|
|
719
|
+
* at month-end anchors and paged the previous on-call user (audit F0). So we
|
|
720
|
+
* iterate one clamped period at a time for Month/Year. The boundary count
|
|
721
|
+
* stays small even over decades of monthly/yearly rotation, so this is cheap.
|
|
722
|
+
*/
|
|
723
|
+
if (
|
|
724
|
+
intervalType === EventInterval.Month ||
|
|
725
|
+
intervalType === EventInterval.Year
|
|
726
|
+
) {
|
|
727
|
+
let periods: number = 0;
|
|
728
|
+
let boundary: Date = firstBoundary;
|
|
729
|
+
let safety: number = 0;
|
|
730
|
+
while (OneUptimeDate.isOnOrBefore(boundary, target) && safety < 100000) {
|
|
731
|
+
periods++;
|
|
732
|
+
// step from the PREVIOUS boundary, mirroring the main-loop rotation.
|
|
733
|
+
boundary = this.addRotationUnits(boundary, periodUnits, intervalType);
|
|
734
|
+
safety++;
|
|
735
|
+
}
|
|
736
|
+
return periods;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/*
|
|
740
|
+
* Hour/Day/Week have no calendar-length clamping, so the O(1) analytic count
|
|
741
|
+
* (a multiplied step) is exact and equals iterating.
|
|
742
|
+
*/
|
|
743
|
+
const unitsBetween: number = this.getUnitsBetweenDates(
|
|
744
|
+
firstBoundary,
|
|
745
|
+
target,
|
|
746
|
+
intervalType,
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
let periods: number = Math.floor(unitsBetween / periodUnits);
|
|
750
|
+
if (periods < 0) {
|
|
751
|
+
periods = 0;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/*
|
|
755
|
+
* `periods` is a lower bound (unit diffs truncate toward zero). Advance
|
|
756
|
+
* until firstBoundary + periods*periodUnits is strictly after target; the
|
|
757
|
+
* resulting count equals the number of boundaries on-or-before target.
|
|
758
|
+
*/
|
|
759
|
+
let safety: number = 0;
|
|
760
|
+
while (
|
|
761
|
+
OneUptimeDate.isOnOrBefore(
|
|
762
|
+
this.addRotationUnits(
|
|
763
|
+
firstBoundary,
|
|
764
|
+
periods * periodUnits,
|
|
765
|
+
intervalType,
|
|
766
|
+
),
|
|
767
|
+
target,
|
|
768
|
+
) &&
|
|
769
|
+
safety < 100000
|
|
770
|
+
) {
|
|
771
|
+
periods++;
|
|
772
|
+
safety++;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return periods;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
private addRotationUnits(
|
|
779
|
+
date: Date,
|
|
780
|
+
units: number,
|
|
781
|
+
intervalType: EventInterval,
|
|
782
|
+
): Date {
|
|
783
|
+
/*
|
|
784
|
+
* Day/Week/Month/Year preserve schedule wall-clock across DST (consistent
|
|
785
|
+
* with moveHandsOffTimeAfterCurrentEventStartTime); Hour is absolute.
|
|
786
|
+
*/
|
|
787
|
+
const tz: string | undefined = this.timezone;
|
|
788
|
+
switch (intervalType) {
|
|
789
|
+
case EventInterval.Hour:
|
|
790
|
+
return OneUptimeDate.addRemoveHours(date, units);
|
|
791
|
+
case EventInterval.Day:
|
|
792
|
+
return OneUptimeDate.addRemoveDays(date, units, tz);
|
|
793
|
+
case EventInterval.Week:
|
|
794
|
+
return OneUptimeDate.addRemoveWeeks(date, units, tz);
|
|
795
|
+
case EventInterval.Month:
|
|
796
|
+
return OneUptimeDate.addRemoveMonths(date, units, tz);
|
|
797
|
+
case EventInterval.Year:
|
|
798
|
+
return OneUptimeDate.addRemoveYears(date, units, tz);
|
|
799
|
+
default:
|
|
800
|
+
return OneUptimeDate.addRemoveDays(date, units, tz);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
private getUnitsBetweenDates(
|
|
805
|
+
from: Date,
|
|
806
|
+
to: Date,
|
|
807
|
+
intervalType: EventInterval,
|
|
808
|
+
): number {
|
|
809
|
+
switch (intervalType) {
|
|
810
|
+
case EventInterval.Hour:
|
|
811
|
+
return OneUptimeDate.getHoursBetweenTwoDates(from, to);
|
|
812
|
+
case EventInterval.Day:
|
|
813
|
+
return OneUptimeDate.getDaysBetweenTwoDates(from, to);
|
|
814
|
+
case EventInterval.Week:
|
|
815
|
+
return OneUptimeDate.getWeeksBetweenTwoDates(from, to);
|
|
816
|
+
case EventInterval.Month:
|
|
817
|
+
return OneUptimeDate.getMonthsBetweenTwoDates(from, to);
|
|
818
|
+
case EventInterval.Year:
|
|
819
|
+
return OneUptimeDate.getYearsBetweenTwoDates(from, to);
|
|
820
|
+
default:
|
|
821
|
+
return OneUptimeDate.getDaysBetweenTwoDates(from, to);
|
|
822
|
+
}
|
|
534
823
|
}
|
|
535
824
|
|
|
536
825
|
public trimStartAndEndTimesBasedOnRestrictionTimes(data: {
|
|
@@ -553,23 +842,32 @@ export default class LayerUtil {
|
|
|
553
842
|
restrictionTimes.restictionType === RestrictionType.Daily &&
|
|
554
843
|
restrictionTimes.dayRestrictionTimes
|
|
555
844
|
) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
845
|
+
/*
|
|
846
|
+
* Move the restriction window to the event's day WITHOUT mutating the
|
|
847
|
+
* shared RestrictionTimes object. The previous code wrote the moved
|
|
848
|
+
* start/end back into restrictionTimes.dayRestrictionTimes, corrupting the
|
|
849
|
+
* caller's object across events/layers/calls and making resolution
|
|
850
|
+
* order-dependent (a hygiene defect flagged in the audit). keepTimeButMoveDay
|
|
851
|
+
* preserves the time-of-day regardless of the base day, so working on a
|
|
852
|
+
* local copy produces identical windows with no shared-state side effects.
|
|
853
|
+
*/
|
|
854
|
+
const movedDayRestriction: StartAndEndTime = {
|
|
855
|
+
startTime: OneUptimeDate.keepTimeButMoveDay(
|
|
559
856
|
restrictionTimes.dayRestrictionTimes.startTime,
|
|
560
857
|
data.eventStartTime,
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
OneUptimeDate.keepTimeButMoveDay(
|
|
858
|
+
this.timezone,
|
|
859
|
+
),
|
|
860
|
+
endTime: OneUptimeDate.keepTimeButMoveDay(
|
|
565
861
|
restrictionTimes.dayRestrictionTimes.endTime,
|
|
566
862
|
data.eventStartTime,
|
|
567
|
-
|
|
863
|
+
this.timezone,
|
|
864
|
+
),
|
|
865
|
+
};
|
|
568
866
|
|
|
569
867
|
return this.getEventsByDailyRestriction({
|
|
570
868
|
eventStartTime: data.eventStartTime,
|
|
571
869
|
eventEndTime: data.eventEndTime,
|
|
572
|
-
restrictionStartAndEndTime:
|
|
870
|
+
restrictionStartAndEndTime: movedDayRestriction,
|
|
573
871
|
props: {
|
|
574
872
|
intervalType: EventInterval.Day,
|
|
575
873
|
},
|
|
@@ -624,7 +922,58 @@ export default class LayerUtil {
|
|
|
624
922
|
];
|
|
625
923
|
}
|
|
626
924
|
|
|
627
|
-
|
|
925
|
+
/*
|
|
926
|
+
* Collapse overlapping/touching segments. The wrap-around split in
|
|
927
|
+
* getWeeklyRestrictionTimesForWeek emits a head segment (early-week tail)
|
|
928
|
+
* plus a main segment, and getEventsByDailyRestriction tiles each weekly
|
|
929
|
+
* across the event window. For a rotation event spanning more than one ISO
|
|
930
|
+
* week, week k's main segment already covers the Sunday->Monday that week
|
|
931
|
+
* (k+1)'s head segment re-covers, producing duplicate/overlapping events for
|
|
932
|
+
* the same user (audit F3). Merging contiguous coverage is always safe here
|
|
933
|
+
* because every segment belongs to the same layer/user.
|
|
934
|
+
*/
|
|
935
|
+
return this.mergeOverlappingStartAndEndTimes(trimmedStartAndEndTimes);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private mergeOverlappingStartAndEndTimes(
|
|
939
|
+
times: Array<StartAndEndTime>,
|
|
940
|
+
): Array<StartAndEndTime> {
|
|
941
|
+
if (times.length <= 1) {
|
|
942
|
+
return times;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const sorted: Array<StartAndEndTime> = [...times].sort(
|
|
946
|
+
(a: StartAndEndTime, b: StartAndEndTime) => {
|
|
947
|
+
if (OneUptimeDate.isBefore(a.startTime, b.startTime)) {
|
|
948
|
+
return -1;
|
|
949
|
+
}
|
|
950
|
+
if (OneUptimeDate.isAfter(a.startTime, b.startTime)) {
|
|
951
|
+
return 1;
|
|
952
|
+
}
|
|
953
|
+
return 0;
|
|
954
|
+
},
|
|
955
|
+
);
|
|
956
|
+
|
|
957
|
+
const merged: Array<StartAndEndTime> = [];
|
|
958
|
+
|
|
959
|
+
for (const current of sorted) {
|
|
960
|
+
const last: StartAndEndTime | undefined = merged[merged.length - 1];
|
|
961
|
+
|
|
962
|
+
// overlapping or directly touching the previous window -> extend it.
|
|
963
|
+
if (last && OneUptimeDate.isOnOrAfter(last.endTime, current.startTime)) {
|
|
964
|
+
if (OneUptimeDate.isAfter(current.endTime, last.endTime)) {
|
|
965
|
+
last.endTime = current.endTime;
|
|
966
|
+
}
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
merged.push({
|
|
971
|
+
startTime: current.startTime,
|
|
972
|
+
endTime: current.endTime,
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
return merged;
|
|
628
977
|
}
|
|
629
978
|
|
|
630
979
|
public getWeeklyRestrictionTimesForWeek(data: {
|
|
@@ -650,13 +999,15 @@ export default class LayerUtil {
|
|
|
650
999
|
startTime = OneUptimeDate.moveDateToTheDayOfWeek(
|
|
651
1000
|
startTime,
|
|
652
1001
|
eventStartTime,
|
|
653
|
-
OneUptimeDate.getDayOfWeek(startTime),
|
|
1002
|
+
OneUptimeDate.getDayOfWeek(startTime, this.timezone),
|
|
1003
|
+
this.timezone,
|
|
654
1004
|
);
|
|
655
1005
|
|
|
656
1006
|
endTime = OneUptimeDate.moveDateToTheDayOfWeek(
|
|
657
1007
|
endTime,
|
|
658
1008
|
eventStartTime,
|
|
659
|
-
OneUptimeDate.getDayOfWeek(endTime),
|
|
1009
|
+
OneUptimeDate.getDayOfWeek(endTime, this.timezone),
|
|
1010
|
+
this.timezone,
|
|
660
1011
|
);
|
|
661
1012
|
|
|
662
1013
|
// now we have true start and end times of the weekly restriction
|
|
@@ -670,25 +1021,72 @@ export default class LayerUtil {
|
|
|
670
1021
|
* and the other for end of the week .
|
|
671
1022
|
*/
|
|
672
1023
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1024
|
+
/*
|
|
1025
|
+
* Anchor the split to the START OF THE ISO WEEK that contains the event,
|
|
1026
|
+
* NOT to data.eventStartTime. When resolution begins mid-week (the live
|
|
1027
|
+
* "who is on call now" path always starts its window at the current
|
|
1028
|
+
* instant), using eventStartTime made the head segment
|
|
1029
|
+
* [eventStartTime, endTime] inverted (start > end) whenever "now" was
|
|
1030
|
+
* already past the window's end-day. getEventsByDailyRestriction then
|
|
1031
|
+
* mis-read that inverted segment as an overnight window and sprayed
|
|
1032
|
+
* phantom all-day on-call coverage across every day of the week, paging
|
|
1033
|
+
* the wrong user during hours the restriction excludes. Using the real
|
|
1034
|
+
* week start keeps the head segment correctly ordered; the later
|
|
1035
|
+
* intersection with the event window discards any portion that has
|
|
1036
|
+
* already elapsed.
|
|
1037
|
+
*/
|
|
1038
|
+
const startOfWeek: Date = OneUptimeDate.getStartOfTheWeek(
|
|
1039
|
+
data.eventStartTime,
|
|
1040
|
+
this.timezone, // anchor to the schedule zone's week boundary (audit F6)
|
|
1041
|
+
);
|
|
676
1042
|
|
|
1043
|
+
/*
|
|
1044
|
+
* Head segment: the early-week tail (week start -> endTime) of a weekend
|
|
1045
|
+
* window that opened the PREVIOUS period. This is what covers an
|
|
1046
|
+
* in-progress wrap-around window when resolution starts mid-weekend
|
|
1047
|
+
* (e.g. resolving on the Sunday of a Fri 20:00 -> Mon 08:00 window).
|
|
1048
|
+
*/
|
|
677
1049
|
startAndEndTimesOfWeeklyRestrictions.push({
|
|
678
1050
|
startTime: startOfWeek,
|
|
679
1051
|
endTime: endTime,
|
|
680
1052
|
});
|
|
681
1053
|
|
|
1054
|
+
/*
|
|
1055
|
+
* Main segment: the contiguous window from startTime (this week) through
|
|
1056
|
+
* endTime moved to the NEXT week. Because this is a wrap-around,
|
|
1057
|
+
* startTime is later in the week than endTime, so endTime + 7 days is the
|
|
1058
|
+
* window's true close (e.g. Fri 20:00 -> the following Mon 08:00).
|
|
1059
|
+
* Expressing it as one forward window — rather than clipping to the end
|
|
1060
|
+
* of THIS ISO week — lets getEventsByDailyRestriction tile it weekly
|
|
1061
|
+
* across a multi-week rotation event without leaving the Sunday/Monday
|
|
1062
|
+
* portion of the weekend uncovered.
|
|
1063
|
+
*/
|
|
682
1064
|
startAndEndTimesOfWeeklyRestrictions.push({
|
|
683
1065
|
startTime: startTime,
|
|
684
|
-
|
|
1066
|
+
/*
|
|
1067
|
+
* Forward the schedule timezone so this +7-day step is a wall-clock
|
|
1068
|
+
* week in the schedule's zone, consistent with every sibling
|
|
1069
|
+
* day-step in the weekly tiling path (audit F8). Without it, when the
|
|
1070
|
+
* server zone differs from the schedule zone across a DST transition,
|
|
1071
|
+
* the wrap-around window's close drifted by the DST offset and that
|
|
1072
|
+
* drift then propagated to every subsequent weekend of the expansion.
|
|
1073
|
+
*/
|
|
1074
|
+
endTime: OneUptimeDate.addRemoveDays(endTime, 7, this.timezone),
|
|
1075
|
+
});
|
|
1076
|
+
} else {
|
|
1077
|
+
/*
|
|
1078
|
+
* Non-wrapping restriction: emit the single window. This is gated in an
|
|
1079
|
+
* `else` because the wrap-around case above is already fully described
|
|
1080
|
+
* by the two split segments; previously this raw push ran
|
|
1081
|
+
* unconditionally, adding a third INVERTED (start > end) segment that
|
|
1082
|
+
* getEventsByDailyRestriction then re-expanded into phantom nightly
|
|
1083
|
+
* on-call windows on every day of the week.
|
|
1084
|
+
*/
|
|
1085
|
+
startAndEndTimesOfWeeklyRestrictions.push({
|
|
1086
|
+
startTime,
|
|
1087
|
+
endTime,
|
|
685
1088
|
});
|
|
686
1089
|
}
|
|
687
|
-
|
|
688
|
-
startAndEndTimesOfWeeklyRestrictions.push({
|
|
689
|
-
startTime,
|
|
690
|
-
endTime,
|
|
691
|
-
});
|
|
692
1090
|
}
|
|
693
1091
|
|
|
694
1092
|
return startAndEndTimesOfWeeklyRestrictions;
|
|
@@ -731,13 +1129,41 @@ export default class LayerUtil {
|
|
|
731
1129
|
if (OneUptimeDate.isBefore(restrictionEndTime, restrictionStartTime)) {
|
|
732
1130
|
const results: Array<StartAndEndTime> = [];
|
|
733
1131
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1132
|
+
/*
|
|
1133
|
+
* Iterate day-by-day within the event range. We start ONE day BEFORE the
|
|
1134
|
+
* event's start day so the "morning" tail of the window that opened the
|
|
1135
|
+
* previous night (e.g. a 22:00 -> 06:00 window covering 00:00 -> 06:00 on
|
|
1136
|
+
* the event's own first day) is emitted. Previously the loop started at
|
|
1137
|
+
* getStartOfDay(eventStart) and only ever tied the morning segment to the
|
|
1138
|
+
* NEXT day, so a rotation event beginning at midnight lost its first-day
|
|
1139
|
+
* morning coverage entirely, leaving a nightly gap where nobody was on
|
|
1140
|
+
* call. The addIntersection clip to [eventStart, eventEnd] discards any
|
|
1141
|
+
* segment of the extra leading day that falls outside the event.
|
|
1142
|
+
*/
|
|
1143
|
+
let currentDayStart: Date = OneUptimeDate.addRemoveDays(
|
|
1144
|
+
OneUptimeDate.getStartOfDay(data.eventStartTime, this.timezone),
|
|
1145
|
+
-1,
|
|
1146
|
+
this.timezone, // step wall-clock days in the schedule zone (audit L1)
|
|
737
1147
|
);
|
|
738
1148
|
const absoluteEventEnd: Date = data.eventEndTime;
|
|
739
1149
|
let safetyCounter: number = 0;
|
|
740
|
-
|
|
1150
|
+
/*
|
|
1151
|
+
* Scale the day-by-day safeguard to the actual event span. A fixed 62-day
|
|
1152
|
+
* cap dropped every night past ~day 62 for rotation events longer than
|
|
1153
|
+
* that (e.g. a quarterly/annual rotation with an overnight restriction),
|
|
1154
|
+
* leaving those nights with no on-call coverage (audit F2). Bound to the
|
|
1155
|
+
* event length plus margin, with a hard ceiling for pathological inputs.
|
|
1156
|
+
*/
|
|
1157
|
+
const maxDays: number = Math.min(
|
|
1158
|
+
4000,
|
|
1159
|
+
Math.max(
|
|
1160
|
+
62,
|
|
1161
|
+
OneUptimeDate.getDaysBetweenTwoDates(
|
|
1162
|
+
data.eventStartTime,
|
|
1163
|
+
data.eventEndTime,
|
|
1164
|
+
) + 3,
|
|
1165
|
+
),
|
|
1166
|
+
);
|
|
741
1167
|
|
|
742
1168
|
while (
|
|
743
1169
|
OneUptimeDate.isOnOrBefore(currentDayStart, absoluteEventEnd) &&
|
|
@@ -748,19 +1174,26 @@ export default class LayerUtil {
|
|
|
748
1174
|
const segmentNightStart: Date = OneUptimeDate.keepTimeButMoveDay(
|
|
749
1175
|
restrictionStartTime,
|
|
750
1176
|
currentDayStart,
|
|
1177
|
+
this.timezone,
|
|
1178
|
+
);
|
|
1179
|
+
const segmentNightEnd: Date = OneUptimeDate.getEndOfDay(
|
|
1180
|
+
segmentNightStart,
|
|
1181
|
+
this.timezone,
|
|
751
1182
|
);
|
|
752
|
-
const segmentNightEnd: Date =
|
|
753
|
-
OneUptimeDate.getEndOfDay(segmentNightStart);
|
|
754
1183
|
|
|
755
1184
|
const nextDayStart: Date = OneUptimeDate.addRemoveDays(
|
|
756
1185
|
currentDayStart,
|
|
757
1186
|
1,
|
|
1187
|
+
this.timezone, // wall-clock day step; avoids revisiting a day across fall-back DST (audit L1)
|
|
1188
|
+
);
|
|
1189
|
+
const segmentMorningStart: Date = OneUptimeDate.getStartOfDay(
|
|
1190
|
+
nextDayStart,
|
|
1191
|
+
this.timezone,
|
|
758
1192
|
);
|
|
759
|
-
const segmentMorningStart: Date =
|
|
760
|
-
OneUptimeDate.getStartOfDay(nextDayStart);
|
|
761
1193
|
const segmentMorningEnd: Date = OneUptimeDate.keepTimeButMoveDay(
|
|
762
1194
|
restrictionEndTime,
|
|
763
1195
|
nextDayStart,
|
|
1196
|
+
this.timezone,
|
|
764
1197
|
);
|
|
765
1198
|
|
|
766
1199
|
// helper to add intersection if it overlaps the event window
|
|
@@ -803,9 +1236,23 @@ export default class LayerUtil {
|
|
|
803
1236
|
|
|
804
1237
|
let reachedTheEndOfTheCurrentEvent: boolean = false;
|
|
805
1238
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1239
|
+
/*
|
|
1240
|
+
* Scale the break clause to the event span. The loop advances one restriction
|
|
1241
|
+
* period (1 day for a Daily restriction, 7 days for a Weekly one) per
|
|
1242
|
+
* iteration, so a single rotation event longer than ~50 days had its later
|
|
1243
|
+
* days silently dropped, leaving no on-call coverage (audit F2). Days-in-event
|
|
1244
|
+
* is a safe upper bound for both the daily (+1/day) and weekly (+7/day) paths.
|
|
1245
|
+
*/
|
|
1246
|
+
const maxLoopCount: number = Math.min(
|
|
1247
|
+
4000,
|
|
1248
|
+
Math.max(
|
|
1249
|
+
50,
|
|
1250
|
+
OneUptimeDate.getDaysBetweenTwoDates(
|
|
1251
|
+
data.eventStartTime,
|
|
1252
|
+
data.eventEndTime,
|
|
1253
|
+
) + 10,
|
|
1254
|
+
),
|
|
1255
|
+
);
|
|
809
1256
|
let loopCount: number = 0;
|
|
810
1257
|
|
|
811
1258
|
while (!reachedTheEndOfTheCurrentEvent) {
|
|
@@ -826,10 +1273,29 @@ export default class LayerUtil {
|
|
|
826
1273
|
return trimmedStartAndEndTimes;
|
|
827
1274
|
}
|
|
828
1275
|
|
|
829
|
-
|
|
830
|
-
|
|
1276
|
+
/*
|
|
1277
|
+
* The event begins after THIS day's restriction window has already ended.
|
|
1278
|
+
* Do NOT drop the whole event — a multi-day rotation event (e.g. a WEEKLY
|
|
1279
|
+
* rotation whose handoff/start is at 20:00, with a 09:00-17:00 daily
|
|
1280
|
+
* restriction) must still be covered on its subsequent days. Advance the
|
|
1281
|
+
* restriction window to the next day/week and re-test instead of returning
|
|
1282
|
+
* empty. Termination is preserved: after at most one advance the window's
|
|
1283
|
+
* end moves past currentStartTime, and the "restrictionStart past
|
|
1284
|
+
* currentEnd" guard above returns once the window moves past the event end
|
|
1285
|
+
* (so a short event entirely after the window still yields no coverage).
|
|
1286
|
+
*/
|
|
831
1287
|
if (OneUptimeDate.isOnOrAfter(currentStartTime, restrictionEndTime)) {
|
|
832
|
-
|
|
1288
|
+
restrictionStartTime = OneUptimeDate.addRemoveDays(
|
|
1289
|
+
restrictionStartTime,
|
|
1290
|
+
data.props.intervalType === EventInterval.Day ? 1 : 7, // daily or weekly
|
|
1291
|
+
this.timezone,
|
|
1292
|
+
);
|
|
1293
|
+
restrictionEndTime = OneUptimeDate.addRemoveDays(
|
|
1294
|
+
restrictionEndTime,
|
|
1295
|
+
data.props.intervalType === EventInterval.Day ? 1 : 7, // daily or weekly
|
|
1296
|
+
this.timezone,
|
|
1297
|
+
);
|
|
1298
|
+
continue;
|
|
833
1299
|
}
|
|
834
1300
|
|
|
835
1301
|
// if the restriction end time is before the restriction start time, we need to add one day to the restriction end time
|
|
@@ -840,8 +1306,16 @@ export default class LayerUtil {
|
|
|
840
1306
|
);
|
|
841
1307
|
}
|
|
842
1308
|
|
|
843
|
-
|
|
1309
|
+
/*
|
|
1310
|
+
* The four cases below are mutually exclusive for a given iteration and
|
|
1311
|
+
* are expressed as an if / else-if chain. This matters because cases 2 and
|
|
1312
|
+
* 4 MUTATE currentStartTime / restrictionStartTime / restrictionEndTime and
|
|
1313
|
+
* then continue the loop; without else-if, a later case would re-evaluate
|
|
1314
|
+
* against the freshly-mutated state within the same iteration and emit a
|
|
1315
|
+
* duplicate (or overlapping) window.
|
|
1316
|
+
*/
|
|
844
1317
|
|
|
1318
|
+
// 1 - the event falls entirely within the restriction window: emit it and finish.
|
|
845
1319
|
if (
|
|
846
1320
|
OneUptimeDate.isOnOrAfter(currentStartTime, restrictionStartTime) &&
|
|
847
1321
|
OneUptimeDate.isOnOrAfter(restrictionEndTime, currentEndTime)
|
|
@@ -851,14 +1325,18 @@ export default class LayerUtil {
|
|
|
851
1325
|
endTime: currentEndTime,
|
|
852
1326
|
});
|
|
853
1327
|
reachedTheEndOfTheCurrentEvent = true;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1328
|
+
} else if (
|
|
1329
|
+
/*
|
|
1330
|
+
* 2 - Start Restriction: the event starts inside the restriction window
|
|
1331
|
+
* but extends past its end. Emit [currentStart, restrictionEnd], then
|
|
1332
|
+
* ADVANCE to the next restriction day/week and continue, so every
|
|
1333
|
+
* remaining day of a multi-day rotation event is emitted. Previously this
|
|
1334
|
+
* terminated the loop after the first day, dropping on-call coverage for
|
|
1335
|
+
* every subsequent day of the rotation period (e.g. a weekly rotation
|
|
1336
|
+
* with a 09:00-17:00 daily restriction and a handoff at/after 09:00
|
|
1337
|
+
* covered only day 1). This now mirrors case 4's advance-and-continue.
|
|
1338
|
+
* Strict isAfter on the end keeps this exclusive from case 1.
|
|
1339
|
+
*/
|
|
862
1340
|
OneUptimeDate.isOnOrAfter(currentStartTime, restrictionStartTime) &&
|
|
863
1341
|
OneUptimeDate.isAfter(currentEndTime, restrictionEndTime)
|
|
864
1342
|
) {
|
|
@@ -866,12 +1344,24 @@ export default class LayerUtil {
|
|
|
866
1344
|
startTime: currentStartTime,
|
|
867
1345
|
endTime: restrictionEndTime,
|
|
868
1346
|
});
|
|
869
|
-
reachedTheEndOfTheCurrentEvent = true;
|
|
870
|
-
}
|
|
871
1347
|
|
|
872
|
-
|
|
1348
|
+
currentStartTime = OneUptimeDate.addRemoveSeconds(
|
|
1349
|
+
restrictionEndTime,
|
|
1350
|
+
1,
|
|
1351
|
+
);
|
|
873
1352
|
|
|
874
|
-
|
|
1353
|
+
restrictionStartTime = OneUptimeDate.addRemoveDays(
|
|
1354
|
+
restrictionStartTime,
|
|
1355
|
+
data.props.intervalType === EventInterval.Day ? 1 : 7, // daily or weekly
|
|
1356
|
+
this.timezone, // preserve wall-clock across DST (audit F5)
|
|
1357
|
+
);
|
|
1358
|
+
restrictionEndTime = OneUptimeDate.addRemoveDays(
|
|
1359
|
+
restrictionEndTime,
|
|
1360
|
+
data.props.intervalType === EventInterval.Day ? 1 : 7, // daily or weekly
|
|
1361
|
+
this.timezone, // preserve wall-clock across DST (audit F5)
|
|
1362
|
+
);
|
|
1363
|
+
} else if (
|
|
1364
|
+
// 3 - End Restriction - the event starts before the window and ends inside it.
|
|
875
1365
|
OneUptimeDate.isBefore(currentStartTime, restrictionStartTime) &&
|
|
876
1366
|
OneUptimeDate.isBefore(currentEndTime, restrictionEndTime) &&
|
|
877
1367
|
OneUptimeDate.isAfter(currentEndTime, restrictionStartTime)
|
|
@@ -881,11 +1371,8 @@ export default class LayerUtil {
|
|
|
881
1371
|
endTime: currentEndTime,
|
|
882
1372
|
});
|
|
883
1373
|
reachedTheEndOfTheCurrentEvent = true;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// 4 - If the current event starts before the restriction start time and ends after the restriction end time, we need to return the current event with the start time of the restriction and end time of the restriction.
|
|
887
|
-
|
|
888
|
-
if (
|
|
1374
|
+
} else if (
|
|
1375
|
+
// 4 - the event spans the whole window: emit it, advance a day/week, continue.
|
|
889
1376
|
OneUptimeDate.isBefore(currentStartTime, restrictionStartTime) &&
|
|
890
1377
|
OneUptimeDate.isOnOrAfter(currentEndTime, restrictionEndTime)
|
|
891
1378
|
) {
|
|
@@ -904,10 +1391,12 @@ export default class LayerUtil {
|
|
|
904
1391
|
restrictionStartTime = OneUptimeDate.addRemoveDays(
|
|
905
1392
|
restrictionStartTime,
|
|
906
1393
|
data.props.intervalType === EventInterval.Day ? 1 : 7, // daily or weekly
|
|
1394
|
+
this.timezone, // preserve wall-clock across DST (audit F5)
|
|
907
1395
|
);
|
|
908
1396
|
restrictionEndTime = OneUptimeDate.addRemoveDays(
|
|
909
1397
|
restrictionEndTime,
|
|
910
1398
|
data.props.intervalType === EventInterval.Day ? 1 : 7, // daily or weekly
|
|
1399
|
+
this.timezone, // preserve wall-clock across DST (audit F5)
|
|
911
1400
|
);
|
|
912
1401
|
}
|
|
913
1402
|
}
|
|
@@ -975,18 +1464,25 @@ export default class LayerUtil {
|
|
|
975
1464
|
let layerPriority: number = 1;
|
|
976
1465
|
|
|
977
1466
|
for (const layer of data.layers) {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1467
|
+
/*
|
|
1468
|
+
* Do NOT forward getNumberOfEvents to the per-layer expansion. Capping
|
|
1469
|
+
* each layer to N events before the priority merge can drop a lower-
|
|
1470
|
+
* priority (fallback) layer's post-block coverage: if a higher-priority
|
|
1471
|
+
* layer's restricted block swallows the fallback's first N events, the
|
|
1472
|
+
* fallback's (N+1)-th event — the true "next" on-call after the block —
|
|
1473
|
+
* is never generated, corrupting the merged "next" roster. The cap is
|
|
1474
|
+
* applied only once, after the merge, below.
|
|
1475
|
+
*/
|
|
1476
|
+
const layerEvents: Array<CalendarEvent> = this.getEvents({
|
|
1477
|
+
users: layer.users,
|
|
1478
|
+
startDateTimeOfLayer: layer.startDateTimeOfLayer,
|
|
1479
|
+
restrictionTimes: layer.restrictionTimes,
|
|
1480
|
+
handOffTime: layer.handOffTime,
|
|
1481
|
+
rotation: layer.rotation,
|
|
1482
|
+
timezone: layer.timezone,
|
|
1483
|
+
calendarStartDate: data.calendarStartDate,
|
|
1484
|
+
calendarEndDate: data.calendarEndDate,
|
|
1485
|
+
});
|
|
990
1486
|
|
|
991
1487
|
// add priority to each event
|
|
992
1488
|
|
|
@@ -1094,11 +1590,40 @@ export default class LayerUtil {
|
|
|
1094
1590
|
*/
|
|
1095
1591
|
const tempFinalEventEnd: Date = finalEvent.end;
|
|
1096
1592
|
|
|
1593
|
+
/*
|
|
1594
|
+
* Reconstruct the trailing tail FIRST, before the front-collapse
|
|
1595
|
+
* removal below. If the lower-priority (fallback) event originally
|
|
1596
|
+
* extended past the higher-priority event, the portion AFTER the
|
|
1597
|
+
* higher-priority window must survive as its own segment — even when
|
|
1598
|
+
* the FRONT of the final event collapses to zero/negative length
|
|
1599
|
+
* (which happens when the higher-priority event starts at or before
|
|
1600
|
+
* the final event's start, e.g. two back-to-back higher-priority
|
|
1601
|
+
* rotation windows over a 24/7 fallback layer). Previously this block
|
|
1602
|
+
* ran only AFTER the collapse checks, whose `continue` skipped it,
|
|
1603
|
+
* silently deleting the fallback layer's coverage after the higher-
|
|
1604
|
+
* priority window and leaving on-call gaps where nobody is paged.
|
|
1605
|
+
*/
|
|
1606
|
+
if (OneUptimeDate.isAfter(tempFinalEventEnd, event.end)) {
|
|
1607
|
+
// add the trailing segment of the lower-priority event
|
|
1608
|
+
const trimmedEvent: PriorityCalendarEvents = {
|
|
1609
|
+
...finalEvent,
|
|
1610
|
+
priority: finalEvent.priority,
|
|
1611
|
+
start: OneUptimeDate.addRemoveSeconds(event.end, 1),
|
|
1612
|
+
end: tempFinalEventEnd,
|
|
1613
|
+
};
|
|
1614
|
+
|
|
1615
|
+
// only keep it if it has positive length
|
|
1616
|
+
if (OneUptimeDate.isAfter(trimmedEvent.end, trimmedEvent.start)) {
|
|
1617
|
+
finalEvents.push(trimmedEvent);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1097
1621
|
finalEvent.end = OneUptimeDate.addRemoveSeconds(event.start, -1);
|
|
1098
1622
|
|
|
1099
1623
|
/*
|
|
1100
1624
|
* check if the final event end time is before the start time of the current event
|
|
1101
1625
|
* if it is, we need to remove the final event from the final events array
|
|
1626
|
+
* (the trailing tail, if any, was already preserved above)
|
|
1102
1627
|
*/
|
|
1103
1628
|
if (OneUptimeDate.isBefore(finalEvent.end, finalEvent.start)) {
|
|
1104
1629
|
finalEvents.splice(i, 1);
|
|
@@ -1112,28 +1637,21 @@ export default class LayerUtil {
|
|
|
1112
1637
|
i--; // Adjust index after removal
|
|
1113
1638
|
continue;
|
|
1114
1639
|
}
|
|
1115
|
-
|
|
1116
|
-
// final event was originally ending after the current event, so we need to add the trimmed event to the final events array
|
|
1117
|
-
if (OneUptimeDate.isAfter(tempFinalEventEnd, event.end)) {
|
|
1118
|
-
// add the trimmed event to the final events array
|
|
1119
|
-
const trimmedEvent: PriorityCalendarEvents = {
|
|
1120
|
-
...finalEvent,
|
|
1121
|
-
priority: finalEvent.priority,
|
|
1122
|
-
start: OneUptimeDate.addRemoveSeconds(event.end, 1),
|
|
1123
|
-
end: tempFinalEventEnd,
|
|
1124
|
-
};
|
|
1125
|
-
|
|
1126
|
-
// check if the event end time is before the start time of the trimmed event
|
|
1127
|
-
if (OneUptimeDate.isAfter(trimmedEvent.end, trimmedEvent.start)) {
|
|
1128
|
-
finalEvents.push(trimmedEvent);
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
1640
|
} else {
|
|
1132
1641
|
/*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1642
|
+
* Trim the current (lower-priority) event: push its start past this
|
|
1643
|
+
* higher-priority window. Use getGreaterDate (a monotonic max)
|
|
1644
|
+
* instead of a bare assignment so the result does NOT depend on the
|
|
1645
|
+
* order finalEvents are visited. That makes it safe to hoist the
|
|
1646
|
+
* per-iteration finalEvents.sort() out of the loop (audit H2): with
|
|
1647
|
+
* the old in-loop ascending sort, successive overlaps already had
|
|
1648
|
+
* monotonically increasing ends, so max equals the old assignment and
|
|
1649
|
+
* the output is unchanged.
|
|
1135
1650
|
*/
|
|
1136
|
-
event.start = OneUptimeDate.
|
|
1651
|
+
event.start = OneUptimeDate.getGreaterDate(
|
|
1652
|
+
event.start,
|
|
1653
|
+
OneUptimeDate.addRemoveSeconds(finalEvent.end, 1),
|
|
1654
|
+
);
|
|
1137
1655
|
}
|
|
1138
1656
|
}
|
|
1139
1657
|
}
|
|
@@ -1143,19 +1661,14 @@ export default class LayerUtil {
|
|
|
1143
1661
|
finalEvents.push(event);
|
|
1144
1662
|
}
|
|
1145
1663
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
return 1;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
return 0;
|
|
1158
|
-
});
|
|
1664
|
+
/*
|
|
1665
|
+
* The finalEvents.sort() that used to run HERE — inside the per-event
|
|
1666
|
+
* loop — made the merge O(n^2 log n). It is hoisted to a single sort after
|
|
1667
|
+
* the loop (below). Correctness is preserved because overlap detection and
|
|
1668
|
+
* trimming do not depend on finalEvents being sorted (the current-event
|
|
1669
|
+
* trim above now uses a monotonic max), so sorting once at the end yields
|
|
1670
|
+
* the same result. Audit H2.
|
|
1671
|
+
*/
|
|
1159
1672
|
|
|
1160
1673
|
// if an event starts and end at the same time, we need to remove it
|
|
1161
1674
|
|
|
@@ -1181,6 +1694,23 @@ export default class LayerUtil {
|
|
|
1181
1694
|
}
|
|
1182
1695
|
}
|
|
1183
1696
|
|
|
1697
|
+
/*
|
|
1698
|
+
* Single final sort by start time (hoisted out of the per-event loop above,
|
|
1699
|
+
* audit H2). Downstream consumers (getEvents id assignment, the schedule
|
|
1700
|
+
* service's current/next selection) expect events in start order.
|
|
1701
|
+
*/
|
|
1702
|
+
finalEvents.sort((a: CalendarEvent, b: CalendarEvent) => {
|
|
1703
|
+
if (OneUptimeDate.isBefore(a.start, b.start)) {
|
|
1704
|
+
return -1;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (OneUptimeDate.isAfter(a.start, b.start)) {
|
|
1708
|
+
return 1;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
return 0;
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1184
1714
|
// convert PriorityCalendarEvents to CalendarEvents
|
|
1185
1715
|
|
|
1186
1716
|
const calendarEvents: CalendarEvent[] = [];
|