diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp index 8feca5f..6952291 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistBootstrap/HyperTwistContractLibrary.cpp @@ -1367,6 +1367,72 @@ FHyperTwistCoachSchedulePolicySummary UHyperTwistContractLibrary::MakeSampleTrai ); } +FHyperTwistTrainingCoachSessionQueueExecutionSummary UHyperTwistContractLibrary::MakeSampleTrainingCoachSessionQueueExecutionSummary() +{ + FHyperTwistTrainingRepositoryState RepositoryState = MakeSampleTrainingRepositoryState(); + FHyperTwistTrainingCoachSessionQueueState QueueState = + UHyperTwistTrainingRepositoryLibrary::BuildCoachSessionQueueState( + MakeSampleTrainingCoachSessionQueueSummary(), + TEXT("coach_queue_active"), + TEXT("2026-04-28T12:12:00Z") + ); + RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + RepositoryState, + QueueState + ); + QueueState = UHyperTwistTrainingRepositoryLibrary::DeferCoachSessionQueueEntry( + QueueState, + TEXT("coach_queue_follow_up"), + TEXT("2026-04-28T18:00:00Z"), + TEXT("follow-up-held-behind-primary"), + false, + TEXT("2026-04-28T12:13:00Z") + ); + RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + RepositoryState, + QueueState + ); + QueueState = UHyperTwistTrainingRepositoryLibrary::DeferCoachSessionQueueEntry( + QueueState, + TEXT("coach_queue_archive"), + TEXT("2026-04-29T09:00:00Z"), + TEXT("archive-retention-window"), + true, + TEXT("2026-04-28T12:14:00Z") + ); + RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + RepositoryState, + QueueState + ); + QueueState = UHyperTwistTrainingRepositoryLibrary::StartCoachSessionQueueEntryBySourceLabel( + QueueState, + TEXT("primary-brief"), + TEXT("coach_training_session_queued_01"), + TEXT("2026-04-28T12:20:00Z") + ); + RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + RepositoryState, + QueueState + ); + QueueState = UHyperTwistTrainingRepositoryLibrary::FinalizeCoachSessionQueueEntry( + QueueState, + TEXT("coach_training_session_queued_01"), + TEXT("2026-04-28T12:34:00Z"), + false + ); + RepositoryState = UHyperTwistTrainingRepositoryLibrary::UpsertCoachSessionQueueState( + RepositoryState, + QueueState + ); + + return UHyperTwistTrainingRepositoryLibrary::DeriveCoachSessionQueueExecutionSummary( + RepositoryState, + TEXT("local-user"), + TEXT("coach_queue_active"), + TEXT("2026-04-28T12:40:00Z") + ); +} + bool UHyperTwistContractLibrary::IsPuzzleStateStructurallyValid(const FHyperTwistPuzzleState& State) { return State.IsStructurallyValid(); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp index 46a7c97..761cc45 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.cpp @@ -704,6 +704,147 @@ namespace HyperTwistTrainingRepositoryLibraryInternal return NormalizedState; } + bool DidCoachSessionQueueEntryChange( + const FHyperTwistTrainingCoachSessionQueueStateEntry& PreviousEntry, + const FHyperTwistTrainingCoachSessionQueueStateEntry& CurrentEntry + ) + { + return PreviousEntry.WorkFingerprint != CurrentEntry.WorkFingerprint + || PreviousEntry.EntryState != CurrentEntry.EntryState + || PreviousEntry.ScheduledAtUtc != CurrentEntry.ScheduledAtUtc + || PreviousEntry.DeferredUntilUtc != CurrentEntry.DeferredUntilUtc + || PreviousEntry.DeferredAtUtc != CurrentEntry.DeferredAtUtc + || PreviousEntry.DeferralReasonLabel != CurrentEntry.DeferralReasonLabel + || PreviousEntry.StartedAtUtc != CurrentEntry.StartedAtUtc + || PreviousEntry.CompletedAtUtc != CurrentEntry.CompletedAtUtc + || PreviousEntry.RescheduleCount != CurrentEntry.RescheduleCount + || PreviousEntry.SourceTrainingSessionId != CurrentEntry.SourceTrainingSessionId + || PreviousEntry.bWasManuallyRescheduled != CurrentEntry.bWasManuallyRescheduled; + } + + EHyperTwistTrainingCoachSessionQueueEventKind DeriveCoachSessionQueueEventKind( + const FHyperTwistTrainingCoachSessionQueueStateEntry* PreviousEntry, + const FHyperTwistTrainingCoachSessionQueueStateEntry& CurrentEntry + ) + { + if (CurrentEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::InProgress) + { + return EHyperTwistTrainingCoachSessionQueueEventKind::Started; + } + if (CurrentEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred) + { + return EHyperTwistTrainingCoachSessionQueueEventKind::Deferred; + } + if (CurrentEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Completed) + { + return EHyperTwistTrainingCoachSessionQueueEventKind::Completed; + } + if (CurrentEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Skipped) + { + return EHyperTwistTrainingCoachSessionQueueEventKind::Skipped; + } + if (CurrentEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Expired) + { + return EHyperTwistTrainingCoachSessionQueueEventKind::Expired; + } + if (CurrentEntry.EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Ready) + { + return PreviousEntry != nullptr + && PreviousEntry->EntryState == EHyperTwistTrainingCoachSessionQueueEntryState::Deferred + ? EHyperTwistTrainingCoachSessionQueueEventKind::Promoted + : EHyperTwistTrainingCoachSessionQueueEventKind::Ready; + } + + return EHyperTwistTrainingCoachSessionQueueEventKind::Queued; + } + + FString ResolveCoachSessionQueueHistoryEventAtUtc( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FHyperTwistTrainingCoachSessionQueueStateEntry& Entry, + const EHyperTwistTrainingCoachSessionQueueEventKind EventKind + ) + { + switch (EventKind) + { + case EHyperTwistTrainingCoachSessionQueueEventKind::Started: + return ResolveReferenceUtc(!Entry.StartedAtUtc.IsEmpty() ? Entry.StartedAtUtc : QueueState.LastUpdatedAtUtc); + case EHyperTwistTrainingCoachSessionQueueEventKind::Deferred: + return ResolveReferenceUtc(!Entry.DeferredAtUtc.IsEmpty() ? Entry.DeferredAtUtc : QueueState.LastUpdatedAtUtc); + case EHyperTwistTrainingCoachSessionQueueEventKind::Completed: + case EHyperTwistTrainingCoachSessionQueueEventKind::Skipped: + case EHyperTwistTrainingCoachSessionQueueEventKind::Expired: + return ResolveReferenceUtc(!Entry.CompletedAtUtc.IsEmpty() ? Entry.CompletedAtUtc : QueueState.LastUpdatedAtUtc); + case EHyperTwistTrainingCoachSessionQueueEventKind::Promoted: + case EHyperTwistTrainingCoachSessionQueueEventKind::Ready: + return ResolveReferenceUtc(!Entry.ScheduledAtUtc.IsEmpty() ? Entry.ScheduledAtUtc : QueueState.LastUpdatedAtUtc); + default: + return ResolveReferenceUtc(QueueState.LastUpdatedAtUtc); + } + } + + FString MakeCoachSessionQueueHistoryEntryId( + const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Entry + ) + { + return FString::Printf( + TEXT("%s|%s|%d|%s|%s|%s"), + *Entry.QueueId, + *Entry.EntryId, + static_cast(Entry.EventKind), + *Entry.EventAtUtc, + *Entry.WorkFingerprint, + *Entry.SourceTrainingSessionId + ); + } + + FHyperTwistTrainingCoachSessionQueueHistoryEntry NormalizeCoachSessionQueueHistoryEntry( + const FHyperTwistTrainingCoachSessionQueueHistoryEntry& HistoryEntry + ) + { + FHyperTwistTrainingCoachSessionQueueHistoryEntry NormalizedEntry = HistoryEntry; + NormalizedEntry.EventAtUtc = ResolveReferenceUtc(NormalizedEntry.EventAtUtc); + NormalizedEntry.FocusCaseIds = DeduplicateCaseIds(NormalizedEntry.FocusCaseIds); + if (NormalizedEntry.HistoryEntryId.IsEmpty()) + { + NormalizedEntry.HistoryEntryId = MakeCoachSessionQueueHistoryEntryId(NormalizedEntry); + } + return NormalizedEntry; + } + + FHyperTwistTrainingCoachSessionQueueHistoryEntry BuildCoachSessionQueueHistoryEntry( + const FHyperTwistTrainingCoachSessionQueueState& QueueState, + const FHyperTwistTrainingCoachSessionQueueStateEntry& CurrentEntry, + const FHyperTwistTrainingCoachSessionQueueStateEntry* PreviousEntry + ) + { + FHyperTwistTrainingCoachSessionQueueHistoryEntry HistoryEntry; + HistoryEntry.QueueId = QueueState.QueueId; + HistoryEntry.UserId = QueueState.UserId; + HistoryEntry.EntryId = CurrentEntry.EntryId; + HistoryEntry.SourceLabel = CurrentEntry.SourceLabel; + HistoryEntry.WorkFingerprint = CurrentEntry.WorkFingerprint; + HistoryEntry.EventKind = DeriveCoachSessionQueueEventKind(PreviousEntry, CurrentEntry); + HistoryEntry.EventAtUtc = ResolveCoachSessionQueueHistoryEventAtUtc( + QueueState, + CurrentEntry, + HistoryEntry.EventKind + ); + HistoryEntry.PreviousEntryState = PreviousEntry != nullptr + ? PreviousEntry->EntryState + : CurrentEntry.EntryState; + HistoryEntry.NewEntryState = CurrentEntry.EntryState; + HistoryEntry.SourceTrainingSessionId = CurrentEntry.SourceTrainingSessionId; + HistoryEntry.DeferredUntilUtc = CurrentEntry.DeferredUntilUtc; + HistoryEntry.DeferralReasonLabel = CurrentEntry.DeferralReasonLabel; + HistoryEntry.FocusDeckId = CurrentEntry.FocusDeckId; + HistoryEntry.FocusMethodSegmentId = CurrentEntry.FocusMethodSegmentId; + HistoryEntry.FocusCaseIds = CurrentEntry.FocusCaseIds; + HistoryEntry.bRequiresFollowUp = CurrentEntry.bRequiresFollowUp; + HistoryEntry.bRequiresArchiveRetentionTuning = CurrentEntry.bRequiresArchiveRetentionTuning; + HistoryEntry.bWasManualReschedule = CurrentEntry.bWasManuallyRescheduled; + return NormalizeCoachSessionQueueHistoryEntry(HistoryEntry); + } + FHyperTwistTrainingCaseLearnerState DeriveLearnerStateFromReviewHistory( const FString& UserId, const FString& DeckId, @@ -2932,6 +3073,26 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar SeenCoachSessionQueueKeys.Add(QueueKey); } + Report.TotalCoachSessionQueueHistoryCount = RepositoryState.StoredCoachSessionQueueHistory.Num(); + TSet SeenCoachSessionQueueHistoryIds; + for (const FHyperTwistTrainingCoachSessionQueueHistoryEntry& HistoryEntry : RepositoryState.StoredCoachSessionQueueHistory) + { + const FHyperTwistTrainingCoachSessionQueueHistoryEntry NormalizedHistoryEntry = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueHistoryEntry(HistoryEntry); + if (!NormalizedHistoryEntry.IsStructurallyValid()) + { + Report.InvalidCoachSessionQueueHistoryCount += 1; + continue; + } + if (SeenCoachSessionQueueHistoryIds.Contains(NormalizedHistoryEntry.HistoryEntryId)) + { + Report.DuplicateCoachSessionQueueHistoryCount += 1; + continue; + } + + SeenCoachSessionQueueHistoryIds.Add(NormalizedHistoryEntry.HistoryEntryId); + } + if (Report.InvalidRunRecordCount > 0) { HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction( @@ -3047,6 +3208,20 @@ FHyperTwistTrainingRepositoryIntegrityReport UHyperTwistTrainingRepositoryLibrar TEXT("normalize-and-deduplicate-coach-session-queues") ); } + if (Report.InvalidCoachSessionQueueHistoryCount > 0) + { + HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction( + Report.RepairActions, + TEXT("drop-invalid-coach-session-queue-history") + ); + } + if (Report.DuplicateCoachSessionQueueHistoryCount > 0) + { + HyperTwistTrainingRepositoryLibraryInternal::AppendRepairAction( + Report.RepairActions, + TEXT("normalize-and-deduplicate-coach-session-queue-history") + ); + } return Report; } @@ -3409,6 +3584,55 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::RepairR return Left.QueueId < Right.QueueId; }); + TMap CoachSessionQueueHistoryIndexById; + for (const FHyperTwistTrainingCoachSessionQueueHistoryEntry& HistoryEntry : RepositoryState.StoredCoachSessionQueueHistory) + { + const FHyperTwistTrainingCoachSessionQueueHistoryEntry NormalizedHistoryEntry = + HyperTwistTrainingRepositoryLibraryInternal::NormalizeCoachSessionQueueHistoryEntry(HistoryEntry); + if (!NormalizedHistoryEntry.IsStructurallyValid()) + { + continue; + } + + if (const int32* ExistingIndex = CoachSessionQueueHistoryIndexById.Find(NormalizedHistoryEntry.HistoryEntryId)) + { + RepairedState.StoredCoachSessionQueueHistory[*ExistingIndex] = NormalizedHistoryEntry; + } + else + { + CoachSessionQueueHistoryIndexById.Add( + NormalizedHistoryEntry.HistoryEntryId, + RepairedState.StoredCoachSessionQueueHistory.Add(NormalizedHistoryEntry) + ); + } + } + + RepairedState.StoredCoachSessionQueueHistory.Sort([]( + const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Left, + const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Right + ) + { + if (Left.UserId != Right.UserId) + { + return Left.UserId < Right.UserId; + } + if (Left.QueueId != Right.QueueId) + { + return Left.QueueId < Right.QueueId; + } + + const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + Left.EventAtUtc, + Right.EventAtUtc + ); + if (UtcCompare != 0) + { + return UtcCompare < 0; + } + + return Left.HistoryEntryId < Right.HistoryEntryId; + }); + return RepairedState; } @@ -5657,6 +5881,47 @@ TArray UHyperTwistTrainingRepositoryL return QueueStates; } +TArray UHyperTwistTrainingRepositoryLibrary::ListCoachSessionQueueHistoryEntries( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& QueueId +) +{ + TArray HistoryEntries; + if (UserId.IsEmpty() || QueueId.IsEmpty()) + { + return HistoryEntries; + } + + for (const FHyperTwistTrainingCoachSessionQueueHistoryEntry& HistoryEntry : RepositoryState.StoredCoachSessionQueueHistory) + { + if (HistoryEntry.UserId == UserId + && HistoryEntry.QueueId == QueueId + && HistoryEntry.IsStructurallyValid()) + { + HistoryEntries.Add(HistoryEntry); + } + } + + HistoryEntries.Sort([]( + const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Left, + const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Right + ) + { + const int32 UtcCompare = HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + Left.EventAtUtc, + Right.EventAtUtc + ); + if (UtcCompare != 0) + { + return UtcCompare < 0; + } + + return Left.HistoryEntryId < Right.HistoryEntryId; + }); + return HistoryEntries; +} + bool UHyperTwistTrainingRepositoryLibrary::TryGetCoachSessionQueueState( const FHyperTwistTrainingRepositoryState& RepositoryState, const FString& UserId, @@ -5701,7 +5966,7 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertC return UpdatedRepositoryState; } - FHyperTwistTrainingCoachSessionQueueState* ExistingQueueState = + const FHyperTwistTrainingCoachSessionQueueState* ExistingQueueState = UpdatedRepositoryState.StoredCoachSessionQueues.FindByPredicate( [&NormalizedQueueState](const FHyperTwistTrainingCoachSessionQueueState& Candidate) { @@ -5709,9 +5974,65 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertC && Candidate.QueueId == NormalizedQueueState.QueueId; } ); - if (ExistingQueueState != nullptr) + + for (const FHyperTwistTrainingCoachSessionQueueStateEntry& CurrentEntry : NormalizedQueueState.Entries) { - *ExistingQueueState = NormalizedQueueState; + const FHyperTwistTrainingCoachSessionQueueStateEntry* PreviousEntry = ExistingQueueState != nullptr + ? ExistingQueueState->Entries.FindByPredicate( + [&CurrentEntry](const FHyperTwistTrainingCoachSessionQueueStateEntry& Candidate) + { + return Candidate.EntryId == CurrentEntry.EntryId; + } + ) + : nullptr; + if (PreviousEntry != nullptr && PreviousEntry->WorkFingerprint != CurrentEntry.WorkFingerprint) + { + PreviousEntry = nullptr; + } + if (PreviousEntry != nullptr + && !HyperTwistTrainingRepositoryLibraryInternal::DidCoachSessionQueueEntryChange( + *PreviousEntry, + CurrentEntry + )) + { + continue; + } + + const FHyperTwistTrainingCoachSessionQueueHistoryEntry HistoryEntry = + HyperTwistTrainingRepositoryLibraryInternal::BuildCoachSessionQueueHistoryEntry( + NormalizedQueueState, + CurrentEntry, + PreviousEntry + ); + if (!HistoryEntry.IsStructurallyValid()) + { + continue; + } + + const bool bAlreadyExists = + UpdatedRepositoryState.StoredCoachSessionQueueHistory.ContainsByPredicate( + [&HistoryEntry](const FHyperTwistTrainingCoachSessionQueueHistoryEntry& Candidate) + { + return Candidate.HistoryEntryId == HistoryEntry.HistoryEntryId; + } + ); + if (!bAlreadyExists) + { + UpdatedRepositoryState.StoredCoachSessionQueueHistory.Add(HistoryEntry); + } + } + + FHyperTwistTrainingCoachSessionQueueState* MutableExistingQueueState = + UpdatedRepositoryState.StoredCoachSessionQueues.FindByPredicate( + [&NormalizedQueueState](const FHyperTwistTrainingCoachSessionQueueState& Candidate) + { + return Candidate.UserId == NormalizedQueueState.UserId + && Candidate.QueueId == NormalizedQueueState.QueueId; + } + ); + if (MutableExistingQueueState != nullptr) + { + *MutableExistingQueueState = NormalizedQueueState; } else { @@ -5721,6 +6042,202 @@ FHyperTwistTrainingRepositoryState UHyperTwistTrainingRepositoryLibrary::UpsertC return UpdatedRepositoryState; } +FHyperTwistTrainingCoachSessionQueueExecutionSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachSessionQueueExecutionSummary( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& QueueId, + const FString& ReferenceUtc +) +{ + FHyperTwistTrainingCoachSessionQueueExecutionSummary Summary; + if (UserId.IsEmpty() || QueueId.IsEmpty()) + { + return Summary; + } + + Summary.UserId = UserId; + Summary.QueueId = QueueId; + Summary.ReferenceUtc = HyperTwistTrainingRepositoryLibraryInternal::ResolveReferenceUtc(ReferenceUtc); + + FHyperTwistTrainingCoachSessionQueueState QueueState; + if (TryGetCoachSessionQueueState(RepositoryState, UserId, QueueId, QueueState)) + { + Summary.FocusDeckId = QueueState.FocusDeckId; + Summary.FocusMethodSegmentId = QueueState.FocusMethodSegmentId; + Summary.bHasOutstandingDeferredWork = + QueueState.DeferredEntryCount > 0 || QueueState.PendingEntryCount > 0; + } + + const TArray HistoryEntries = + ListCoachSessionQueueHistoryEntries(RepositoryState, UserId, QueueId); + if (HistoryEntries.Num() == 0) + { + return Summary; + } + + Summary.TotalHistoryEntryCount = HistoryEntries.Num(); + TSet UniqueEntryIds; + TMap DeferredCountsByEntryId; + TMap CompletedCountsByEntryId; + TMap DeferralReasonCounts; + + for (const FHyperTwistTrainingCoachSessionQueueHistoryEntry& HistoryEntry : HistoryEntries) + { + if (!HistoryEntry.IsStructurallyValid()) + { + continue; + } + + UniqueEntryIds.Add(HistoryEntry.EntryId); + if (Summary.FocusDeckId.IsEmpty()) + { + Summary.FocusDeckId = HistoryEntry.FocusDeckId; + } + if (Summary.FocusMethodSegmentId.IsEmpty()) + { + Summary.FocusMethodSegmentId = HistoryEntry.FocusMethodSegmentId; + } + + if (HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + HistoryEntry.EventAtUtc, + Summary.ReferenceUtc + ) > 0) + { + continue; + } + + switch (HistoryEntry.EventKind) + { + case EHyperTwistTrainingCoachSessionQueueEventKind::Queued: + Summary.QueuedEventCount += 1; + break; + case EHyperTwistTrainingCoachSessionQueueEventKind::Ready: + Summary.ReadyEventCount += 1; + break; + case EHyperTwistTrainingCoachSessionQueueEventKind::Started: + Summary.StartedEventCount += 1; + if (Summary.LastStartedAtUtc.IsEmpty() + || HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + HistoryEntry.EventAtUtc, + Summary.LastStartedAtUtc + ) > 0) + { + Summary.LastStartedAtUtc = HistoryEntry.EventAtUtc; + } + break; + case EHyperTwistTrainingCoachSessionQueueEventKind::Deferred: + Summary.DeferredEventCount += 1; + DeferredCountsByEntryId.FindOrAdd(HistoryEntry.EntryId) += 1; + if (!HistoryEntry.DeferralReasonLabel.IsEmpty()) + { + DeferralReasonCounts.FindOrAdd(HistoryEntry.DeferralReasonLabel) += 1; + } + if (Summary.LastDeferredAtUtc.IsEmpty() + || HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + HistoryEntry.EventAtUtc, + Summary.LastDeferredAtUtc + ) > 0) + { + Summary.LastDeferredAtUtc = HistoryEntry.EventAtUtc; + } + break; + case EHyperTwistTrainingCoachSessionQueueEventKind::Promoted: + Summary.PromotedEventCount += 1; + break; + case EHyperTwistTrainingCoachSessionQueueEventKind::Completed: + Summary.CompletedEventCount += 1; + CompletedCountsByEntryId.FindOrAdd(HistoryEntry.EntryId) += 1; + if (Summary.LastCompletedAtUtc.IsEmpty() + || HyperTwistTrainingRepositoryLibraryInternal::CompareUtcStrings( + HistoryEntry.EventAtUtc, + Summary.LastCompletedAtUtc + ) > 0) + { + Summary.LastCompletedAtUtc = HistoryEntry.EventAtUtc; + } + break; + case EHyperTwistTrainingCoachSessionQueueEventKind::Skipped: + Summary.SkippedEventCount += 1; + break; + case EHyperTwistTrainingCoachSessionQueueEventKind::Expired: + Summary.ExpiredEventCount += 1; + break; + default: + break; + } + + if (HistoryEntry.bWasManualReschedule) + { + Summary.ManualRescheduleEventCount += 1; + } + if (HistoryEntry.bRequiresArchiveRetentionTuning) + { + Summary.ArchiveAwareEventCount += 1; + } + if (HistoryEntry.bRequiresFollowUp + && HistoryEntry.EventKind == EHyperTwistTrainingCoachSessionQueueEventKind::Deferred) + { + Summary.BlockedFollowUpEventCount += 1; + } + } + + Summary.UniqueEntryCount = UniqueEntryIds.Num(); + + int32 DominantDeferredCount = 0; + for (const TPair& Pair : DeferredCountsByEntryId) + { + if (Pair.Value > DominantDeferredCount + || (Pair.Value == DominantDeferredCount + && (Summary.MostDeferredEntryId.IsEmpty() || Pair.Key < Summary.MostDeferredEntryId))) + { + DominantDeferredCount = Pair.Value; + Summary.MostDeferredEntryId = Pair.Key; + } + } + + int32 DominantCompletedCount = 0; + for (const TPair& Pair : CompletedCountsByEntryId) + { + if (Pair.Value > DominantCompletedCount + || (Pair.Value == DominantCompletedCount + && (Summary.MostCompletedEntryId.IsEmpty() || Pair.Key < Summary.MostCompletedEntryId))) + { + DominantCompletedCount = Pair.Value; + Summary.MostCompletedEntryId = Pair.Key; + } + } + + int32 DominantReasonCount = 0; + for (const TPair& Pair : DeferralReasonCounts) + { + if (Pair.Value > DominantReasonCount + || (Pair.Value == DominantReasonCount + && (Summary.DominantDeferralReasonLabel.IsEmpty() || Pair.Key < Summary.DominantDeferralReasonLabel))) + { + DominantReasonCount = Pair.Value; + Summary.DominantDeferralReasonLabel = Pair.Key; + } + } + + const int32 TerminalEventCount = + Summary.CompletedEventCount + Summary.SkippedEventCount + Summary.ExpiredEventCount; + Summary.bHasCompletionGap = Summary.StartedEventCount > TerminalEventCount; + Summary.bHasArchiveAwareFriction = Summary.ArchiveAwareEventCount > 0; + Summary.bHasBlockedFollowUpFriction = Summary.BlockedFollowUpEventCount > 0; + Summary.bHasRepeatManualRescheduleFriction = Summary.ManualRescheduleEventCount >= 2; + Summary.ExecutionPressureScore = FMath::Clamp( + static_cast(Summary.DeferredEventCount) * 7.0f + + static_cast(Summary.ManualRescheduleEventCount) * 10.0f + + static_cast(Summary.ArchiveAwareEventCount) * 12.0f + + static_cast(Summary.BlockedFollowUpEventCount) * 12.0f + + (Summary.bHasCompletionGap ? 10.0f : 0.0f), + 0.0f, + 100.0f + ); + + return Summary; +} + FHyperTwistTrainingCoachActionPlanSummary UHyperTwistTrainingRepositoryLibrary::DeriveCoachActionPlanSummary( const FHyperTwistTrainingRepositoryState& RepositoryState, const FString& UserId, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp index d4c14b6..de1aa7d 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.cpp @@ -405,6 +405,18 @@ FHyperTwistCoachSchedulePolicySummary UHyperTwistTrainingRuntimeLibrary::GetActi return FHyperTwistCoachSchedulePolicySummary(); } +FHyperTwistTrainingCoachSessionQueueExecutionSummary UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingCoachSessionQueueExecutionSummary( + UObject* WorldContextObject +) +{ + if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) + { + return TrainingSubsystem->GetActiveCoachSessionQueueExecutionSummary(); + } + + return FHyperTwistTrainingCoachSessionQueueExecutionSummary(); +} + TArray UHyperTwistTrainingRuntimeLibrary::GetActiveTrainingLearnerMethodSegments(UObject* WorldContextObject) { if (UHyperTwistTrainingSubsystem* TrainingSubsystem = GetTrainingSubsystem(WorldContextObject)) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp index 0c58cf3..e4de8ff 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSubsystem.cpp @@ -183,6 +183,11 @@ FHyperTwistCoachSchedulePolicySummary UHyperTwistTrainingSubsystem::GetActiveCoa return ActiveCoachSchedulePolicySummary; } +FHyperTwistTrainingCoachSessionQueueExecutionSummary UHyperTwistTrainingSubsystem::GetActiveCoachSessionQueueExecutionSummary() const +{ + return ActiveCoachSessionQueueExecutionSummary; +} + TArray UHyperTwistTrainingSubsystem::GetActiveLearnerMethodSegments() const { return ActiveLearnerMethodSegments; @@ -1385,6 +1390,7 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews() ActiveCoachSessionQueueState = FHyperTwistTrainingCoachSessionQueueState(); ActiveCoachScheduleHorizonSummary = FHyperTwistCoachScheduleHorizonSummary(); ActiveCoachSchedulePolicySummary = FHyperTwistCoachSchedulePolicySummary(); + ActiveCoachSessionQueueExecutionSummary = FHyperTwistTrainingCoachSessionQueueExecutionSummary(); ActiveLearnerMethodSegments.Reset(); ActiveLearnerPresets.Reset(); ActiveTrainingSessionTemplates.Reset(); @@ -1598,12 +1604,20 @@ void UHyperTwistTrainingSubsystem::RefreshRepositoryViews() ActiveCoachArchivePolicySummary, ActiveLearnerDeckStateSummary.ReferenceUtc ); + ActiveCoachSessionQueueExecutionSummary = + UHyperTwistTrainingRepositoryLibrary::DeriveCoachSessionQueueExecutionSummary( + TrainingRepositoryState, + ActiveCoachSessionQueueState.UserId, + ActiveCoachSessionQueueState.QueueId, + ActiveLearnerDeckStateSummary.ReferenceUtc + ); } else { ActiveCoachSessionQueueState = FHyperTwistTrainingCoachSessionQueueState(); ActiveCoachScheduleHorizonSummary = FHyperTwistCoachScheduleHorizonSummary(); ActiveCoachSchedulePolicySummary = FHyperTwistCoachSchedulePolicySummary(); + ActiveCoachSessionQueueExecutionSummary = FHyperTwistTrainingCoachSessionQueueExecutionSummary(); } } diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h index 957d61b..85c85ac 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistBootstrap/HyperTwistContractLibrary.h @@ -218,6 +218,9 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") static FHyperTwistCoachSchedulePolicySummary MakeSampleTrainingCoachSchedulePolicySummary(); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + static FHyperTwistTrainingCoachSessionQueueExecutionSummary MakeSampleTrainingCoachSessionQueueExecutionSummary(); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Validation") static bool IsPuzzleStateStructurallyValid(const FHyperTwistPuzzleState& State); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h index 9ae05b9..dc497f1 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRepositoryLibrary.h @@ -400,6 +400,13 @@ public: const FString& UserId ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static TArray ListCoachSessionQueueHistoryEntries( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& QueueId + ); + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Repository|Coach") static bool TryGetCoachSessionQueueState( const FHyperTwistTrainingRepositoryState& RepositoryState, @@ -414,6 +421,14 @@ public: const FHyperTwistTrainingCoachSessionQueueState& QueueState ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") + static FHyperTwistTrainingCoachSessionQueueExecutionSummary DeriveCoachSessionQueueExecutionSummary( + const FHyperTwistTrainingRepositoryState& RepositoryState, + const FString& UserId, + const FString& QueueId, + const FString& ReferenceUtc + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Repository|Coach") static FHyperTwistTrainingCoachActionPlanSummary DeriveCoachActionPlanSummary( const FHyperTwistTrainingRepositoryState& RepositoryState, diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h index 731114a..3631798 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h @@ -135,6 +135,11 @@ public: UObject* WorldContextObject ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach", meta = (WorldContext = "WorldContextObject")) + static FHyperTwistTrainingCoachSessionQueueExecutionSummary GetActiveTrainingCoachSessionQueueExecutionSummary( + UObject* WorldContextObject + ); + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training", meta = (WorldContext = "WorldContextObject")) static TArray GetActiveTrainingLearnerMethodSegments(UObject* WorldContextObject); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h index a7609a1..e6a65eb 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSubsystem.h @@ -116,6 +116,9 @@ public: UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") FHyperTwistCoachSchedulePolicySummary GetActiveCoachSchedulePolicySummary() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach") + FHyperTwistTrainingCoachSessionQueueExecutionSummary GetActiveCoachSessionQueueExecutionSummary() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training") TArray GetActiveLearnerMethodSegments() const; @@ -498,6 +501,9 @@ private: UPROPERTY() FHyperTwistCoachSchedulePolicySummary ActiveCoachSchedulePolicySummary; + UPROPERTY() + FHyperTwistTrainingCoachSessionQueueExecutionSummary ActiveCoachSessionQueueExecutionSummary; + UPROPERTY() TArray ActiveLearnerMethodSegments; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 4217ceb..b920b39 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -2730,6 +2730,19 @@ enum class EHyperTwistTrainingCoachSessionQueueEntryState : uint8 Expired UMETA(DisplayName = "Expired") }; +UENUM(BlueprintType) +enum class EHyperTwistTrainingCoachSessionQueueEventKind : uint8 +{ + Queued UMETA(DisplayName = "Queued"), + Ready UMETA(DisplayName = "Ready"), + Started UMETA(DisplayName = "Started"), + Deferred UMETA(DisplayName = "Deferred"), + Promoted UMETA(DisplayName = "Promoted"), + Completed UMETA(DisplayName = "Completed"), + Skipped UMETA(DisplayName = "Skipped"), + Expired UMETA(DisplayName = "Expired") +}; + USTRUCT(BlueprintType) struct FHyperTwistTrainingCoachActionPlanEntry { @@ -3035,6 +3048,182 @@ struct FHyperTwistTrainingCoachSessionQueueState } }; +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachSessionQueueHistoryEntry +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString HistoryEntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString QueueId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString EntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString WorkFingerprint; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingCoachSessionQueueEventKind EventKind = + EHyperTwistTrainingCoachSessionQueueEventKind::Queued; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString EventAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingCoachSessionQueueEntryState PreviousEntryState = + EHyperTwistTrainingCoachSessionQueueEntryState::Pending; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + EHyperTwistTrainingCoachSessionQueueEntryState NewEntryState = + EHyperTwistTrainingCoachSessionQueueEntryState::Pending; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SourceTrainingSessionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeferredUntilUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeferralReasonLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusMethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray FocusCaseIds; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bRequiresFollowUp = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bRequiresArchiveRetentionTuning = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bWasManualReschedule = false; + + bool IsStructurallyValid() const + { + return !HistoryEntryId.IsEmpty() + && !QueueId.IsEmpty() + && !UserId.IsEmpty() + && !EntryId.IsEmpty() + && !EventAtUtc.IsEmpty(); + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachSessionQueueExecutionSummary +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString UserId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString QueueId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ReferenceUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString FocusMethodSegmentId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalHistoryEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 UniqueEntryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 QueuedEventCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ReadyEventCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 StartedEventCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DeferredEventCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 PromotedEventCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CompletedEventCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SkippedEventCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ExpiredEventCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ManualRescheduleEventCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ArchiveAwareEventCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 BlockedFollowUpEventCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString MostDeferredEntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString MostCompletedEntryId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DominantDeferralReasonLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LastStartedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LastDeferredAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LastCompletedAtUtc; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + float ExecutionPressureScore = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasOutstandingDeferredWork = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasCompletionGap = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasArchiveAwareFriction = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasBlockedFollowUpFriction = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasRepeatManualRescheduleFriction = false; + + bool IsStructurallyValid() const + { + return !UserId.IsEmpty() && !QueueId.IsEmpty(); + } +}; + USTRUCT(BlueprintType) struct FHyperTwistTrainingCoachActionPlanSummary { @@ -3703,6 +3892,9 @@ struct FHyperTwistTrainingRepositoryState UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") TArray StoredCoachSessionQueues; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + TArray StoredCoachSessionQueueHistory; }; USTRUCT(BlueprintType) @@ -3782,6 +3974,15 @@ struct FHyperTwistTrainingRepositoryIntegrityReport UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") int32 DuplicateCoachSessionQueueCount = 0; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TotalCoachSessionQueueHistoryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 InvalidCoachSessionQueueHistoryCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DuplicateCoachSessionQueueHistoryCount = 0; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") TArray RepairActions; @@ -3802,6 +4003,8 @@ struct FHyperTwistTrainingRepositoryIntegrityReport || InvalidCoachActionPlanCount > 0 || DuplicateCoachActionPlanCount > 0 || InvalidCoachSessionQueueCount > 0 - || DuplicateCoachSessionQueueCount > 0; + || DuplicateCoachSessionQueueCount > 0 + || InvalidCoachSessionQueueHistoryCount > 0 + || DuplicateCoachSessionQueueHistoryCount > 0; } };