From a4b69211bc8408c5051dc65c54ea1ebe1bb534a6 Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Mon, 27 Apr 2026 18:41:10 +0200 Subject: [PATCH] Show queue recovery outcomes on dashboard --- .../HyperTwistCoachDashboardWidget.cpp | 232 +++++++++++++++++- .../HyperTwistCoachDashboardWidget.h | 46 ++++ 2 files changed, 270 insertions(+), 8 deletions(-) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index d2b692f..f1594ca 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -259,10 +259,29 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() return false; } + const float BeforePressureScore = CachedCoachSchedulePolicySummary.ReschedulePressureScore; + const int32 BeforeDeferredEventCount = CachedCoachQueueExecutionSummary.DeferredEventCount; + const bool bBeforeBlockedFollowUp = CachedCoachSchedulePolicySummary.bHasBlockedFollowUpDeferrals; + const bool bBeforeOutstandingDeferred = CachedCoachQueueExecutionSummary.bHasOutstandingDeferredWork; + const bool bBeforeCompletionGap = CachedCoachQueueExecutionSummary.bHasCompletionGap; + const FString SourceLabel = CachedCoachPanelState.PreferredGuidanceSourceLabel; + const FString DeferredEntryId = FindFirstDeferredQueueEntryId(); if (!DeferredEntryId.IsEmpty()) { - return PromoteFirstDeferredQueueEntry(); + const bool bSucceeded = PromoteFirstDeferredQueueEntry(); + RecordQueueRecoveryOutcome( + SourceLabel, + TEXT("promote-deferred-recovery"), + BeforePressureScore, + BeforeDeferredEventCount, + bBeforeBlockedFollowUp, + bBeforeOutstandingDeferred, + bBeforeCompletionGap, + bSucceeded + ); + UpdateDashboardPresentation(); + return bSucceeded; } const bool bQueueRecoveryMemorySource = @@ -291,7 +310,19 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() && CachedCoachPanelState.NextQueueSourceLabel == TEXT("primary-brief")); if (bQueueMatchesPreference) { - return StartRunAndRefresh(StartNextQueuedCoachRun(DefaultCoachMaxCases)); + const bool bSucceeded = StartRunAndRefresh(StartNextQueuedCoachRun(DefaultCoachMaxCases)); + RecordQueueRecoveryOutcome( + SourceLabel, + TEXT("launch-queued-recovery"), + BeforePressureScore, + BeforeDeferredEventCount, + bBeforeBlockedFollowUp, + bBeforeOutstandingDeferred, + bBeforeCompletionGap, + bSucceeded + ); + UpdateDashboardPresentation(); + return bSucceeded; } } @@ -299,27 +330,99 @@ bool UHyperTwistCoachDashboardWidget::ApplyQueueRecoveryPosture() && CachedCoachPanelState.bCanStartCoachFollowUpRun && CachedCoachPanelState.bHasFollowUpGuidance) { - return StartRunAndRefresh(StartCoachFollowUpRun(DefaultCoachMaxCases)); + 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) { - return StartRunAndRefresh(StartCoachRecommendedRun(DefaultCoachMaxCases)); + const bool bSucceeded = StartRunAndRefresh(StartCoachRecommendedRun(DefaultCoachMaxCases)); + RecordQueueRecoveryOutcome( + SourceLabel, + TEXT("launch-recommended-recovery"), + BeforePressureScore, + BeforeDeferredEventCount, + bBeforeBlockedFollowUp, + bBeforeOutstandingDeferred, + bBeforeCompletionGap, + bSucceeded + ); + UpdateDashboardPresentation(); + return bSucceeded; } if (CachedCoachPanelState.bCanStartQueuedRun) { - return StartRunAndRefresh(StartNextQueuedCoachRun(DefaultCoachMaxCases)); + 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) { - return StartRunAndRefresh(StartCoachFollowUpRun(DefaultCoachMaxCases)); + 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) { - return StartRunAndRefresh(StartCoachRecommendedRun(DefaultCoachMaxCases)); + 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(); - return FallbackRunState.Session.IsStructurallyValid(); + const bool bSucceeded = FallbackRunState.Session.IsStructurallyValid(); + RecordQueueRecoveryOutcome( + SourceLabel, + TEXT("launch-primary-fallback"), + BeforePressureScore, + BeforeDeferredEventCount, + bBeforeBlockedFollowUp, + bBeforeOutstandingDeferred, + bBeforeCompletionGap, + bSucceeded + ); + UpdateDashboardPresentation(); + return bSucceeded; } bool UHyperTwistCoachDashboardWidget::SelectPreviousQueueEntry() @@ -1866,6 +1969,24 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() WidgetTree, RootLayout, TEXT("CoachQueueRecoveryDetail"), + 2 + ); + QueueRecoveryOutcomeHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachQueueRecoveryOutcomeHeader"), + 2 + ); + QueueRecoveryOutcomeStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachQueueRecoveryOutcomeStatus"), + 2 + ); + QueueRecoveryOutcomeDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachQueueRecoveryOutcomeDetail"), 8 ); VerificationStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( @@ -3108,6 +3229,48 @@ void UHyperTwistCoachDashboardWidget::RecordGuidanceOutcomeDecision( } } +void UHyperTwistCoachDashboardWidget::RecordQueueRecoveryOutcome( + const FString& SourceLabel, + const FString& AppliedActionLabel, + const float BeforePressureScore, + const int32 BeforeDeferredEventCount, + const bool bBeforeBlockedFollowUp, + const bool bBeforeOutstandingDeferred, + const bool bBeforeCompletionGap, + const bool bSucceeded) +{ + QueueRecoveryOutcome.RecordedAtUtc = FDateTime::UtcNow().ToIso8601(); + QueueRecoveryOutcome.SourceLabel = SourceLabel; + QueueRecoveryOutcome.AppliedActionLabel = AppliedActionLabel; + QueueRecoveryOutcome.BeforePressureScore = BeforePressureScore; + QueueRecoveryOutcome.AfterPressureScore = CachedCoachSchedulePolicySummary.ReschedulePressureScore; + QueueRecoveryOutcome.BeforeDeferredEventCount = BeforeDeferredEventCount; + QueueRecoveryOutcome.AfterDeferredEventCount = CachedCoachQueueExecutionSummary.DeferredEventCount; + QueueRecoveryOutcome.bBeforeBlockedFollowUp = bBeforeBlockedFollowUp; + QueueRecoveryOutcome.bAfterBlockedFollowUp = CachedCoachSchedulePolicySummary.bHasBlockedFollowUpDeferrals; + QueueRecoveryOutcome.bBeforeOutstandingDeferred = bBeforeOutstandingDeferred; + QueueRecoveryOutcome.bAfterOutstandingDeferred = CachedCoachQueueExecutionSummary.bHasOutstandingDeferredWork; + QueueRecoveryOutcome.bBeforeCompletionGap = bBeforeCompletionGap; + QueueRecoveryOutcome.bAfterCompletionGap = CachedCoachQueueExecutionSummary.bHasCompletionGap; + QueueRecoveryOutcome.bSucceeded = bSucceeded; + QueueRecoveryOutcome.bReducedFriction = bSucceeded + && (QueueRecoveryOutcome.AfterPressureScore < QueueRecoveryOutcome.BeforePressureScore + || QueueRecoveryOutcome.AfterDeferredEventCount < QueueRecoveryOutcome.BeforeDeferredEventCount + || (!QueueRecoveryOutcome.bAfterBlockedFollowUp && QueueRecoveryOutcome.bBeforeBlockedFollowUp) + || (!QueueRecoveryOutcome.bAfterOutstandingDeferred && QueueRecoveryOutcome.bBeforeOutstandingDeferred) + || (!QueueRecoveryOutcome.bAfterCompletionGap && QueueRecoveryOutcome.bBeforeCompletionGap)); + QueueRecoveryOutcome.bShiftedFriction = bSucceeded + && !QueueRecoveryOutcome.bReducedFriction + && ( + !FMath::IsNearlyEqual(QueueRecoveryOutcome.AfterPressureScore, QueueRecoveryOutcome.BeforePressureScore) + || QueueRecoveryOutcome.AfterDeferredEventCount != QueueRecoveryOutcome.BeforeDeferredEventCount + || QueueRecoveryOutcome.bAfterBlockedFollowUp != QueueRecoveryOutcome.bBeforeBlockedFollowUp + || QueueRecoveryOutcome.bAfterOutstandingDeferred != QueueRecoveryOutcome.bBeforeOutstandingDeferred + || QueueRecoveryOutcome.bAfterCompletionGap != QueueRecoveryOutcome.bBeforeCompletionGap + ); + bHasQueueRecoveryOutcome = QueueRecoveryOutcome.IsStructurallyValid(); +} + void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid() @@ -3563,6 +3726,47 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() CachedCoachSchedulePolicySummary.bHasArchiveAwareDeferrals ? TEXT("yes") : TEXT("no"), CachedCoachQueueExecutionSummary.bHasCompletionGap ? TEXT("yes") : TEXT("no") ); + const FString QueueRecoveryOutcomeStatusLine = !bHasQueueRecoveryOutcome || !QueueRecoveryOutcome.IsStructurallyValid() + ? TEXT("Outcome: no queue-recovery action has been applied yet.") + : (!QueueRecoveryOutcome.bSucceeded + ? FString::Printf( + TEXT("Outcome: %s failed | Source: %s"), + *QueueRecoveryOutcome.AppliedActionLabel, + QueueRecoveryOutcome.SourceLabel.IsEmpty() ? TEXT("n/a") : *QueueRecoveryOutcome.SourceLabel + ) + : (QueueRecoveryOutcome.bReducedFriction + ? FString::Printf( + TEXT("Outcome: %s reduced queue friction | Source: %s"), + *QueueRecoveryOutcome.AppliedActionLabel, + QueueRecoveryOutcome.SourceLabel.IsEmpty() ? TEXT("n/a") : *QueueRecoveryOutcome.SourceLabel + ) + : (QueueRecoveryOutcome.bShiftedFriction + ? FString::Printf( + TEXT("Outcome: %s shifted queue friction without a full reduction | Source: %s"), + *QueueRecoveryOutcome.AppliedActionLabel, + QueueRecoveryOutcome.SourceLabel.IsEmpty() ? TEXT("n/a") : *QueueRecoveryOutcome.SourceLabel + ) + : FString::Printf( + TEXT("Outcome: %s applied with no immediate friction change | Source: %s"), + *QueueRecoveryOutcome.AppliedActionLabel, + QueueRecoveryOutcome.SourceLabel.IsEmpty() ? TEXT("n/a") : *QueueRecoveryOutcome.SourceLabel + )))); + const FString QueueRecoveryOutcomeDetailLine = !bHasQueueRecoveryOutcome || !QueueRecoveryOutcome.IsStructurallyValid() + ? TEXT("Outcome detail: apply a queue-recovery action to compare before/after pressure, deferred count, blocked follow-up state, and completion-gap state.") + : FString::Printf( + TEXT("Recorded: %s | Pressure %.2f -> %.2f | Deferred %d -> %d | Blocked follow-up %s -> %s | Outstanding deferred %s -> %s | Completion gap %s -> %s"), + *QueueRecoveryOutcome.RecordedAtUtc, + QueueRecoveryOutcome.BeforePressureScore, + QueueRecoveryOutcome.AfterPressureScore, + QueueRecoveryOutcome.BeforeDeferredEventCount, + QueueRecoveryOutcome.AfterDeferredEventCount, + QueueRecoveryOutcome.bBeforeBlockedFollowUp ? TEXT("yes") : TEXT("no"), + QueueRecoveryOutcome.bAfterBlockedFollowUp ? TEXT("yes") : TEXT("no"), + QueueRecoveryOutcome.bBeforeOutstandingDeferred ? TEXT("yes") : TEXT("no"), + QueueRecoveryOutcome.bAfterOutstandingDeferred ? TEXT("yes") : TEXT("no"), + QueueRecoveryOutcome.bBeforeCompletionGap ? TEXT("yes") : TEXT("no"), + QueueRecoveryOutcome.bAfterCompletionGap ? TEXT("yes") : TEXT("no") + ); const FString PreferredDeferredRecoveryEntryId = FindFirstDeferredQueueEntryId(); const bool bCanApplyQueueRecovery = !bHasLiveAttemptTimer @@ -4625,6 +4829,18 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { QueueRecoveryDetailTextBlock->SetText(FText::FromString(QueueRecoveryDetailLine)); } + if (QueueRecoveryOutcomeHeaderTextBlock != nullptr) + { + QueueRecoveryOutcomeHeaderTextBlock->SetText(FText::FromString(TEXT("[Queue Recovery Outcome]"))); + } + if (QueueRecoveryOutcomeStatusTextBlock != nullptr) + { + QueueRecoveryOutcomeStatusTextBlock->SetText(FText::FromString(QueueRecoveryOutcomeStatusLine)); + } + if (QueueRecoveryOutcomeDetailTextBlock != nullptr) + { + QueueRecoveryOutcomeDetailTextBlock->SetText(FText::FromString(QueueRecoveryOutcomeDetailLine)); + } if (VerificationStatusTextBlock != nullptr) { VerificationStatusTextBlock->SetText(FText::FromString(CachedCoachPanelState.VerificationStatusLine)); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index add054f..6c50fea 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -102,6 +102,31 @@ struct FHyperTwistCoachDashboardGuidanceLanePreference } }; +struct FHyperTwistCoachDashboardQueueRecoveryOutcome +{ + FString RecordedAtUtc; + FString SourceLabel; + FString AppliedActionLabel; + float BeforePressureScore = 0.0f; + float AfterPressureScore = 0.0f; + int32 BeforeDeferredEventCount = 0; + int32 AfterDeferredEventCount = 0; + bool bBeforeBlockedFollowUp = false; + bool bAfterBlockedFollowUp = false; + bool bBeforeOutstandingDeferred = false; + bool bAfterOutstandingDeferred = false; + bool bBeforeCompletionGap = false; + bool bAfterCompletionGap = false; + bool bSucceeded = false; + bool bReducedFriction = false; + bool bShiftedFriction = false; + + bool IsStructurallyValid() const + { + return !RecordedAtUtc.IsEmpty() && !AppliedActionLabel.IsEmpty(); + } +}; + UCLASS(BlueprintType, Blueprintable) class UNREALHYPERTWIST_API UHyperTwistCoachDashboardWidget : public UHyperTwistTrainingPanelWidget { @@ -507,6 +532,15 @@ protected: UPROPERTY(Transient) TObjectPtr QueueRecoveryDetailTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr QueueRecoveryOutcomeHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr QueueRecoveryOutcomeStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr QueueRecoveryOutcomeDetailTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr VerificationStatusTextBlock = nullptr; @@ -814,6 +848,16 @@ private: FHyperTwistCoachDashboardGuidanceLanePreference BuildGuidanceLanePreference() const; FString BuildGuidanceOutcomeActionLabel(EHyperTwistCoachDashboardOutcomeAction Action) const; FHyperTwistTrainingDeck BuildGuidanceOutcomeSelectedCaseDeck() const; + void RecordQueueRecoveryOutcome( + const FString& SourceLabel, + const FString& AppliedActionLabel, + float BeforePressureScore, + int32 BeforeDeferredEventCount, + bool bBeforeBlockedFollowUp, + bool bBeforeOutstandingDeferred, + bool bBeforeCompletionGap, + bool bSucceeded + ); void RecordGuidanceOutcomeDecision( EHyperTwistCoachDashboardOutcomeAction RecommendedAction, EHyperTwistCoachDashboardOutcomeAction AppliedAction, @@ -860,6 +904,8 @@ private: FHyperTwistTrainingDeck RetainedGuidanceComparisonDeck; FString RetainedGuidanceComparisonSelectedCaseId; TArray GuidanceDecisionHistoryEntries; + bool bHasQueueRecoveryOutcome = false; + FHyperTwistCoachDashboardQueueRecoveryOutcome QueueRecoveryOutcome; EHyperTwistCoachDashboardGuidancePreviewLane ActiveGuidancePreviewLane = EHyperTwistCoachDashboardGuidancePreviewLane::None; int32 SelectedGuidancePreviewCaseIndex = INDEX_NONE;