@oneuptime/common 11.3.0 → 11.3.1

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.
@@ -450,67 +450,106 @@ export class ProjectService extends DatabaseService {
450
450
  });
451
451
  }
452
452
  }
453
+ /**
454
+ * Names of every row of `service` already scoped to `projectId`.
455
+ *
456
+ * The addDefault* seeders below each create a fixed set of named rows on a
457
+ * column that is unique per project (@UniqueColumnBy("projectId") on `name`).
458
+ * The SAME defaults are created in two places: here, in the project-create
459
+ * hooks, AND by backfill data migrations (e.g.
460
+ * AddDefaultAlertSeverityAndStateToExistingProjects). Creating a name that
461
+ * already exists throws "<X> with the same name already exists" in
462
+ * DatabaseService.checkUniqueColumnBy — and because the DataMigration runner
463
+ * halts the whole chain at the first failure, that single throw freezes every
464
+ * later migration. Each seeder guards its creates against this set so both
465
+ * paths are idempotent / safe to re-run.
466
+ */
467
+ async getExistingProjectScopedNames(service, projectId) {
468
+ const items = await service.findBy({
469
+ query: {
470
+ projectId: projectId,
471
+ },
472
+ select: {
473
+ name: true,
474
+ },
475
+ skip: 0,
476
+ limit: LIMIT_MAX,
477
+ props: {
478
+ isRoot: true,
479
+ },
480
+ });
481
+ return new Set(items.map((item) => {
482
+ return item.name;
483
+ }));
484
+ }
453
485
  async addDefaultScheduledMaintenanceState(createdItem) {
454
- let createdScheduledMaintenanceState = new ScheduledMaintenanceState();
455
- createdScheduledMaintenanceState.name = "Scheduled";
456
- createdScheduledMaintenanceState.description =
457
- "When an event is scheduled, it belongs to this state";
458
- createdScheduledMaintenanceState.color = Black;
459
- createdScheduledMaintenanceState.isScheduledState = true;
460
- createdScheduledMaintenanceState.projectId = createdItem.id;
461
- createdScheduledMaintenanceState.order = 1;
462
- createdScheduledMaintenanceState =
486
+ const projectId = createdItem.id;
487
+ // Idempotent — see getExistingProjectScopedNames.
488
+ const existingNames = await this.getExistingProjectScopedNames(ScheduledMaintenanceStateService, projectId);
489
+ if (!existingNames.has("Scheduled")) {
490
+ const createdScheduledMaintenanceState = new ScheduledMaintenanceState();
491
+ createdScheduledMaintenanceState.name = "Scheduled";
492
+ createdScheduledMaintenanceState.description =
493
+ "When an event is scheduled, it belongs to this state";
494
+ createdScheduledMaintenanceState.color = Black;
495
+ createdScheduledMaintenanceState.isScheduledState = true;
496
+ createdScheduledMaintenanceState.projectId = projectId;
497
+ createdScheduledMaintenanceState.order = 1;
463
498
  await ScheduledMaintenanceStateService.create({
464
499
  data: createdScheduledMaintenanceState,
465
500
  props: {
466
501
  isRoot: true,
467
502
  },
468
503
  });
469
- let ongoingScheduledMaintenanceState = new ScheduledMaintenanceState();
470
- ongoingScheduledMaintenanceState.name = "Ongoing";
471
- ongoingScheduledMaintenanceState.description =
472
- "When an event is ongoing, it belongs to this state.";
473
- ongoingScheduledMaintenanceState.color = Yellow;
474
- ongoingScheduledMaintenanceState.isOngoingState = true;
475
- ongoingScheduledMaintenanceState.projectId = createdItem.id;
476
- ongoingScheduledMaintenanceState.order = 2;
477
- ongoingScheduledMaintenanceState =
504
+ }
505
+ if (!existingNames.has("Ongoing")) {
506
+ const ongoingScheduledMaintenanceState = new ScheduledMaintenanceState();
507
+ ongoingScheduledMaintenanceState.name = "Ongoing";
508
+ ongoingScheduledMaintenanceState.description =
509
+ "When an event is ongoing, it belongs to this state.";
510
+ ongoingScheduledMaintenanceState.color = Yellow;
511
+ ongoingScheduledMaintenanceState.isOngoingState = true;
512
+ ongoingScheduledMaintenanceState.projectId = projectId;
513
+ ongoingScheduledMaintenanceState.order = 2;
478
514
  await ScheduledMaintenanceStateService.create({
479
515
  data: ongoingScheduledMaintenanceState,
480
516
  props: {
481
517
  isRoot: true,
482
518
  },
483
519
  });
484
- let endedScheduledMaintenanceState = new ScheduledMaintenanceState();
485
- endedScheduledMaintenanceState.name = "Ended";
486
- endedScheduledMaintenanceState.description =
487
- "Scheduled maintenance events switch to this state when they end.";
488
- endedScheduledMaintenanceState.color = new Color("#4A4A4A");
489
- endedScheduledMaintenanceState.isEndedState = true;
490
- endedScheduledMaintenanceState.projectId = createdItem.id;
491
- endedScheduledMaintenanceState.order = 3;
492
- endedScheduledMaintenanceState =
520
+ }
521
+ if (!existingNames.has("Ended")) {
522
+ const endedScheduledMaintenanceState = new ScheduledMaintenanceState();
523
+ endedScheduledMaintenanceState.name = "Ended";
524
+ endedScheduledMaintenanceState.description =
525
+ "Scheduled maintenance events switch to this state when they end.";
526
+ endedScheduledMaintenanceState.color = new Color("#4A4A4A");
527
+ endedScheduledMaintenanceState.isEndedState = true;
528
+ endedScheduledMaintenanceState.projectId = projectId;
529
+ endedScheduledMaintenanceState.order = 3;
493
530
  await ScheduledMaintenanceStateService.create({
494
531
  data: endedScheduledMaintenanceState,
495
532
  props: {
496
533
  isRoot: true,
497
534
  },
498
535
  });
499
- let completedScheduledMaintenanceState = new ScheduledMaintenanceState();
500
- completedScheduledMaintenanceState.name = "Completed";
501
- completedScheduledMaintenanceState.description =
502
- "When an event is completed, it belongs to this state.";
503
- completedScheduledMaintenanceState.color = Green;
504
- completedScheduledMaintenanceState.isResolvedState = true;
505
- completedScheduledMaintenanceState.projectId = createdItem.id;
506
- completedScheduledMaintenanceState.order = 4;
507
- completedScheduledMaintenanceState =
536
+ }
537
+ if (!existingNames.has("Completed")) {
538
+ const completedScheduledMaintenanceState = new ScheduledMaintenanceState();
539
+ completedScheduledMaintenanceState.name = "Completed";
540
+ completedScheduledMaintenanceState.description =
541
+ "When an event is completed, it belongs to this state.";
542
+ completedScheduledMaintenanceState.color = Green;
543
+ completedScheduledMaintenanceState.isResolvedState = true;
544
+ completedScheduledMaintenanceState.projectId = projectId;
545
+ completedScheduledMaintenanceState.order = 4;
508
546
  await ScheduledMaintenanceStateService.create({
509
547
  data: completedScheduledMaintenanceState,
510
548
  props: {
511
549
  isRoot: true,
512
550
  },
513
551
  });
552
+ }
514
553
  return createdItem;
515
554
  }
