Weight queue recovery by dashboard outcomes

This commit is contained in:
axiomlogicnexus 2026-04-27 18:50:15 +02:00
parent a4b69211bc
commit f494f0707d
2 changed files with 375 additions and 144 deletions

View file

@ -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<float>(Weighting.PromoteCount);
}
if (Weighting.LaunchCount > 0)
{
Weighting.LaunchAveragePressureDelta =
LaunchDeltaAccumulator / static_cast<float>(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<float>(ReducedCount) * 3.0f
+ static_cast<float>(NeutralCount) * 0.5f
+ AverageDelta
- static_cast<float>(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

View file

@ -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<FHyperTwistCoachDashboardDecisionHistoryEntry> GuidanceDecisionHistoryEntries;
bool bHasQueueRecoveryOutcome = false;
FHyperTwistCoachDashboardQueueRecoveryOutcome QueueRecoveryOutcome;
TArray<FHyperTwistCoachDashboardQueueRecoveryOutcome> QueueRecoveryOutcomeHistory;
EHyperTwistCoachDashboardGuidancePreviewLane ActiveGuidancePreviewLane =
EHyperTwistCoachDashboardGuidancePreviewLane::None;
int32 SelectedGuidancePreviewCaseIndex = INDEX_NONE;