Show queue recovery outcomes on dashboard

This commit is contained in:
axiomlogicnexus 2026-04-27 18:41:10 +02:00
parent 1e104702f3
commit a4b69211bc
2 changed files with 270 additions and 8 deletions

View file

@ -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));

View file

@ -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<UTextBlock> QueueRecoveryDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> QueueRecoveryOutcomeHeaderTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> QueueRecoveryOutcomeStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> QueueRecoveryOutcomeDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> 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<FHyperTwistCoachDashboardDecisionHistoryEntry> GuidanceDecisionHistoryEntries;
bool bHasQueueRecoveryOutcome = false;
FHyperTwistCoachDashboardQueueRecoveryOutcome QueueRecoveryOutcome;
EHyperTwistCoachDashboardGuidancePreviewLane ActiveGuidancePreviewLane =
EHyperTwistCoachDashboardGuidancePreviewLane::None;
int32 SelectedGuidancePreviewCaseIndex = INDEX_NONE;