516
555
  async onCreateSuccess(_onCreate, createdItem) {
@@ -631,262 +670,317 @@ export class ProjectService extends DatabaseService {
631
670
  return createdItem;
632
671
  }
633
672
  async addDefaultIncidentState(createdItem) {
634
- let createdIncidentState = new IncidentState();
635
- createdIncidentState.name = "Identified";
636
- createdIncidentState.description =
637
- "When an incident is created, it belongs to this state";
638
- createdIncidentState.color = Red;
639
- createdIncidentState.isCreatedState = true;
640
- createdIncidentState.projectId = createdItem.id;
641
- createdIncidentState.order = 1;
642
- createdIncidentState = await IncidentStateService.create({
643
- data: createdIncidentState,
644
- props: {
645
- isRoot: true,
646
- },
647
- });
648
- let acknowledgedIncidentState = new IncidentState();
649
- acknowledgedIncidentState.name = "Acknowledged";
650
- acknowledgedIncidentState.description =
651
- "When an incident is acknowledged, it belongs to this state.";
652
- acknowledgedIncidentState.color = Yellow;
653
- acknowledgedIncidentState.isAcknowledgedState = true;
654
- acknowledgedIncidentState.projectId = createdItem.id;
655
- acknowledgedIncidentState.order = 2;
656
- acknowledgedIncidentState = await IncidentStateService.create({
657
- data: acknowledgedIncidentState,
658
- props: {
659
- isRoot: true,
660
- },
661
- });
662
- let resolvedIncidentState = new IncidentState();
663
- resolvedIncidentState.name = "Resolved";
664
- resolvedIncidentState.description =
665
- "When an incident is resolved, it belongs to this state.";
666
- resolvedIncidentState.color = Green;
667
- resolvedIncidentState.isResolvedState = true;
668
- resolvedIncidentState.projectId = createdItem.id;
669
- resolvedIncidentState.order = 3;
670
- resolvedIncidentState = await IncidentStateService.create({
671
- data: resolvedIncidentState,
672
- props: {
673
- isRoot: true,
674
- },
675
- });
673
+ const projectId = createdItem.id;
674
+ // Idempotent — see getExistingProjectScopedNames.
675
+ const existingNames = await this.getExistingProjectScopedNames(IncidentStateService, projectId);
676
+ if (!existingNames.has("Identified")) {
677
+ const createdIncidentState = new IncidentState();
678
+ createdIncidentState.name = "Identified";
679
+ createdIncidentState.description =
680
+ "When an incident is created, it belongs to this state";
681
+ createdIncidentState.color = Red;
682
+ createdIncidentState.isCreatedState = true;
683
+ createdIncidentState.projectId = projectId;
684
+ createdIncidentState.order = 1;
685
+ await IncidentStateService.create({
686
+ data: createdIncidentState,
687
+ props: {
688
+ isRoot: true,
689
+ },
690
+ });
691
+ }
692
+ if (!existingNames.has("Acknowledged")) {
693
+ const acknowledgedIncidentState = new IncidentState();
694
+ acknowledgedIncidentState.name = "Acknowledged";
695
+ acknowledgedIncidentState.description =
696
+ "When an incident is acknowledged, it belongs to this state.";
697
+ acknowledgedIncidentState.color = Yellow;
698
+ acknowledgedIncidentState.isAcknowledgedState = true;
699
+ acknowledgedIncidentState.projectId = projectId;
700
+ acknowledgedIncidentState.order = 2;
701
+ await IncidentStateService.create({
702
+ data: acknowledgedIncidentState,
703
+ props: {
704
+ isRoot: true,
705
+ },
706
+ });
707
+ }
708
+ if (!existingNames.has("Resolved")) {
709
+ const resolvedIncidentState = new IncidentState();
710
+ resolvedIncidentState.name = "Resolved";
711
+ resolvedIncidentState.description =
712
+ "When an incident is resolved, it belongs to this state.";
713
+ resolvedIncidentState.color = Green;
714
+ resolvedIncidentState.isResolvedState = true;
715
+ resolvedIncidentState.projectId = projectId;
716
+ resolvedIncidentState.order = 3;
717
+ await IncidentStateService.create({
718
+ data: resolvedIncidentState,
719
+ props: {
720
+ isRoot: true,
721
+ },
722
+ });
723
+ }
676
724
  return createdItem;
677
725
  }
678
726
  async addDefaultAlertState(createdItem) {
679
- let createdAlertState = new AlertState();
680
- createdAlertState.name = "Identified";
681
- createdAlertState.description =
682
- "When an alert is created, it belongs to this state";
683
- createdAlertState.color = Red;
684
- createdAlertState.isCreatedState = true;
685
- createdAlertState.projectId = createdItem.id;
686
- createdAlertState.order = 1;
687
- createdAlertState = await AlertStateService.create({
688
- data: createdAlertState,
689
- props: {
690
- isRoot: true,
691
- },
692
- });
693
- let acknowledgedAlertState = new AlertState();
694
- acknowledgedAlertState.name = "Acknowledged";
695
- acknowledgedAlertState.description =
696
- "When an alert is acknowledged, it belongs to this state.";
697
- acknowledgedAlertState.color = Yellow;
698
- acknowledgedAlertState.isAcknowledgedState = true;
699
- acknowledgedAlertState.projectId = createdItem.id;
700
- acknowledgedAlertState.order = 2;
701
- acknowledgedAlertState = await AlertStateService.create({
702
- data: acknowledgedAlertState,
703
- props: {
704
- isRoot: true,
705
- },
706
- });
707
- let resolvedAlertState = new AlertState();
708
- resolvedAlertState.name = "Resolved";
709
- resolvedAlertState.description =
710
- "When an incident is resolved, it belongs to this state.";
711
- resolvedAlertState.color = Green;
712
- resolvedAlertState.isResolvedState = true;
713
- resolvedAlertState.projectId = createdItem.id;
714
- resolvedAlertState.order = 3;
715
- resolvedAlertState = await AlertStateService.create({
716
- data: resolvedAlertState,
717
- props: {
718
- isRoot: true,
719
- },
720
- });
727
+ const projectId = createdItem.id;
728
+ // Idempotent — see getExistingProjectScopedNames.
729
+ const existingNames = await this.getExistingProjectScopedNames(AlertStateService, projectId);
730
+ if (!existingNames.has("Identified")) {
731
+ const createdAlertState = new AlertState();
732
+ createdAlertState.name = "Identified";
733
+ createdAlertState.description =
734
+ "When an alert is created, it belongs to this state";
735
+ createdAlertState.color = Red;
736
+ createdAlertState.isCreatedState = true;
737
+ createdAlertState.projectId = projectId;
738
+ createdAlertState.order = 1;
739
+ await AlertStateService.create({
740
+ data: createdAlertState,
741
+ props: {
742
+ isRoot: true,
743
+ },
744
+ });
745
+ }
746
+ if (!existingNames.has("Acknowledged")) {
747
+ const acknowledgedAlertState = new AlertState();
748
+ acknowledgedAlertState.name = "Acknowledged";
749
+ acknowledgedAlertState.description =
750
+ "When an alert is acknowledged, it belongs to this state.";
751
+ acknowledgedAlertState.color = Yellow;
752
+ acknowledgedAlertState.isAcknowledgedState = true;
753
+ acknowledgedAlertState.projectId = projectId;
754
+ acknowledgedAlertState.order = 2;
755
+ await AlertStateService.create({
756
+ data: acknowledgedAlertState,
757
+ props: {
758
+ isRoot: true,
759
+ },
760
+ });
761
+ }
762
+ if (!existingNames.has("Resolved")) {
763
+ const resolvedAlertState = new AlertState();
764
+ resolvedAlertState.name = "Resolved";
765
+ resolvedAlertState.description =
766
+ "When an incident is resolved, it belongs to this state.";
767
+ resolvedAlertState.color = Green;
768
+ resolvedAlertState.isResolvedState = true;
769
+ resolvedAlertState.projectId = projectId;
770
+ resolvedAlertState.order = 3;
771
+ await AlertStateService.create({
772
+ data: resolvedAlertState,
773
+ props: {
774
+ isRoot: true,
775
+ },
776
+ });
777
+ }
721
778
  return createdItem;
722
779
  }
723
780
  async addDefaultAlertSeverity(createdItem) {
724
- let highSeverity = new AlertSeverity();
725
- highSeverity.name = "High";
726
- highSeverity.description =
727
- "Issues causing very high impact to customers. Immediate attention is required.";
728
- highSeverity.color = Moroon500;
729
- highSeverity.projectId = createdItem.id;
730
- highSeverity.order = 1;
731
- highSeverity = await AlertSeverityService.create({
732
- data: highSeverity,
733
- props: {
734
- isRoot: true,
735
- },
736
- });
737
- let lowSeverity = new AlertSeverity();
738
- lowSeverity.name = "Low";
739
- lowSeverity.description = "Issues causing low impact to customers.";
740
- lowSeverity.color = Yellow500;
741
- lowSeverity.projectId = createdItem.id;
742
- lowSeverity.order = 2;
743
- lowSeverity = await AlertSeverityService.create({
744
- data: lowSeverity,
745
- props: {
746
- isRoot: true,
747
- },
748
- });
781
+ const projectId = createdItem.id;
782
+ // Idempotent — see getExistingProjectScopedNames.
783
+ const existingNames = await this.getExistingProjectScopedNames(AlertSeverityService, projectId);
784
+ if (!existingNames.has("High")) {
785
+ const highSeverity = new AlertSeverity();
786
+ highSeverity.name = "High";
787
+ highSeverity.description =
788
+ "Issues causing very high impact to customers. Immediate attention is required.";
789
+ highSeverity.color = Moroon500;
790
+ highSeverity.projectId = projectId;
791
+ highSeverity.order = 1;
792
+ await AlertSeverityService.create({
793
+ data: highSeverity,
794
+ props: {
795
+ isRoot: true,
796
+ },
797
+ });
798
+ }
799
+ if (!existingNames.has("Low")) {
800
+ const lowSeverity = new AlertSeverity();
801
+ lowSeverity.name = "Low";
802
+ lowSeverity.description = "Issues causing low impact to customers.";
803
+ lowSeverity.color = Yellow500;
804
+ lowSeverity.projectId = projectId;
805
+ lowSeverity.order = 2;
806
+ await AlertSeverityService.create({
807
+ data: lowSeverity,
808
+ props: {
809
+ isRoot: true,
810
+ },
811
+ });
812
+ }
749
813
  return createdItem;
750
814
  }
751
815
  async addDefaultIncidentSeverity(createdItem) {
752
- let criticalIncident = new IncidentSeverity();
753
- criticalIncident.name = "Critical Incident";
754
- criticalIncident.description =
755
- "Issues causing very high impact to customers. Immediate response is required. Examples include a full outage, or a data breach.";
756
- criticalIncident.color = Moroon500;
757
- criticalIncident.projectId = createdItem.id;
758
- criticalIncident.order = 1;
759
- criticalIncident = await IncidentSeverityService.create({
760
- data: criticalIncident,
761
- props: {
762
- isRoot: true,
763
- },
764
- });
765
- let majorIncident = new IncidentSeverity();
766
- majorIncident.name = "Major Incident";
767
- majorIncident.description =
768
- "Issues causing significant impact. Immediate response is usually required. We might have some workarounds that mitigate the impact on customers. Examples include an important sub-system failing.";
769
- majorIncident.color = Red;
770
- majorIncident.projectId = createdItem.id;
771
- majorIncident.order = 2;
772
- majorIncident = await IncidentSeverityService.create({
773
- data: majorIncident,
774
- props: {
775
- isRoot: true,
776
- },
777
- });
778
- let minorIncident = new IncidentSeverity();
779
- minorIncident.name = "Minor Incident";
780
- minorIncident.description =
781
- "Issues with low impact, which can usually be handled within working hours. Most customers are unlikely to notice any problems. Examples include a slight drop in application performance.";
782
- minorIncident.color = Yellow;
783
- minorIncident.projectId = createdItem.id;
784
- minorIncident.order = 3;
785
- minorIncident = await IncidentSeverityService.create({
786
- data: minorIncident,
787
- props: {
788
- isRoot: true,
789
- },
790
- });
816
+ const projectId = createdItem.id;
817
+ // Idempotent see getExistingProjectScopedNames.
818
+ const existingNames = await this.getExistingProjectScopedNames(IncidentSeverityService, projectId);
819
+ if (!existingNames.has("Critical Incident")) {
820
+ const criticalIncident = new IncidentSeverity();
821
+ criticalIncident.name = "Critical Incident";
822
+ criticalIncident.description =
823
+ "Issues causing very high impact to customers. Immediate response is required. Examples include a full outage, or a data breach.";
824
+ criticalIncident.color = Moroon500;
825
+ criticalIncident.projectId = projectId;
826
+ criticalIncident.order = 1;
827
+ await IncidentSeverityService.create({
828
+ data: criticalIncident,
829
+ props: {
830
+ isRoot: true,
831
+ },
832
+ });
833
+ }
834
+ if (!existingNames.has("Major Incident")) {
835
+ const majorIncident = new IncidentSeverity();
836
+ majorIncident.name = "Major Incident";
837
+ majorIncident.description =
838
+ "Issues causing significant impact. Immediate response is usually required. We might have some workarounds that mitigate the impact on customers. Examples include an important sub-system failing.";
839
+ majorIncident.color = Red;
840
+ majorIncident.projectId = projectId;
841
+ majorIncident.order = 2;
842
+ await IncidentSeverityService.create({
843
+ data: majorIncident,
844
+ props: {
845
+ isRoot: true,
846
+ },
847
+ });
848
+ }
849
+ if (!existingNames.has("Minor Incident")) {
850
+ const minorIncident = new IncidentSeverity();
851
+ minorIncident.name = "Minor Incident";
852
+ minorIncident.description =
853
+ "Issues with low impact, which can usually be handled within working hours. Most customers are unlikely to notice any problems. Examples include a slight drop in application performance.";
854
+ minorIncident.color = Yellow;
855
+ minorIncident.projectId = projectId;
856
+ minorIncident.order = 3;
857
+ await IncidentSeverityService.create({
858
+ data: minorIncident,
859
+ props: {
860
+ isRoot: true,
861
+ },
862
+ });
863
+ }
791
864
  return createdItem;
792
865
  }
793
866
  async addDefaultIncidentRoles(createdItem) {
794
- let incidentCommander = new IncidentRole();
795
- incidentCommander.name = "Incident Commander";
796
- incidentCommander.description =
797
- "Primary decision maker during an incident. Responsible for coordinating the response and making final decisions.";
798
- incidentCommander.color = Purple500;
799
- incidentCommander.roleIcon = IconProp.ShieldCheck;
800
- incidentCommander.projectId = createdItem.id;
801
- incidentCommander.isPrimaryRole = true;
802
- incidentCommander.isDeleteable = false;
803
- incidentCommander = await IncidentRoleService.create({
804
- data: incidentCommander,
805
- props: {
806
- isRoot: true,
807
- },
808
- });
809
- let responder = new IncidentRole();
810
- responder.name = "Responder";
811
- responder.description =
812
- "Active participant in incident resolution. Performs hands-on work to resolve the incident.";
813
- responder.color = Blue500;
814
- responder.roleIcon = IconProp.Wrench;
815
- responder.projectId = createdItem.id;
816
- responder = await IncidentRoleService.create({
817
- data: responder,
818
- props: {
819
- isRoot: true,
820
- },
821
- });
822
- let communicationsLead = new IncidentRole();
823
- communicationsLead.name = "Communications Lead";
824
- communicationsLead.description =
825
- "Handles stakeholder communication and status updates during an incident.";
826
- communicationsLead.color = Teal500;
827
- communicationsLead.roleIcon = IconProp.Announcement;
828
- communicationsLead.projectId = createdItem.id;
829
- communicationsLead = await IncidentRoleService.create({
830
- data: communicationsLead,
831
- props: {
832
- isRoot: true,
833
- },
834
- });
835
- let observer = new IncidentRole();
836
- observer.name = "Observer";
837
- observer.description =
838
- "Read-only participant who monitors the incident without active involvement.";
839
- observer.color = Gray500;
840
- observer.roleIcon = IconProp.Activity;
841
- observer.projectId = createdItem.id;
842
- observer.canAssignMultipleUsers = true;
843
- observer = await IncidentRoleService.create({
844
- data: observer,
845
- props: {
846
- isRoot: true,
847
- },
848
- });
867
+ const projectId = createdItem.id;
868
+ // Idempotent see getExistingProjectScopedNames.
869
+ const existingNames = await this.getExistingProjectScopedNames(IncidentRoleService, projectId);
870
+ if (!existingNames.has("Incident Commander")) {
871
+ const incidentCommander = new IncidentRole();
872
+ incidentCommander.name = "Incident Commander";
873
+ incidentCommander.description =
874
+ "Primary decision maker during an incident. Responsible for coordinating the response and making final decisions.";
875
+ incidentCommander.color = Purple500;
876
+ incidentCommander.roleIcon = IconProp.ShieldCheck;
877
+ incidentCommander.projectId = projectId;
878
+ incidentCommander.isPrimaryRole = true;
879
+ incidentCommander.isDeleteable = false;
880
+ await IncidentRoleService.create({
881
+ data: incidentCommander,
882
+ props: {
883
+ isRoot: true,
884
+ },
885
+ });
886
+ }
887
+ if (!existingNames.has("Responder")) {
888
+ const responder = new IncidentRole();
889
+ responder.name = "Responder";
890
+ responder.description =
891
+ "Active participant in incident resolution. Performs hands-on work to resolve the incident.";
892
+ responder.color = Blue500;
893
+ responder.roleIcon = IconProp.Wrench;
894
+ responder.projectId = projectId;
895
+ await IncidentRoleService.create({
896
+ data: responder,
897
+ props: {
898
+ isRoot: true,
899
+ },
900
+ });
901
+ }
902
+ if (!existingNames.has("Communications Lead")) {
903
+ const communicationsLead = new IncidentRole();
904
+ communicationsLead.name = "Communications Lead";
905
+ communicationsLead.description =
906
+ "Handles stakeholder communication and status updates during an incident.";
907
+ communicationsLead.color = Teal500;
908
+ communicationsLead.roleIcon = IconProp.Announcement;
909
+ communicationsLead.projectId = projectId;
910
+ await IncidentRoleService.create({
911
+ data: communicationsLead,
912
+ props: {
913
+ isRoot: true,
914
+ },
915
+ });
916
+ }
917
+ if (!existingNames.has("Observer")) {
918
+ const observer = new IncidentRole();
919
+ observer.name = "Observer";
920
+ observer.description =
921
+ "Read-only participant who monitors the incident without active involvement.";
922
+ observer.color = Gray500;
923
+ observer.roleIcon = IconProp.Activity;
924
+ observer.projectId = projectId;
925
+ observer.canAssignMultipleUsers = true;
926
+ await IncidentRoleService.create({
927
+ data: observer,
928
+ props: {
929
+ isRoot: true,
930
+ },
931
+ });
932
+ }
849
933
  return createdItem;
850
934
  }
851
935
  async addDefaultMonitorStatus(createdItem) {
852
- let operationalStatus = new MonitorStatus();
853
- operationalStatus.name = "Operational";
854
- operationalStatus.description = "Monitor operating normally";
855
- operationalStatus.projectId = createdItem.id;
856
- operationalStatus.priority = 1;
857
- operationalStatus.isOperationalState = true;
858
- operationalStatus.color = Green;
859
- operationalStatus = await MonitorStatusService.create({
860
- data: operationalStatus,
861
- props: {
862
- isRoot: true,
863
- },
864
- });
865
- let degradedStatus = new MonitorStatus();
866
- degradedStatus.name = "Degraded";
867
- degradedStatus.description = "Monitor is operating at reduced performance.";
868
- degradedStatus.priority = 2;
869
- degradedStatus.projectId = createdItem.id;
870
- degradedStatus.color = Yellow;
871
- degradedStatus = await MonitorStatusService.create({
872
- data: degradedStatus,
873
- props: {
874
- isRoot: true,
875
- },
876
- });
877
- let downStatus = new MonitorStatus();
878
- downStatus.name = "Offline";
879
- downStatus.description = "Monitor is offline.";
880
- downStatus.isOfflineState = true;
881
- downStatus.projectId = createdItem.id;
882
- downStatus.priority = 3;
883
- downStatus.color = Red;
884
- downStatus = await MonitorStatusService.create({
885
- data: downStatus,
886
- props: {
887
- isRoot: true,
888
- },
889
- });
936
+ const projectId = createdItem.id;
937
+ // Idempotent — see getExistingProjectScopedNames.
938
+ const existingNames = await this.getExistingProjectScopedNames(MonitorStatusService, projectId);
939
+ if (!existingNames.has("Operational")) {
940
+ const operationalStatus = new MonitorStatus();
941
+ operationalStatus.name = "Operational";
942
+ operationalStatus.description = "Monitor operating normally";
943
+ operationalStatus.projectId = projectId;
944
+ operationalStatus.priority = 1;
945
+ operationalStatus.isOperationalState = true;
946
+ operationalStatus.color = Green;
947
+ await MonitorStatusService.create({
948
+ data: operationalStatus,
949
+ props: {
950
+ isRoot: true,
951
+ },
952
+ });
953
+ }
954
+ if (!existingNames.has("Degraded")) {
955
+ const degradedStatus = new MonitorStatus();
956
+ degradedStatus.name = "Degraded";
957
+ degradedStatus.description =
958
+ "Monitor is operating at reduced performance.";
959
+ degradedStatus.priority = 2;
960
+ degradedStatus.projectId = projectId;
961
+ degradedStatus.color = Yellow;
962
+ await MonitorStatusService.create({
963
+ data: degradedStatus,
964
+ props: {
965
+ isRoot: true,
966
+ },
967
+ });
968
+ }
969
+ if (!existingNames.has("Offline")) {
970
+ const downStatus = new MonitorStatus();
971
+ downStatus.name = "Offline";
972
+ downStatus.description = "Monitor is offline.";
973
+ downStatus.isOfflineState = true;
974
+ downStatus.projectId = projectId;
975
+ downStatus.priority = 3;
976
+ downStatus.color = Red;
977
+ await MonitorStatusService.create({
978
+ data: downStatus,
979
+ props: {
980
+ isRoot: true,
981
+ },
982
+ });
983
+ }
890
984
  return createdItem;
891
985
  }
892
986
  async addDefaultProjectTeams(createdItem) {