From f494f0707d264ff3dd5c2f6389d6974d53f8fa3d Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Mon, 27 Apr 2026 18:50:15 +0200 Subject: [PATCH] Weight queue recovery by dashboard outcomes --- .../HyperTwistCoachDashboardWidget.cpp | 487 ++++++++++++------ .../HyperTwistCoachDashboardWidget.h | 32 ++ 2 files changed, 375 insertions(+), 144 deletions(-) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index f1594ca..a068c00 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -265,31 +265,24 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() const bool bBeforeOutstandingDeferred = CachedCoachQueueExecutionSummary.bHasOutstandingDeferredWork; const bool bBeforeCompletionGap = CachedCoachQueueExecutionSummary.bHasCompletionGap; const FString SourceLabel = CachedCoachPanelState.PreferredGuidanceSourceLabel; + const FHyperTwistCoachDashboardQueueRecoveryWeighting QueueRecoveryWeighting = BuildQueueRecoveryWeighting(); const FString DeferredEntryId = FindFirstDeferredQueueEntryId(); - if (!DeferredEntryId.IsEmpty()) - { - const bool bSucceeded = PromoteFirstDeferredQueueEntry(); - RecordQueueRecoveryOutcome( - SourceLabel, - TEXT("promote-deferred-recovery"), - BeforePressureScore, - BeforeDeferredEventCount, - bBeforeBlockedFollowUp, - bBeforeOutstandingDeferred, - bBeforeCompletionGap, - bSucceeded - ); - UpdateDashboardPresentation(); - return bSucceeded; - } - const bool bQueueRecoveryMemorySource = CachedCoachPanelState.PreferredGuidanceSourceLabel.StartsWith(TEXT("coach-memory-queue-")); const bool bPrefersFollowUp = CachedCoachPanelState.PreferredGuidanceLane == EHyperTwistTrainingCoachGuidanceLane::FollowUp; const bool bPrefersRecommended = CachedCoachPanelState.PreferredGuidanceLane == EHyperTwistTrainingCoachGuidanceLane::Recommended; + const bool bCanLaunchQueueRecovery = + CachedCoachPanelState.bCanStartQueuedRun + || (CachedCoachPanelState.bCanStartCoachFollowUpRun && CachedCoachPanelState.bHasFollowUpGuidance) + || CachedCoachPanelState.bCanStartCoachRecommendedRun; + const bool bPreferLaunchBeforePromote = + !DeferredEntryId.IsEmpty() + && bCanLaunchQueueRecovery + && QueueRecoveryWeighting.PreferredActionKind + == FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery; auto StartRunAndRefresh = [this](const FHyperTwistTrainingRunState& RunState) { @@ -299,130 +292,145 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() UpdateDashboardPresentation(); return RunState.Session.IsStructurallyValid(); }; - - if (bQueueRecoveryMemorySource && CachedCoachPanelState.bCanStartQueuedRun) - { - const bool bQueueMatchesPreference = - (!bPrefersFollowUp && !bPrefersRecommended) - || (bPrefersFollowUp - && CachedCoachPanelState.NextQueueSourceLabel == TEXT("follow-up-brief")) - || (bPrefersRecommended - && CachedCoachPanelState.NextQueueSourceLabel == TEXT("primary-brief")); - if (bQueueMatchesPreference) - { - const bool bSucceeded = StartRunAndRefresh(StartNextQueuedCoachRun(DefaultCoachMaxCases)); - RecordQueueRecoveryOutcome( - SourceLabel, - TEXT("launch-queued-recovery"), - BeforePressureScore, - BeforeDeferredEventCount, - bBeforeBlockedFollowUp, - bBeforeOutstandingDeferred, - bBeforeCompletionGap, - bSucceeded - ); - UpdateDashboardPresentation(); - return bSucceeded; - } - } - - if (bPrefersFollowUp - && CachedCoachPanelState.bCanStartCoachFollowUpRun - && CachedCoachPanelState.bHasFollowUpGuidance) - { - const bool bSucceeded = StartRunAndRefresh(StartCoachFollowUpRun(DefaultCoachMaxCases)); - RecordQueueRecoveryOutcome( - SourceLabel, - TEXT("launch-follow-up-recovery"), - BeforePressureScore, - BeforeDeferredEventCount, - bBeforeBlockedFollowUp, - bBeforeOutstandingDeferred, - bBeforeCompletionGap, - bSucceeded - ); - UpdateDashboardPresentation(); - return bSucceeded; - } - if (bPrefersRecommended && CachedCoachPanelState.bCanStartCoachRecommendedRun) - { - const bool bSucceeded = StartRunAndRefresh(StartCoachRecommendedRun(DefaultCoachMaxCases)); - RecordQueueRecoveryOutcome( - SourceLabel, - TEXT("launch-recommended-recovery"), - BeforePressureScore, - BeforeDeferredEventCount, - bBeforeBlockedFollowUp, - bBeforeOutstandingDeferred, - bBeforeCompletionGap, - bSucceeded - ); - UpdateDashboardPresentation(); - return bSucceeded; - } - if (CachedCoachPanelState.bCanStartQueuedRun) - { - const bool bSucceeded = StartRunAndRefresh(StartNextQueuedCoachRun(DefaultCoachMaxCases)); - RecordQueueRecoveryOutcome( - SourceLabel, - TEXT("launch-queued-fallback"), - BeforePressureScore, - BeforeDeferredEventCount, - bBeforeBlockedFollowUp, - bBeforeOutstandingDeferred, - bBeforeCompletionGap, - bSucceeded - ); - UpdateDashboardPresentation(); - return bSucceeded; - } - if (CachedCoachPanelState.bCanStartCoachFollowUpRun && CachedCoachPanelState.bHasFollowUpGuidance) - { - const bool bSucceeded = StartRunAndRefresh(StartCoachFollowUpRun(DefaultCoachMaxCases)); - RecordQueueRecoveryOutcome( - SourceLabel, - TEXT("launch-follow-up-fallback"), - BeforePressureScore, - BeforeDeferredEventCount, - bBeforeBlockedFollowUp, - bBeforeOutstandingDeferred, - bBeforeCompletionGap, - bSucceeded - ); - UpdateDashboardPresentation(); - return bSucceeded; - } - if (CachedCoachPanelState.bCanStartCoachRecommendedRun) - { - const bool bSucceeded = StartRunAndRefresh(StartCoachRecommendedRun(DefaultCoachMaxCases)); - RecordQueueRecoveryOutcome( - SourceLabel, - TEXT("launch-recommended-fallback"), - BeforePressureScore, - BeforeDeferredEventCount, - bBeforeBlockedFollowUp, - bBeforeOutstandingDeferred, - bBeforeCompletionGap, - bSucceeded - ); - UpdateDashboardPresentation(); - return bSucceeded; - } - - const FHyperTwistTrainingRunState FallbackRunState = ExecutePrimaryCoachAction(); - const bool bSucceeded = FallbackRunState.Session.IsStructurallyValid(); - RecordQueueRecoveryOutcome( - SourceLabel, - TEXT("launch-primary-fallback"), + auto RecordAndFinalizeOutcome = [ + this, + &SourceLabel, BeforePressureScore, BeforeDeferredEventCount, bBeforeBlockedFollowUp, bBeforeOutstandingDeferred, - bBeforeCompletionGap, - bSucceeded - ); - UpdateDashboardPresentation(); - return bSucceeded; + bBeforeCompletionGap + ]( + const TCHAR* AppliedActionLabel, + const FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind ActionKind, + const bool bSucceeded) + { + RecordQueueRecoveryOutcome( + SourceLabel, + AppliedActionLabel, + ActionKind, + BeforePressureScore, + BeforeDeferredEventCount, + bBeforeBlockedFollowUp, + bBeforeOutstandingDeferred, + bBeforeCompletionGap, + bSucceeded + ); + UpdateDashboardPresentation(); + return bSucceeded; + }; + auto TryPromoteRecovery = [&]() -> bool + { + if (DeferredEntryId.IsEmpty()) + { + return false; + } + + const bool bSucceeded = PromoteFirstDeferredQueueEntry(); + return RecordAndFinalizeOutcome( + TEXT("promote-deferred-recovery"), + FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::PromoteDeferred, + bSucceeded + ); + }; + auto TryLaunchRecovery = [&]() -> bool + { + if (bQueueRecoveryMemorySource && CachedCoachPanelState.bCanStartQueuedRun) + { + const bool bQueueMatchesPreference = + (!bPrefersFollowUp && !bPrefersRecommended) + || (bPrefersFollowUp + && CachedCoachPanelState.NextQueueSourceLabel == TEXT("follow-up-brief")) + || (bPrefersRecommended + && CachedCoachPanelState.NextQueueSourceLabel == TEXT("primary-brief")); + if (bQueueMatchesPreference) + { + const bool bSucceeded = StartRunAndRefresh(StartNextQueuedCoachRun(DefaultCoachMaxCases)); + return RecordAndFinalizeOutcome( + TEXT("launch-queued-recovery"), + FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery, + bSucceeded + ); + } + } + + if (bPrefersFollowUp + && CachedCoachPanelState.bCanStartCoachFollowUpRun + && CachedCoachPanelState.bHasFollowUpGuidance) + { + const bool bSucceeded = StartRunAndRefresh(StartCoachFollowUpRun(DefaultCoachMaxCases)); + return RecordAndFinalizeOutcome( + TEXT("launch-follow-up-recovery"), + FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery, + bSucceeded + ); + } + if (bPrefersRecommended && CachedCoachPanelState.bCanStartCoachRecommendedRun) + { + const bool bSucceeded = StartRunAndRefresh(StartCoachRecommendedRun(DefaultCoachMaxCases)); + return RecordAndFinalizeOutcome( + TEXT("launch-recommended-recovery"), + FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery, + bSucceeded + ); + } + if (CachedCoachPanelState.bCanStartQueuedRun) + { + const bool bSucceeded = StartRunAndRefresh(StartNextQueuedCoachRun(DefaultCoachMaxCases)); + return RecordAndFinalizeOutcome( + TEXT("launch-queued-fallback"), + FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery, + bSucceeded + ); + } + if (CachedCoachPanelState.bCanStartCoachFollowUpRun && CachedCoachPanelState.bHasFollowUpGuidance) + { + const bool bSucceeded = StartRunAndRefresh(StartCoachFollowUpRun(DefaultCoachMaxCases)); + return RecordAndFinalizeOutcome( + TEXT("launch-follow-up-fallback"), + FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery, + bSucceeded + ); + } + if (CachedCoachPanelState.bCanStartCoachRecommendedRun) + { + const bool bSucceeded = StartRunAndRefresh(StartCoachRecommendedRun(DefaultCoachMaxCases)); + return RecordAndFinalizeOutcome( + TEXT("launch-recommended-fallback"), + FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery, + bSucceeded + ); + } + + const FHyperTwistTrainingRunState FallbackRunState = ExecutePrimaryCoachAction(); + const bool bSucceeded = FallbackRunState.Session.IsStructurallyValid(); + return RecordAndFinalizeOutcome( + TEXT("launch-primary-fallback"), + FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery, + bSucceeded + ); + }; + + if (bPreferLaunchBeforePromote) + { + const bool bLaunchSucceeded = TryLaunchRecovery(); + if (bLaunchSucceeded) + { + return true; + } + if (!DeferredEntryId.IsEmpty()) + { + return TryPromoteRecovery(); + } + return false; + } + + if (!DeferredEntryId.IsEmpty()) + { + return TryPromoteRecovery(); + } + + return TryLaunchRecovery(); } bool UHyperTwistCoachDashboardWidget::SelectPreviousQueueEntry() @@ -3119,6 +3127,151 @@ FHyperTwistCoachDashboardGuidanceLanePreference UHyperTwistCoachDashboardWidget: return Preference; } +FHyperTwistCoachDashboardQueueRecoveryWeighting UHyperTwistCoachDashboardWidget::BuildQueueRecoveryWeighting() const +{ + FHyperTwistCoachDashboardQueueRecoveryWeighting Weighting; + float PromoteDeltaAccumulator = 0.0f; + float LaunchDeltaAccumulator = 0.0f; + + auto ConsumeOutcome = [ + &Weighting, + &PromoteDeltaAccumulator, + &LaunchDeltaAccumulator + ](const FHyperTwistCoachDashboardQueueRecoveryOutcome& Outcome) + { + if (!Outcome.IsStructurallyValid()) + { + return; + } + + const float PressureDelta = Outcome.BeforePressureScore - Outcome.AfterPressureScore; + switch (Outcome.ActionKind) + { + case FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::PromoteDeferred: + ++Weighting.PromoteCount; + PromoteDeltaAccumulator += PressureDelta; + if (Outcome.bReducedFriction) + { + ++Weighting.PromoteReducedCount; + } + if (!Outcome.bSucceeded) + { + ++Weighting.PromoteFailureCount; + } + break; + case FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery: + ++Weighting.LaunchCount; + LaunchDeltaAccumulator += PressureDelta; + if (Outcome.bReducedFriction) + { + ++Weighting.LaunchReducedCount; + } + if (!Outcome.bSucceeded) + { + ++Weighting.LaunchFailureCount; + } + break; + default: + break; + } + }; + + for (const FHyperTwistCoachDashboardQueueRecoveryOutcome& Outcome : QueueRecoveryOutcomeHistory) + { + ConsumeOutcome(Outcome); + } + + if (QueueRecoveryOutcome.IsStructurallyValid() + && !QueueRecoveryOutcomeHistory.ContainsByPredicate( + [this](const FHyperTwistCoachDashboardQueueRecoveryOutcome& Candidate) + { + return Candidate.RecordedAtUtc == QueueRecoveryOutcome.RecordedAtUtc + && Candidate.AppliedActionLabel == QueueRecoveryOutcome.AppliedActionLabel + && Candidate.ActionKind == QueueRecoveryOutcome.ActionKind; + })) + { + ConsumeOutcome(QueueRecoveryOutcome); + } + + if (Weighting.PromoteCount > 0) + { + Weighting.PromoteAveragePressureDelta = + PromoteDeltaAccumulator / static_cast(Weighting.PromoteCount); + } + if (Weighting.LaunchCount > 0) + { + Weighting.LaunchAveragePressureDelta = + LaunchDeltaAccumulator / static_cast(Weighting.LaunchCount); + } + + Weighting.bHasSignal = Weighting.PromoteCount > 0 || Weighting.LaunchCount > 0; + if (!Weighting.bHasSignal) + { + return Weighting; + } + + auto ScoreAction = [](const int32 Count, const int32 ReducedCount, const int32 FailureCount, const float AverageDelta) + { + if (Count <= 0) + { + return -1000000.0f; + } + + const int32 NeutralCount = FMath::Max(Count - ReducedCount - FailureCount, 0); + return static_cast(ReducedCount) * 3.0f + + static_cast(NeutralCount) * 0.5f + + AverageDelta + - static_cast(FailureCount) * 2.0f; + }; + + const float PromoteScore = ScoreAction( + Weighting.PromoteCount, + Weighting.PromoteReducedCount, + Weighting.PromoteFailureCount, + Weighting.PromoteAveragePressureDelta + ); + const float LaunchScore = ScoreAction( + Weighting.LaunchCount, + Weighting.LaunchReducedCount, + Weighting.LaunchFailureCount, + Weighting.LaunchAveragePressureDelta + ); + + if (PromoteScore > LaunchScore) + { + Weighting.PreferredActionKind = + FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::PromoteDeferred; + } + else if (LaunchScore > PromoteScore) + { + Weighting.PreferredActionKind = + FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery; + } + else + { + for (int32 Index = QueueRecoveryOutcomeHistory.Num() - 1; Index >= 0; --Index) + { + const FHyperTwistCoachDashboardQueueRecoveryOutcome& Outcome = QueueRecoveryOutcomeHistory[Index]; + if (Outcome.IsStructurallyValid() + && Outcome.ActionKind != FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::None) + { + Weighting.PreferredActionKind = Outcome.ActionKind; + break; + } + } + if (Weighting.PreferredActionKind + == FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::None) + { + Weighting.PreferredActionKind = + Weighting.PromoteCount >= Weighting.LaunchCount + ? FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::PromoteDeferred + : FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery; + } + } + + return Weighting; +} + FString UHyperTwistCoachDashboardWidget::BuildGuidanceOutcomeActionLabel( const EHyperTwistCoachDashboardOutcomeAction Action) const { @@ -3232,6 +3385,7 @@ void UHyperTwistCoachDashboardWidget::RecordGuidanceOutcomeDecision( void UHyperTwistCoachDashboardWidget::RecordQueueRecoveryOutcome( const FString& SourceLabel, const FString& AppliedActionLabel, + const FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind ActionKind, const float BeforePressureScore, const int32 BeforeDeferredEventCount, const bool bBeforeBlockedFollowUp, @@ -3242,6 +3396,7 @@ void UHyperTwistCoachDashboardWidget::RecordQueueRecoveryOutcome( QueueRecoveryOutcome.RecordedAtUtc = FDateTime::UtcNow().ToIso8601(); QueueRecoveryOutcome.SourceLabel = SourceLabel; QueueRecoveryOutcome.AppliedActionLabel = AppliedActionLabel; + QueueRecoveryOutcome.ActionKind = ActionKind; QueueRecoveryOutcome.BeforePressureScore = BeforePressureScore; QueueRecoveryOutcome.AfterPressureScore = CachedCoachSchedulePolicySummary.ReschedulePressureScore; QueueRecoveryOutcome.BeforeDeferredEventCount = BeforeDeferredEventCount; @@ -3269,6 +3424,16 @@ void UHyperTwistCoachDashboardWidget::RecordQueueRecoveryOutcome( || QueueRecoveryOutcome.bAfterCompletionGap != QueueRecoveryOutcome.bBeforeCompletionGap ); bHasQueueRecoveryOutcome = QueueRecoveryOutcome.IsStructurallyValid(); + if (bHasQueueRecoveryOutcome) + { + QueueRecoveryOutcomeHistory.Add(QueueRecoveryOutcome); + constexpr int32 MaxRetainedQueueRecoveryOutcomes = 16; + if (QueueRecoveryOutcomeHistory.Num() > MaxRetainedQueueRecoveryOutcomes) + { + const int32 ExcessCount = QueueRecoveryOutcomeHistory.Num() - MaxRetainedQueueRecoveryOutcomes; + QueueRecoveryOutcomeHistory.RemoveAt(0, ExcessCount, EAllowShrinking::No); + } + } } void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() @@ -3693,6 +3858,24 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() const FString QueueRecoverySourceLabel = CachedCoachPanelState.PreferredGuidanceSourceLabel.IsEmpty() ? TEXT("n/a") : CachedCoachPanelState.PreferredGuidanceSourceLabel; + const FHyperTwistCoachDashboardQueueRecoveryWeighting QueueRecoveryWeighting = BuildQueueRecoveryWeighting(); + const bool bCanLaunchQueueRecovery = + CachedCoachPanelState.bCanStartQueuedRun + || (CachedCoachPanelState.bCanStartCoachFollowUpRun && CachedCoachPanelState.bHasFollowUpGuidance) + || CachedCoachPanelState.bCanStartCoachRecommendedRun; + const bool bPreferLaunchRecoveryAction = + !FindFirstDeferredQueueEntryId().IsEmpty() + && bCanLaunchQueueRecovery + && QueueRecoveryWeighting.PreferredActionKind + == FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery; + const FString QueueRecoveryWeightingLabel = + QueueRecoveryWeighting.PreferredActionKind + == FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::PromoteDeferred + ? TEXT("prefer promote-deferred recovery") + : (QueueRecoveryWeighting.PreferredActionKind + == FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::LaunchRecovery + ? TEXT("prefer launch-recovery") + : TEXT("no learned preference")); const bool bHasQueueRecoveryFriction = CachedCoachSchedulePolicySummary.bHasBlockedFollowUpDeferrals || CachedCoachSchedulePolicySummary.bHasArchiveAwareDeferrals @@ -3701,20 +3884,27 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() || CachedCoachQueueExecutionSummary.bHasCompletionGap; const FString QueueRecoveryStatusLine = bQueueRecoveryMemorySource ? FString::Printf( - TEXT("Recovery: %s lane | Source: %s | Action: %s | Mode: %s | Selection: %s"), + TEXT("Recovery: %s lane | Source: %s | Action: %s | Mode: %s | Selection: %s | Learned: %s"), *QueueRecoveryLaneLabel, *QueueRecoverySourceLabel, CachedCoachPanelState.PrimaryActionLabel.IsEmpty() ? TEXT("n/a") : *CachedCoachPanelState.PrimaryActionLabel, *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedCoachPanelState.SuggestedMode), - *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedCoachPanelState.SuggestedSelectionPolicy) + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedCoachPanelState.SuggestedSelectionPolicy), + *QueueRecoveryWeightingLabel ) : (bHasQueueRecoveryFriction - ? TEXT("Recovery: queue friction is present, but no queue-memory recovery override is currently active.") - : TEXT("Recovery: no queue-memory recovery override is currently active.")); + ? FString::Printf( + TEXT("Recovery: queue friction is present, but no queue-memory recovery override is currently active | Learned: %s"), + *QueueRecoveryWeightingLabel + ) + : FString::Printf( + TEXT("Recovery: no queue-memory recovery override is currently active | Learned: %s"), + *QueueRecoveryWeightingLabel + )); const FString QueueRecoveryDetailLine = FString::Printf( - TEXT("Deferred events %d | Promoted %d | Manual %d | Pressure %.2f | Dominant reason: %s | Follow-up deferrals: %s | Archive-aware deferrals: %s | Completion gap: %s"), + TEXT("Deferred events %d | Promoted %d | Manual %d | Pressure %.2f | Dominant reason: %s | Follow-up deferrals: %s | Archive-aware deferrals: %s | Completion gap: %s || Learned promote %d/%d reduced, %d failed, avg delta %.2f | launch %d/%d reduced, %d failed, avg delta %.2f"), CachedCoachQueueExecutionSummary.DeferredEventCount, CachedCoachQueueExecutionSummary.PromotedEventCount, CachedCoachQueueExecutionSummary.ManualRescheduleEventCount, @@ -3724,7 +3914,15 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() : *CachedCoachSchedulePolicySummary.DominantDeferralReasonLabel, CachedCoachSchedulePolicySummary.bHasBlockedFollowUpDeferrals ? TEXT("yes") : TEXT("no"), CachedCoachSchedulePolicySummary.bHasArchiveAwareDeferrals ? TEXT("yes") : TEXT("no"), - CachedCoachQueueExecutionSummary.bHasCompletionGap ? TEXT("yes") : TEXT("no") + CachedCoachQueueExecutionSummary.bHasCompletionGap ? TEXT("yes") : TEXT("no"), + QueueRecoveryWeighting.PromoteReducedCount, + QueueRecoveryWeighting.PromoteCount, + QueueRecoveryWeighting.PromoteFailureCount, + QueueRecoveryWeighting.PromoteAveragePressureDelta, + QueueRecoveryWeighting.LaunchReducedCount, + QueueRecoveryWeighting.LaunchCount, + QueueRecoveryWeighting.LaunchFailureCount, + QueueRecoveryWeighting.LaunchAveragePressureDelta ); const FString QueueRecoveryOutcomeStatusLine = !bHasQueueRecoveryOutcome || !QueueRecoveryOutcome.IsStructurallyValid() ? TEXT("Outcome: no queue-recovery action has been applied yet.") @@ -3781,7 +3979,8 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() || (!bQueueRecoveryMemorySource && (CachedCoachPanelState.bCanStartCoachFollowUpRun || CachedCoachPanelState.bCanStartCoachRecommendedRun))); - const FString QueueRecoveryActionLabel = !PreferredDeferredRecoveryEntryId.IsEmpty() + const FString QueueRecoveryActionLabel = + !PreferredDeferredRecoveryEntryId.IsEmpty() && !bPreferLaunchRecoveryAction ? (bQueueRecoveryFollowUp ? TEXT("Promote Follow-Up Recovery") : (bQueueRecoveryRecommended diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 6c50fea..e727da5 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -104,9 +104,17 @@ struct FHyperTwistCoachDashboardGuidanceLanePreference struct FHyperTwistCoachDashboardQueueRecoveryOutcome { + enum class EActionKind : uint8 + { + None, + PromoteDeferred, + LaunchRecovery + }; + FString RecordedAtUtc; FString SourceLabel; FString AppliedActionLabel; + EActionKind ActionKind = EActionKind::None; float BeforePressureScore = 0.0f; float AfterPressureScore = 0.0f; int32 BeforeDeferredEventCount = 0; @@ -127,6 +135,27 @@ struct FHyperTwistCoachDashboardQueueRecoveryOutcome } }; +struct FHyperTwistCoachDashboardQueueRecoveryWeighting +{ + int32 PromoteCount = 0; + int32 PromoteReducedCount = 0; + int32 PromoteFailureCount = 0; + int32 LaunchCount = 0; + int32 LaunchReducedCount = 0; + int32 LaunchFailureCount = 0; + float PromoteAveragePressureDelta = 0.0f; + float LaunchAveragePressureDelta = 0.0f; + FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind PreferredActionKind = + FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::None; + bool bHasSignal = false; + + bool IsStructurallyValid() const + { + return bHasSignal + && PreferredActionKind != FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind::None; + } +}; + UCLASS(BlueprintType, Blueprintable) class UNREALHYPERTWIST_API UHyperTwistCoachDashboardWidget : public UHyperTwistTrainingPanelWidget { @@ -846,11 +875,13 @@ private: EHyperTwistCoachDashboardOutcomeAction BaseAction ) const; FHyperTwistCoachDashboardGuidanceLanePreference BuildGuidanceLanePreference() const; + FHyperTwistCoachDashboardQueueRecoveryWeighting BuildQueueRecoveryWeighting() const; FString BuildGuidanceOutcomeActionLabel(EHyperTwistCoachDashboardOutcomeAction Action) const; FHyperTwistTrainingDeck BuildGuidanceOutcomeSelectedCaseDeck() const; void RecordQueueRecoveryOutcome( const FString& SourceLabel, const FString& AppliedActionLabel, + FHyperTwistCoachDashboardQueueRecoveryOutcome::EActionKind ActionKind, float BeforePressureScore, int32 BeforeDeferredEventCount, bool bBeforeBlockedFollowUp, @@ -906,6 +937,7 @@ private: TArray GuidanceDecisionHistoryEntries; bool bHasQueueRecoveryOutcome = false; FHyperTwistCoachDashboardQueueRecoveryOutcome QueueRecoveryOutcome; + TArray QueueRecoveryOutcomeHistory; EHyperTwistCoachDashboardGuidancePreviewLane ActiveGuidancePreviewLane = EHyperTwistCoachDashboardGuidancePreviewLane::None; int32 SelectedGuidancePreviewCaseIndex = INDEX_NONE;