diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index 4cbb828..70bb7d5 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -10725,6 +10725,278 @@ UHyperTwistCoachDashboardWidget::GetDisplayedRunActionLabelsInspectSurface() con return Surface; } +FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface +UHyperTwistCoachDashboardWidget::GetDisplayedActiveRunStatusInspectSurface() const +{ + FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface Surface; + Surface.Headline = TEXT("Active run status"); + + const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid() + && CachedRunState.ActiveDeck.IsStructurallyValid(); + const FString ActiveDeckLabel = CachedRunState.ActiveDeck.Title.IsEmpty() + ? (CachedRunState.ActiveDeck.DeckId.IsEmpty() ? TEXT("n/a") : CachedRunState.ActiveDeck.DeckId) + : CachedRunState.ActiveDeck.Title; + const FString ActiveCaseId = CachedRunState.CurrentSelection.TrainingCase.CaseId.IsEmpty() + ? TEXT("n/a") + : CachedRunState.CurrentSelection.TrainingCase.CaseId; + + Surface.TrainingSessionId = CachedRunState.Session.TrainingSessionId.IsEmpty() + ? TEXT("n/a") + : CachedRunState.Session.TrainingSessionId; + Surface.SessionStateLabel = + HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.SessionState); + Surface.DeliveryModeLabel = + HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.Mode); + Surface.ActiveDeckLabel = ActiveDeckLabel; + Surface.ActiveCaseId = ActiveCaseId; + Surface.LastCaseId = CachedRunSummary.LastCaseId.IsEmpty() + ? TEXT("n/a") + : CachedRunSummary.LastCaseId; + Surface.RemainingCaseCount = CachedRunState.RemainingCaseIds.Num(); + Surface.AttemptCount = CachedRunSummary.AttemptCount; + Surface.CompletedAttemptCount = CachedRunSummary.CompletedAttemptCount; + Surface.SuccessCount = CachedRunSummary.SuccessCount; + Surface.FailureCount = CachedRunSummary.FailureCount; + Surface.TimeoutCount = CachedRunSummary.TimeoutCount; + Surface.DNFCount = CachedRunSummary.DNFCount; + Surface.BestTotalTimeMs = CachedRunSummary.BestTotalTimeMs; + Surface.AverageTotalTimeMs = CachedRunSummary.AverageTotalTimeMs; + Surface.bHasActiveRun = bHasActiveRun; + + const bool bQueueRecoveryMemorySource = + HyperTwistCoachDashboardWidgetInternal::IsQueueRecoveryMemorySource( + CachedCoachPanelState.PreferredGuidanceSourceLabel); + Surface.bClosedLoopIdleDashboard = + IsCoachLoopActionabilityClosed() + && ( + bQueueRecoveryMemorySource + || !CachedCoachPanelState.PreferredGuidanceSourceLabel.IsEmpty() + || DisplayedCoachHandoffRecommendationKind != EHyperTwistTrainingCoachHandoffKind::None); + + Surface.StatusLine = !DisplayedActiveRunStatusLine.IsEmpty() + ? DisplayedActiveRunStatusLine + : (bHasActiveRun + ? FString::Printf( + TEXT("Session: %s | State: %s | Deck: %s | Mode: %s | Current case: %s | Remaining: %d"), + *Surface.TrainingSessionId, + *Surface.SessionStateLabel, + *Surface.ActiveDeckLabel, + *Surface.DeliveryModeLabel, + *Surface.ActiveCaseId, + Surface.RemainingCaseCount) + : (Surface.bClosedLoopIdleDashboard + ? TEXT("No active run. The coach-to-review loop is closed and idle.") + : TEXT("No active run. Queue and coach guidance are ready for launch from this screen."))); + Surface.SummaryLine = !DisplayedActiveRunSummaryLine.IsEmpty() + ? DisplayedActiveRunSummaryLine + : FString::Printf( + TEXT("Attempts: %d total | %d completed | Success %d | Failure %d | Timeout %d | DNF %d | Best %d ms | Avg %d ms | Last case: %s"), + Surface.AttemptCount, + Surface.CompletedAttemptCount, + Surface.SuccessCount, + Surface.FailureCount, + Surface.TimeoutCount, + Surface.DNFCount, + Surface.BestTotalTimeMs, + Surface.AverageTotalTimeMs, + *Surface.LastCaseId); + return Surface; +} + +FHyperTwistTrainingCoachDashboardActiveAttemptTimerStatusInspectSurface +UHyperTwistCoachDashboardWidget::GetDisplayedActiveAttemptTimerStatusInspectSurface() const +{ + FHyperTwistTrainingCoachDashboardActiveAttemptTimerStatusInspectSurface Surface; + Surface.Headline = TEXT("Active attempt / timer status"); + + int32 ParsedAttemptOverrideMs = 0; + const bool bHasParsedAttemptOverride = TryParseAttemptTimeOverride(ParsedAttemptOverrideMs); + const int32 ResolvedSuccessTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Success); + const int32 ResolvedFailureTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Failure); + const int32 ResolvedTimeoutTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::Timeout); + const int32 ResolvedDnfTimeMs = ResolveAttemptTimeMs(EHyperTwistTrainingAttemptResult::DNF); + const FHyperTwistTrainingTimingPolicy* TimingPolicy = FindActiveTimingPolicy(); + const bool bHasInspectionWindow = TimingPolicy != nullptr + && TimingPolicy->bInspectionEnabled + && TimingPolicy->InspectionDurationMs > 0; + const int32 InspectionLimitMs = bHasInspectionWindow ? FMath::Max(TimingPolicy->InspectionDurationMs, 0) : 0; + const bool bLiveInspectionPhase = IsLiveAttemptInspectionPhase(); + const bool bLiveSolvePhase = bHasLiveAttemptTimer + && LiveAttemptPhase == EHyperTwistCoachDashboardAttemptPhase::Solving; + const bool bRunningSolveTimeHidden = + TimingPolicy != nullptr && TimingPolicy->bHideRunningTime && bLiveSolvePhase; + const FString RunningSolveLabel = + bRunningSolveTimeHidden ? TEXT("hidden") : FString::FromInt(LiveAttemptSolveElapsedMs); + const EHyperTwistTrainingPenalty LiveInspectionPenalty = + (bHasInspectionWindow + && TimingPolicy->bInspectionPenaltiesEnabled + && LiveAttemptInspectionElapsedMs > InspectionLimitMs + && LiveAttemptInspectionElapsedMs <= InspectionLimitMs + 2000) + ? EHyperTwistTrainingPenalty::Plus2 + : ((bHasInspectionWindow + && TimingPolicy->bInspectionPenaltiesEnabled + && LiveAttemptInspectionElapsedMs > InspectionLimitMs + 2000) + ? EHyperTwistTrainingPenalty::DNF + : EHyperTwistTrainingPenalty::None); + const FString InspectionWindowLabel = bHasInspectionWindow + ? FString::Printf(TEXT("inspection target %d ms"), InspectionLimitMs) + : TEXT("no inspection window"); + + Surface.AttemptOverrideMs = ParsedAttemptOverrideMs; + Surface.TargetTimeMs = CachedRunState.CurrentSelection.TrainingCase.TimeTargetMs; + Surface.AverageTotalTimeMs = CachedRunSummary.AverageTotalTimeMs; + Surface.ResolvedSuccessTimeMs = ResolvedSuccessTimeMs; + Surface.ResolvedFailureTimeMs = ResolvedFailureTimeMs; + Surface.ResolvedTimeoutTimeMs = ResolvedTimeoutTimeMs; + Surface.ResolvedDnfTimeMs = ResolvedDnfTimeMs; + Surface.InspectionLimitMs = InspectionLimitMs; + Surface.InspectionElapsedMs = LiveAttemptInspectionElapsedMs; + Surface.SolveElapsedMs = LiveAttemptSolveElapsedMs; + Surface.DisplayedElapsedMs = LiveAttemptDisplayedMs; + Surface.TimerPhaseLabel = bLiveInspectionPhase + ? TEXT("inspection") + : (bLiveSolvePhase ? TEXT("solving") : TEXT("idle")); + Surface.RunningSolveLabel = RunningSolveLabel; + Surface.InspectionWindowLabel = InspectionWindowLabel; + Surface.PendingPenaltyLabel = + HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(LiveInspectionPenalty); + Surface.bHasParsedAttemptOverride = bHasParsedAttemptOverride; + Surface.bHasLiveAttemptTimer = bHasLiveAttemptTimer; + Surface.bLiveInspectionPhase = bLiveInspectionPhase; + Surface.bLiveSolvePhase = bLiveSolvePhase; + Surface.bHasInspectionWindow = bHasInspectionWindow; + Surface.bRunningSolveTimeHidden = bRunningSolveTimeHidden; + + Surface.AttemptInputLine = !DisplayedAttemptInputStatusLine.IsEmpty() + ? DisplayedAttemptInputStatusLine + : FString::Printf( + TEXT("Attempt ms input: %s | Target %d | Avg %d | Success %d | Failure %d | Timeout %d | DNF %d"), + bHasParsedAttemptOverride ? *FString::FromInt(ParsedAttemptOverrideMs) : TEXT("auto"), + Surface.TargetTimeMs, + Surface.AverageTotalTimeMs, + Surface.ResolvedSuccessTimeMs, + Surface.ResolvedFailureTimeMs, + Surface.ResolvedTimeoutTimeMs, + Surface.ResolvedDnfTimeMs); + Surface.TimerStatusLine = !DisplayedTimerStatusLine.IsEmpty() + ? DisplayedTimerStatusLine + : (bLiveInspectionPhase + ? FString::Printf( + TEXT("Phase: inspection | elapsed %d ms | remaining %d ms | pending penalty: %s | click Start Solve when ready"), + Surface.InspectionElapsedMs, + bHasInspectionWindow ? FMath::Max(InspectionLimitMs - LiveAttemptInspectionElapsedMs, 0) : 0, + *Surface.PendingPenaltyLabel) + : (bLiveSolvePhase + ? FString::Printf( + TEXT("Phase: solving | inspection %d ms | solve %s ms | finish with Success / Failure / Timeout / DNF"), + Surface.InspectionElapsedMs, + *Surface.RunningSolveLabel) + : FString::Printf( + TEXT("Timer idle | %s | start a live attempt for less-synthetic run entry"), + *Surface.InspectionWindowLabel))); + return Surface; +} + +FHyperTwistTrainingCoachDashboardCurrentCaseDeckStatusInspectSurface +UHyperTwistCoachDashboardWidget::GetDisplayedCurrentCaseDeckStatusInspectSurface() const +{ + FHyperTwistTrainingCoachDashboardCurrentCaseDeckStatusInspectSurface Surface; + Surface.Headline = TEXT("Current case / deck status"); + + const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid() + && CachedRunState.ActiveDeck.IsStructurallyValid(); + const bool bHasRunnableCurrentCase = + bHasActiveRun && CachedRunState.CurrentSelection.TrainingCase.IsStructurallyValid(); + const FHyperTwistTrainingCase& CurrentTrainingCase = CachedRunState.CurrentSelection.TrainingCase; + const FString ActiveDeckLabel = CachedRunState.ActiveDeck.Title.IsEmpty() + ? (CachedRunState.ActiveDeck.DeckId.IsEmpty() ? TEXT("n/a") : CachedRunState.ActiveDeck.DeckId) + : CachedRunState.ActiveDeck.Title; + const FString ActiveCaseId = CurrentTrainingCase.CaseId.IsEmpty() + ? TEXT("n/a") + : CurrentTrainingCase.CaseId; + const FString TagLine = CurrentTrainingCase.Tags.Num() > 0 + ? FString::Join(CurrentTrainingCase.Tags, TEXT(", ")) + : TEXT("n/a"); + + Surface.ActiveDeckLabel = ActiveDeckLabel; + Surface.ActiveDeckId = CachedRunState.ActiveDeck.DeckId.IsEmpty() + ? TEXT("n/a") + : CachedRunState.ActiveDeck.DeckId; + Surface.DeliveryModeLabel = + HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.Mode); + Surface.ActiveCaseId = ActiveCaseId; + Surface.PromptLabel = CurrentTrainingCase.PromptLabel.IsEmpty() + ? TEXT("n/a") + : CurrentTrainingCase.PromptLabel; + Surface.PromptKindLabel = + HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CurrentTrainingCase.PromptKind); + Surface.SubsetId = CurrentTrainingCase.SubsetId.IsEmpty() + ? TEXT("n/a") + : CurrentTrainingCase.SubsetId; + Surface.ScrambleNotation = CurrentTrainingCase.ScrambleNotation.IsEmpty() + ? TEXT("n/a") + : CurrentTrainingCase.ScrambleNotation; + Surface.CanonicalNotation = CurrentTrainingCase.CanonicalNotation.IsEmpty() + ? TEXT("n/a") + : CurrentTrainingCase.CanonicalNotation; + Surface.TagLine = TagLine; + Surface.TimeTargetMs = CurrentTrainingCase.TimeTargetMs; + Surface.SelectionAttemptCount = CachedRunState.CurrentSelection.AttemptCount; + Surface.SelectionSuccessCount = CachedRunState.CurrentSelection.SuccessCount; + Surface.RemainingCaseCount = CachedRunState.RemainingCaseIds.Num(); + Surface.bHasActiveRun = bHasActiveRun; + Surface.bHasRunnableCurrentCase = bHasRunnableCurrentCase; + + const bool bQueueRecoveryMemorySource = + HyperTwistCoachDashboardWidgetInternal::IsQueueRecoveryMemorySource( + CachedCoachPanelState.PreferredGuidanceSourceLabel); + Surface.bClosedLoopIdleDashboard = + IsCoachLoopActionabilityClosed() + && ( + bQueueRecoveryMemorySource + || !CachedCoachPanelState.PreferredGuidanceSourceLabel.IsEmpty() + || DisplayedCoachHandoffRecommendationKind != EHyperTwistTrainingCoachHandoffKind::None); + + Surface.StatusLine = !DisplayedActiveRunStatusLine.IsEmpty() + ? DisplayedActiveRunStatusLine + : (bHasActiveRun + ? FString::Printf( + TEXT("Session: %s | State: %s | Deck: %s | Mode: %s | Current case: %s | Remaining: %d"), + CachedRunState.Session.TrainingSessionId.IsEmpty() + ? TEXT("n/a") + : *CachedRunState.Session.TrainingSessionId, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.SessionState), + *Surface.ActiveDeckLabel, + *Surface.DeliveryModeLabel, + *Surface.ActiveCaseId, + Surface.RemainingCaseCount) + : (Surface.bClosedLoopIdleDashboard + ? TEXT("No active run. The coach-to-review loop is closed and idle.") + : TEXT("No active run. Queue and coach guidance are ready for launch from this screen."))); + Surface.PromptLine = !DisplayedCurrentCasePromptLine.IsEmpty() + ? DisplayedCurrentCasePromptLine + : (bHasRunnableCurrentCase + ? FString::Printf( + TEXT("Prompt: %s | Kind: %s | Subset: %s | Target: %d ms | Attempts on selection: %d | Successes on selection: %d"), + *Surface.PromptLabel, + *Surface.PromptKindLabel, + *Surface.SubsetId, + Surface.TimeTargetMs, + Surface.SelectionAttemptCount, + Surface.SelectionSuccessCount) + : TEXT("Prompt: no active current case is loaded.")); + Surface.NotationLine = !DisplayedCurrentCaseNotationLine.IsEmpty() + ? DisplayedCurrentCaseNotationLine + : (bHasRunnableCurrentCase + ? FString::Printf( + TEXT("Scramble: %s\nCanonical: %s\nTags: %s"), + *Surface.ScrambleNotation, + *Surface.CanonicalNotation, + *Surface.TagLine) + : TEXT("Scramble / notation: unavailable until a run is active.")); + return Surface; +} + bool UHyperTwistCoachDashboardWidget::SelectPreviousMethodDrillFollowUpHistoryEntry() { if (MethodDrillFollowUpHistoryEntries.Num() <= 0) @@ -22095,37 +22367,39 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { ActiveRunHeaderTextBlock->SetText(FText::FromString(TEXT("[Active Run]"))); } + const FString ActiveRunLine = bHasActiveRun + ? FString::Printf( + TEXT("Session: %s | State: %s | Deck: %s | Mode: %s | Current case: %s | Remaining: %d"), + *CachedRunState.Session.TrainingSessionId, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.SessionState), + *ActiveDeckLabel, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.Mode), + *ActiveCaseId, + CachedRunState.RemainingCaseIds.Num() + ) + : (bHasClosedLoopIdleCoachActionability + ? TEXT("No active run. The coach-to-review loop is closed and idle.") + : TEXT("No active run. Queue and coach guidance are ready for launch from this screen.")); + DisplayedActiveRunStatusLine = ActiveRunLine; if (ActiveRunStatusTextBlock != nullptr) { - const FString ActiveRunLine = bHasActiveRun - ? FString::Printf( - TEXT("Session: %s | State: %s | Deck: %s | Mode: %s | Current case: %s | Remaining: %d"), - *CachedRunState.Session.TrainingSessionId, - *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.SessionState), - *ActiveDeckLabel, - *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CachedRunState.Session.Mode), - *ActiveCaseId, - CachedRunState.RemainingCaseIds.Num() - ) - : (bHasClosedLoopIdleCoachActionability - ? TEXT("No active run. The coach-to-review loop is closed and idle.") - : TEXT("No active run. Queue and coach guidance are ready for launch from this screen.")); - ActiveRunStatusTextBlock->SetText(FText::FromString(ActiveRunLine)); + ActiveRunStatusTextBlock->SetText(FText::FromString(DisplayedActiveRunStatusLine)); } + DisplayedActiveRunSummaryLine = FString::Printf( + TEXT("Attempts: %d total | %d completed | Success %d | Failure %d | Timeout %d | DNF %d | Best %d ms | Avg %d ms | Last case: %s"), + CachedRunSummary.AttemptCount, + CachedRunSummary.CompletedAttemptCount, + CachedRunSummary.SuccessCount, + CachedRunSummary.FailureCount, + CachedRunSummary.TimeoutCount, + CachedRunSummary.DNFCount, + CachedRunSummary.BestTotalTimeMs, + CachedRunSummary.AverageTotalTimeMs, + CachedRunSummary.LastCaseId.IsEmpty() ? TEXT("n/a") : *CachedRunSummary.LastCaseId + ); if (ActiveRunSummaryTextBlock != nullptr) { - ActiveRunSummaryTextBlock->SetText(FText::FromString(FString::Printf( - TEXT("Attempts: %d total | %d completed | Success %d | Failure %d | Timeout %d | DNF %d | Best %d ms | Avg %d ms | Last case: %s"), - CachedRunSummary.AttemptCount, - CachedRunSummary.CompletedAttemptCount, - CachedRunSummary.SuccessCount, - CachedRunSummary.FailureCount, - CachedRunSummary.TimeoutCount, - CachedRunSummary.DNFCount, - CachedRunSummary.BestTotalTimeMs, - CachedRunSummary.AverageTotalTimeMs, - CachedRunSummary.LastCaseId.IsEmpty() ? TEXT("n/a") : *CachedRunSummary.LastCaseId - ))); + ActiveRunSummaryTextBlock->SetText(FText::FromString(DisplayedActiveRunSummaryLine)); } if (RunRecapHeaderTextBlock != nullptr) { @@ -22695,47 +22969,49 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { CurrentCaseHeaderTextBlock->SetText(FText::FromString(TEXT("[Current Case]"))); } + FString CurrentCasePromptLine; + if (bHasRunnableCurrentCase) + { + CurrentCasePromptLine = FString::Printf( + TEXT("Prompt: %s | Kind: %s | Subset: %s | Target: %d ms | Attempts on selection: %d | Successes on selection: %d"), + CurrentTrainingCase.PromptLabel.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.PromptLabel, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CurrentTrainingCase.PromptKind), + CurrentTrainingCase.SubsetId.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.SubsetId, + CurrentTrainingCase.TimeTargetMs, + CachedRunState.CurrentSelection.AttemptCount, + CachedRunState.CurrentSelection.SuccessCount + ); + } + else + { + CurrentCasePromptLine = TEXT("Prompt: no active current case is loaded."); + } + DisplayedCurrentCasePromptLine = CurrentCasePromptLine; if (CurrentCasePromptTextBlock != nullptr) { - if (bHasRunnableCurrentCase) - { - CurrentCasePromptTextBlock->SetText(FText::FromString(FString::Printf( - TEXT("Prompt: %s | Kind: %s | Subset: %s | Target: %d ms | Attempts on selection: %d | Successes on selection: %d"), - CurrentTrainingCase.PromptLabel.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.PromptLabel, - *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(CurrentTrainingCase.PromptKind), - CurrentTrainingCase.SubsetId.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.SubsetId, - CurrentTrainingCase.TimeTargetMs, - CachedRunState.CurrentSelection.AttemptCount, - CachedRunState.CurrentSelection.SuccessCount - ))); - } - else - { - CurrentCasePromptTextBlock->SetText(FText::FromString( - TEXT("Prompt: no active current case is loaded.") - )); - } + CurrentCasePromptTextBlock->SetText(FText::FromString(DisplayedCurrentCasePromptLine)); } + FString CurrentCaseNotationLine; + if (bHasRunnableCurrentCase) + { + const FString TagLine = CurrentTrainingCase.Tags.Num() > 0 + ? FString::Join(CurrentTrainingCase.Tags, TEXT(", ")) + : TEXT("n/a"); + CurrentCaseNotationLine = FString::Printf( + TEXT("Scramble: %s\nCanonical: %s\nTags: %s"), + CurrentTrainingCase.ScrambleNotation.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.ScrambleNotation, + CurrentTrainingCase.CanonicalNotation.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.CanonicalNotation, + *TagLine + ); + } + else + { + CurrentCaseNotationLine = TEXT("Scramble / notation: unavailable until a run is active."); + } + DisplayedCurrentCaseNotationLine = CurrentCaseNotationLine; if (CurrentCaseNotationTextBlock != nullptr) { - if (bHasRunnableCurrentCase) - { - const FString TagLine = CurrentTrainingCase.Tags.Num() > 0 - ? FString::Join(CurrentTrainingCase.Tags, TEXT(", ")) - : TEXT("n/a"); - CurrentCaseNotationTextBlock->SetText(FText::FromString(FString::Printf( - TEXT("Scramble: %s\nCanonical: %s\nTags: %s"), - CurrentTrainingCase.ScrambleNotation.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.ScrambleNotation, - CurrentTrainingCase.CanonicalNotation.IsEmpty() ? TEXT("n/a") : *CurrentTrainingCase.CanonicalNotation, - *TagLine - ))); - } - else - { - CurrentCaseNotationTextBlock->SetText(FText::FromString( - TEXT("Scramble / notation: unavailable until a run is active.") - )); - } + CurrentCaseNotationTextBlock->SetText(FText::FromString(DisplayedCurrentCaseNotationLine)); } if (TransitionHeaderTextBlock != nullptr) { @@ -22749,55 +23025,57 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { AttemptControlHeaderTextBlock->SetText(FText::FromString(TEXT("[Attempt Controls]"))); } + DisplayedAttemptInputStatusLine = FString::Printf( + TEXT("Attempt ms input: %s | Target %d | Avg %d | Success %d | Failure %d | Timeout %d | DNF %d"), + bHasParsedAttemptOverride ? *FString::FromInt(ParsedAttemptOverrideMs) : TEXT("auto"), + CurrentTrainingCase.TimeTargetMs, + CachedRunSummary.AverageTotalTimeMs, + ResolvedSuccessTimeMs, + ResolvedFailureTimeMs, + ResolvedTimeoutTimeMs, + ResolvedDnfTimeMs + ); if (AttemptInputStatusTextBlock != nullptr) { - AttemptInputStatusTextBlock->SetText(FText::FromString(FString::Printf( - TEXT("Attempt ms input: %s | Target %d | Avg %d | Success %d | Failure %d | Timeout %d | DNF %d"), - bHasParsedAttemptOverride ? *FString::FromInt(ParsedAttemptOverrideMs) : TEXT("auto"), - CurrentTrainingCase.TimeTargetMs, - CachedRunSummary.AverageTotalTimeMs, - ResolvedSuccessTimeMs, - ResolvedFailureTimeMs, - ResolvedTimeoutTimeMs, - ResolvedDnfTimeMs - ))); + AttemptInputStatusTextBlock->SetText(FText::FromString(DisplayedAttemptInputStatusLine)); } if (TimerHeaderTextBlock != nullptr) { TimerHeaderTextBlock->SetText(FText::FromString(TEXT("[Live Timer]"))); } + if (bLiveInspectionPhase) + { + const int32 RemainingInspectionMs = bHasInspectionWindow + ? FMath::Max(InspectionLimitMs - LiveAttemptInspectionElapsedMs, 0) + : 0; + DisplayedTimerStatusLine = FString::Printf( + TEXT("Phase: inspection | elapsed %d ms | remaining %d ms | pending penalty: %s | click Start Solve when ready"), + LiveAttemptInspectionElapsedMs, + RemainingInspectionMs, + *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(LiveInspectionPenalty) + ); + } + else if (bLiveSolvePhase) + { + DisplayedTimerStatusLine = FString::Printf( + TEXT("Phase: solving | inspection %d ms | solve %s ms | finish with Success / Failure / Timeout / DNF"), + LiveAttemptInspectionElapsedMs, + *RunningSolveLabel + ); + } + else + { + const FString InspectionLabel = bHasInspectionWindow + ? FString::Printf(TEXT("inspection target %d ms"), InspectionLimitMs) + : TEXT("no inspection window"); + DisplayedTimerStatusLine = FString::Printf( + TEXT("Timer idle | %s | start a live attempt for less-synthetic run entry"), + *InspectionLabel + ); + } if (TimerStatusTextBlock != nullptr) { - if (bLiveInspectionPhase) - { - const int32 RemainingInspectionMs = bHasInspectionWindow - ? FMath::Max(InspectionLimitMs - LiveAttemptInspectionElapsedMs, 0) - : 0; - TimerStatusTextBlock->SetText(FText::FromString(FString::Printf( - TEXT("Phase: inspection | elapsed %d ms | remaining %d ms | pending penalty: %s | click Start Solve when ready"), - LiveAttemptInspectionElapsedMs, - RemainingInspectionMs, - *HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(LiveInspectionPenalty) - ))); - } - else if (bLiveSolvePhase) - { - TimerStatusTextBlock->SetText(FText::FromString(FString::Printf( - TEXT("Phase: solving | inspection %d ms | solve %s ms | finish with Success / Failure / Timeout / DNF"), - LiveAttemptInspectionElapsedMs, - *RunningSolveLabel - ))); - } - else - { - const FString InspectionLabel = bHasInspectionWindow - ? FString::Printf(TEXT("inspection target %d ms"), InspectionLimitMs) - : TEXT("no inspection window"); - TimerStatusTextBlock->SetText(FText::FromString(FString::Printf( - TEXT("Timer idle | %s | start a live attempt for less-synthetic run entry"), - *InspectionLabel - ))); - } + TimerStatusTextBlock->SetText(FText::FromString(DisplayedTimerStatusLine)); } if (SplitHeaderTextBlock != nullptr) { diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index b9c1c0d..304a219 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -815,6 +815,18 @@ public: FHyperTwistTrainingCoachDashboardRunActionLabelsInspectSurface GetDisplayedRunActionLabelsInspectSurface() const; + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach|Dashboard") + FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface + GetDisplayedActiveRunStatusInspectSurface() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach|Dashboard") + FHyperTwistTrainingCoachDashboardActiveAttemptTimerStatusInspectSurface + GetDisplayedActiveAttemptTimerStatusInspectSurface() const; + + UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach|Dashboard") + FHyperTwistTrainingCoachDashboardCurrentCaseDeckStatusInspectSurface + GetDisplayedCurrentCaseDeckStatusInspectSurface() const; + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard") bool SelectPreviousMethodDrillFollowUpHistoryEntry(); @@ -1856,6 +1868,12 @@ private: FString DisplayedCoachHandoffRecommendationDetailLine; FString DisplayedCoachHandoffStatusLine; FString DisplayedTransitionStatusLine; + FString DisplayedActiveRunStatusLine; + FString DisplayedActiveRunSummaryLine; + FString DisplayedCurrentCasePromptLine; + FString DisplayedCurrentCaseNotationLine; + FString DisplayedAttemptInputStatusLine; + FString DisplayedTimerStatusLine; FString DisplayedRecommendedDeckPreviewLine; FString DisplayedFollowUpDeckPreviewLine; FString DisplayedContinueRunLabel; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h index 6c56d58..6ced625 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h @@ -10821,6 +10821,242 @@ struct FHyperTwistTrainingCoachDashboardHandoffRecommendationInspectSurface } }; +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SummaryLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TrainingSessionId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SessionStateLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeliveryModeLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ActiveDeckLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ActiveCaseId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString LastCaseId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RemainingCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AttemptCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 CompletedAttemptCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SuccessCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 FailureCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TimeoutCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DNFCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 BestTotalTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AverageTotalTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasActiveRun = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bClosedLoopIdleDashboard = false; + + bool IsStructurallyValid() const + { + return !StatusLine.IsEmpty() + || !SummaryLine.IsEmpty() + || bHasActiveRun; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachDashboardActiveAttemptTimerStatusInspectSurface +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString AttemptInputLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TimerStatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TimerPhaseLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString RunningSolveLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString InspectionWindowLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PendingPenaltyLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AttemptOverrideMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TargetTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 AverageTotalTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ResolvedSuccessTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ResolvedFailureTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ResolvedTimeoutTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 ResolvedDnfTimeMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 InspectionLimitMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 InspectionElapsedMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SolveElapsedMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 DisplayedElapsedMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasParsedAttemptOverride = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasLiveAttemptTimer = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bLiveInspectionPhase = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bLiveSolvePhase = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasInspectionWindow = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bRunningSolveTimeHidden = false; + + bool IsStructurallyValid() const + { + return !AttemptInputLine.IsEmpty() + || !TimerStatusLine.IsEmpty() + || bHasLiveAttemptTimer; + } +}; + +USTRUCT(BlueprintType) +struct FHyperTwistTrainingCoachDashboardCurrentCaseDeckStatusInspectSurface +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString Headline; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString StatusLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PromptLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString NotationLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ActiveDeckLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ActiveDeckId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString DeliveryModeLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ActiveCaseId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PromptLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString PromptKindLabel; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString SubsetId; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString ScrambleNotation; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString CanonicalNotation; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + FString TagLine; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 TimeTargetMs = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SelectionAttemptCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 SelectionSuccessCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + int32 RemainingCaseCount = 0; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasActiveRun = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bHasRunnableCurrentCase = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist") + bool bClosedLoopIdleDashboard = false; + + bool IsStructurallyValid() const + { + return !StatusLine.IsEmpty() + || !PromptLine.IsEmpty() + || !NotationLine.IsEmpty() + || bHasRunnableCurrentCase + || bHasActiveRun; + } +}; + USTRUCT(BlueprintType) struct FHyperTwistTrainingCoachDashboardHandoffStatusInspectSurface { diff --git a/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md b/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md index 3e93e7d..29c86fc 100644 --- a/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md +++ b/docs/HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md @@ -69,8 +69,8 @@ Current correction call from the source-exposed audit and current execution refr - the latest landed bounded `Phase 4` packet closes the retained-idle comparison/outcome actionability gap at the end of the recognition-assisted coach-to-review continuity lane, and the final retained-surface confirmation pass now leaves that closure lane functionally complete - the retained idle widget surface now keeps retained comparison and decision history inspectable without continuing to advertise live launch or guidance-outcome actions once the coach-to-review loop has already collapsed to truthful closed-loop idle - the now-complete deliberate `UHyperTwistTrainingRuntimeLibrary` world-context consumer lane reaches through active generated-mode selector option owned-execution split-phase current-selection merged ordered roster phase-id lookup, normalized-order lookup, and evaluation-order lookup inspect surfaces, so gameplay/Blueprint callers can inspect one retained merged-roster row directly by phase id, normalized order, or evaluation order without scanning the merged roster array or re-running inspect derivation details directly - - the latest landed bounded `Phase 4` packet stays on that same live `UHyperTwistCoachDashboardWidget` guidance-consumer lane and adds displayed coach handoff status, displayed transition status, and displayed continuation / queued / follow-up / recommended run action-label inspect surfaces over already-derived live dashboard transition and action-label state, so gameplay/Blueprint callers can inspect the current transition posture, coach handoff availability line, and surfaced launch/control labels without scraping dashboard text blocks or reopening transition/handoff derivation - - the next clean move is to stay on that same live dashboard guidance-consumer lane and land the displayed active-run status, active-attempt/timer status, and current-case / current-deck status inspect surface group rather than drifting back into backend continuity or reopening the already-complete retained-history seam + - the latest landed bounded `Phase 4` packet stays on that same live `UHyperTwistCoachDashboardWidget` guidance-consumer lane and adds displayed active-run status, displayed active-attempt/timer status, and displayed current-case / current-deck status inspect surfaces over already-derived live dashboard run-state, timer-state, and current-selection presentation state, so gameplay/Blueprint callers can inspect the current active-run line, run-summary line, attempt-input/timer posture, and current-case/current-deck presentation without scraping dashboard text blocks or reopening run/timer/current-case derivation + - the next clean move is to stay on that same live dashboard guidance-consumer lane and land the displayed last-attempt review summary, displayed last-attempt timing / derived-phase review, and live split-capture status inspect surface group rather than drifting back into backend continuity or reopening the already-complete retained-history seam - the current execution-discipline rule that broad refactor / monolith-splitting work should not interrupt the active bounded roadmap packet unless structure is actually blocking it ## Current execution-reality references diff --git a/docs/arch/HYPERTWIST_PHASE4_DASHBOARD_LIVE_ACTIVE_RUN_ATTEMPT_TIMER_CURRENT_CASE_DECK_STATUS_INSPECT_SURFACE_PACKET_2026-05-09.md b/docs/arch/HYPERTWIST_PHASE4_DASHBOARD_LIVE_ACTIVE_RUN_ATTEMPT_TIMER_CURRENT_CASE_DECK_STATUS_INSPECT_SURFACE_PACKET_2026-05-09.md new file mode 100644 index 0000000..1bf2a65 --- /dev/null +++ b/docs/arch/HYPERTWIST_PHASE4_DASHBOARD_LIVE_ACTIVE_RUN_ATTEMPT_TIMER_CURRENT_CASE_DECK_STATUS_INSPECT_SURFACE_PACKET_2026-05-09.md @@ -0,0 +1,56 @@ +# HyperTwist Phase 4 dashboard live active-run, attempt-timer, and current-case / current-deck status inspect surface packet + +- bounded Phase `4` dashboard-consumer slice +- date: `2026-05-09` + +## Intent + +Stay on the deliberate live `UHyperTwistCoachDashboardWidget` guidance-consumer lane and expose the remaining live active-run, attempt/timer, and current-case/current-deck presentation state directly to gameplay and Blueprint callers. + +## Why this packet exists + +The dashboard already derived: + +- the displayed active-run status line +- the displayed active-run summary line +- the displayed attempt-input and live-timer status lines +- the displayed current-case prompt and notation lines + +But that live state still only surfaced through dashboard text blocks. Callers still had to scrape widget presentation or reopen active-run / timer / current-case derivation to inspect what the dashboard was currently showing. + +## What changed + +- added `FHyperTwistTrainingCoachDashboardActiveRunStatusInspectSurface` +- added `FHyperTwistTrainingCoachDashboardActiveAttemptTimerStatusInspectSurface` +- added `FHyperTwistTrainingCoachDashboardCurrentCaseDeckStatusInspectSurface` +- added `GetDisplayedActiveRunStatusInspectSurface()` +- added `GetDisplayedActiveAttemptTimerStatusInspectSurface()` +- added `GetDisplayedCurrentCaseDeckStatusInspectSurface()` +- retained the exact displayed active-run status/summary lines, attempt-input line, timer-status line, and current-case prompt/notation lines inside the widget so the new inspect surfaces report the same live strings the dashboard is already presenting +- kept the packet bounded to `UHyperTwistCoachDashboardWidget` plus shared surface types, with no dashboard redesign and no backend continuity changes + +## Files + +- `C:\HyperTwist\UnrealHyperTwist\Source\UnrealHyperTwist\Public\HyperTwistTraining\HyperTwistTrainingTypes.h` +- `C:\HyperTwist\UnrealHyperTwist\Source\UnrealHyperTwist\Public\HyperTwistTraining\HyperTwistCoachDashboardWidget.h` +- `C:\HyperTwist\UnrealHyperTwist\Source\UnrealHyperTwist\Private\HyperTwistTraining\HyperTwistCoachDashboardWidget.cpp` +- `C:\HyperTwist\docs\HYPERTWIST_ROADMAP_OVERHAUL_EXPANSION_GUIDE.md` + +## Outcome + +Gameplay and Blueprint callers can now inspect: + +- the displayed active-run line and summarized active-run attempt totals +- the displayed attempt-input posture and live timer phase/status line +- the displayed current-case prompt/notation presentation plus the active current-deck context + +without scraping dashboard text blocks or reopening active-run / timer / current-case derivation. + +## Validation + +- full `Development Editor | Win64` MSBuild on `UnrealHyperTwist.sln` + - passed with `0 Warning(s), 0 Error(s)` + +## Next clean slice + +Stay on the same live dashboard guidance-consumer lane and land the displayed last-attempt review summary, displayed last-attempt timing / derived-phase review, and live split-capture status inspect surface group.