@oneuptime/common 11.3.26 → 11.3.28
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/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/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/Index.ts +8 -0
- package/Server/Infrastructure/PostgresDatabase.ts +11 -0
- package/Server/Services/AIService.ts +74 -2
- package/Server/Services/AnalyticsDatabaseService.ts +10 -1
- package/Server/Services/DatabaseService.ts +10 -1
- package/Server/Services/IncidentService.ts +1 -1
- 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/IncidentAIContextBuilder.ts +18 -3
- package/Server/Utils/AI/IncidentEpisodeAIContextBuilder.ts +6 -1
- package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +12 -2
- package/Server/Utils/AI/Toolbox/IncidentTools.ts +183 -1
- package/Server/Utils/AI/Toolbox/Index.ts +4 -1
- package/Server/Utils/AI/Toolbox/RecentChangesTools.ts +208 -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/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/UserOverrideUtil.test.ts +255 -0
- package/Tests/Types/OnCallDutyPolicy/UserOverrideUtilExhaustive.test.ts +1126 -0
- package/Types/Date.ts +200 -15
- package/Types/OnCallDutyPolicy/Layer.ts +790 -260
- package/Types/OnCallDutyPolicy/RestrictionTimes.ts +39 -13
- package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +21 -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/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/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/Index.js +8 -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/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 +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/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/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 +4 -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/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 +153 -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/UserOverrideUtil.js +20 -5
- package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1631
|
+
data.getNumberOfEvents,
|
|
1156
1632
|
);
|
|
1157
1633
|
logger.debug("Current end time: " + currentEndTime.toISOString(), {
|
|
1158
1634
|
onCallDutyPolicyScheduleId: data.scheduleId.toString(),
|