From 9c46be27b93f88e02b469421cb5f2f41f9b4aaeb Mon Sep 17 00:00:00 2001 From: axiomlogicnexus Date: Mon, 27 Apr 2026 05:23:22 +0200 Subject: [PATCH] Refine coach dashboard operations surface --- .../HyperTwistCoachDashboardWidget.cpp | 249 +++++++++++++++++- .../HyperTwistCoachDashboardWidget.h | 42 +++ 2 files changed, 289 insertions(+), 2 deletions(-) diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp index c30e507..8ab3bad 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp @@ -38,6 +38,7 @@ namespace HyperTwistCoachDashboardWidgetInternal { Slot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, static_cast(PaddingBottom))); } + TextBlock->SetAutoWrapText(true); return TextBlock; } @@ -237,6 +238,20 @@ void UHyperTwistCoachDashboardWidget::HandlePromoteClicked() PromoteFirstDeferredQueueEntry(); } +void UHyperTwistCoachDashboardWidget::HandleCompleteRunClicked() +{ + CompleteActiveTrainingRun(false); + SyncSelectedQueueEntry(); + UpdateDashboardPresentation(); +} + +void UHyperTwistCoachDashboardWidget::HandleClearRunClicked() +{ + ClearActiveTrainingRun(); + SyncSelectedQueueEntry(); + UpdateDashboardPresentation(); +} + void UHyperTwistCoachDashboardWidget::HandlePreviousQueueClicked() { SelectPreviousQueueEntry(); @@ -285,6 +300,24 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() TEXT("CoachFocus"), 8 ); + ActiveRunHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachActiveRunHeader"), + 2 + ); + ActiveRunStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachActiveRunStatus"), + 2 + ); + ActiveRunSummaryTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachActiveRunSummary"), + 8 + ); FollowUpTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, @@ -319,12 +352,42 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() WidgetTree, RootLayout, TEXT("CoachQueueExecution"), + 8 + ); + ScheduleHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachScheduleHeader"), 2 ); + ScheduleStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachScheduleStatus"), + 2 + ); + SchedulePolicyTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachSchedulePolicy"), + 8 + ); VerificationStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( WidgetTree, RootLayout, TEXT("CoachVerificationStatus"), + 2 + ); + VerificationHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachVerificationHeader"), + 2 + ); + VerificationDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow( + WidgetTree, + RootLayout, + TEXT("CoachVerificationDetail"), 10 ); @@ -384,6 +447,40 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() PromoteButtonLabel = PromoteLabelRaw; } + UHorizontalBox* RunControlRow = WidgetTree->ConstructWidget( + UHorizontalBox::StaticClass(), + TEXT("CoachDashboardRunControlButtons") + ); + if (RunControlRow != nullptr) + { + if (UVerticalBoxSlot* RunControlRowSlot = RootLayout->AddChildToVerticalBox(RunControlRow)) + { + RunControlRowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 0.0f)); + } + + UTextBlock* CompleteRunLabelRaw = nullptr; + CompleteRunButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + RunControlRow, + TEXT("CompleteRunButton"), + TEXT("CompleteRunButtonLabel"), + TEXT("Complete Run"), + CompleteRunLabelRaw + ); + CompleteRunButtonLabel = CompleteRunLabelRaw; + + UTextBlock* ClearRunLabelRaw = nullptr; + ClearRunButton = HyperTwistCoachDashboardWidgetInternal::AddButton( + WidgetTree, + RunControlRow, + TEXT("ClearRunButton"), + TEXT("ClearRunButtonLabel"), + TEXT("Clear Run"), + ClearRunLabelRaw + ); + ClearRunButtonLabel = ClearRunLabelRaw; + } + UHorizontalBox* QueueNavRow = WidgetTree->ConstructWidget( UHorizontalBox::StaticClass(), TEXT("CoachDashboardQueueNavButtons") @@ -445,6 +542,14 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt() { PromoteButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePromoteClicked); } + if (CompleteRunButton != nullptr) + { + CompleteRunButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleCompleteRunClicked); + } + if (ClearRunButton != nullptr) + { + ClearRunButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleClearRunClicked); + } if (PreviousQueueButton != nullptr) { PreviousQueueButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandlePreviousQueueClicked); @@ -515,6 +620,18 @@ void UHyperTwistCoachDashboardWidget::SyncSelectedQueueEntry() void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { + const bool bHasActiveRun = CachedRunState.Session.IsStructurallyValid() + && CachedRunState.ActiveDeck.IsStructurallyValid(); + const FString ActiveCaseId = CachedRunState.CurrentSelection.TrainingCase.CaseId.IsEmpty() + ? TEXT("n/a") + : CachedRunState.CurrentSelection.TrainingCase.CaseId; + const FString ActiveDeckLabel = CachedRunState.ActiveDeck.Title.IsEmpty() + ? (CachedRunState.ActiveDeck.DeckId.IsEmpty() ? TEXT("n/a") : CachedRunState.ActiveDeck.DeckId) + : CachedRunState.ActiveDeck.Title; + const FString VerificationNotePreview = CachedRepositoryRoundTripVerification.VerificationNotes.Num() > 0 + ? CachedRepositoryRoundTripVerification.VerificationNotes[0] + : TEXT("none"); + if (HeadlineTextBlock != nullptr) { HeadlineTextBlock->SetText(FText::FromString(CachedCoachPanelState.Headline)); @@ -532,6 +649,40 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() CachedCoachPanelState.FocusCaseCount ))); } + if (ActiveRunHeaderTextBlock != nullptr) + { + ActiveRunHeaderTextBlock->SetText(FText::FromString(TEXT("[Active Run]"))); + } + 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() + ) + : TEXT("No active run. Queue and coach guidance are ready for launch from this screen."); + ActiveRunStatusTextBlock->SetText(FText::FromString(ActiveRunLine)); + } + 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 + ))); + } if (FollowUpTextBlock != nullptr) { const FString FollowUpLine = @@ -614,20 +765,98 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() if (QueueExecutionTextBlock != nullptr) { QueueExecutionTextBlock->SetText(FText::FromString(FString::Printf( - TEXT("Execution: queued %d | started %d | completed %d | deferred %d | manual reschedules %d | outstanding deferred: %s | completion gap: %s"), + TEXT("Execution: queued %d | started %d | completed %d | deferred %d | promoted %d | manual reschedules %d | outstanding deferred: %s | completion gap: %s | archive-aware friction: %s | blocked follow-up friction: %s"), CachedCoachQueueExecutionSummary.QueuedEventCount, CachedCoachQueueExecutionSummary.StartedEventCount, CachedCoachQueueExecutionSummary.CompletedEventCount, CachedCoachQueueExecutionSummary.DeferredEventCount, + CachedCoachQueueExecutionSummary.PromotedEventCount, CachedCoachQueueExecutionSummary.ManualRescheduleEventCount, CachedCoachQueueExecutionSummary.bHasOutstandingDeferredWork ? TEXT("yes") : TEXT("no"), - CachedCoachQueueExecutionSummary.bHasCompletionGap ? TEXT("yes") : TEXT("no") + CachedCoachQueueExecutionSummary.bHasCompletionGap ? TEXT("yes") : TEXT("no"), + CachedCoachQueueExecutionSummary.bHasArchiveAwareFriction ? TEXT("yes") : TEXT("no"), + CachedCoachQueueExecutionSummary.bHasBlockedFollowUpFriction ? TEXT("yes") : TEXT("no") + ))); + } + if (ScheduleHeaderTextBlock != nullptr) + { + ScheduleHeaderTextBlock->SetText(FText::FromString(TEXT("[Schedule / Friction]"))); + } + if (ScheduleStatusTextBlock != nullptr) + { + ScheduleStatusTextBlock->SetText(FText::FromString(FString::Printf( + TEXT("Horizon: %s | Immediate %d | Deferred %d | Archive-aware %d | Follow-up %d | Pinned %d | Immediate window: %s | Deferred window: %s | Blocked follow-up: %s"), + CachedCoachScheduleHorizonSummary.CurrentHorizonLabel.IsEmpty() + ? TEXT("n/a") + : *CachedCoachScheduleHorizonSummary.CurrentHorizonLabel, + CachedCoachScheduleHorizonSummary.ImmediateEntryCount, + CachedCoachScheduleHorizonSummary.DeferredEntryCount, + CachedCoachScheduleHorizonSummary.ArchiveAwareEntryCount, + CachedCoachScheduleHorizonSummary.FollowUpEntryCount, + CachedCoachScheduleHorizonSummary.PinnedEntryCount, + CachedCoachScheduleHorizonSummary.bHasImmediateWindow ? TEXT("yes") : TEXT("no"), + CachedCoachScheduleHorizonSummary.bHasDeferredWindow ? TEXT("yes") : TEXT("no"), + CachedCoachScheduleHorizonSummary.bHasBlockedFollowUp ? TEXT("yes") : TEXT("no") + ))); + } + if (SchedulePolicyTextBlock != nullptr) + { + SchedulePolicyTextBlock->SetText(FText::FromString(FString::Printf( + TEXT("Policy: immediate %d | pending %d | deferred %d | manual %d | earliest deferred: %s | dominant reason: %s | pressure %.2f | manual allowed: %s | expired deferrals: %s | review needed: %s | archive-aware: %s | blocked follow-up: %s"), + CachedCoachSchedulePolicySummary.ImmediateEntryCount, + CachedCoachSchedulePolicySummary.PendingEntryCount, + CachedCoachSchedulePolicySummary.DeferredEntryCount, + CachedCoachSchedulePolicySummary.ManualRescheduleEntryCount, + CachedCoachSchedulePolicySummary.EarliestDeferredUntilUtc.IsEmpty() + ? TEXT("n/a") + : *CachedCoachSchedulePolicySummary.EarliestDeferredUntilUtc, + CachedCoachSchedulePolicySummary.DominantDeferralReasonLabel.IsEmpty() + ? TEXT("n/a") + : *CachedCoachSchedulePolicySummary.DominantDeferralReasonLabel, + CachedCoachSchedulePolicySummary.ReschedulePressureScore, + CachedCoachSchedulePolicySummary.bAllowsManualReschedule ? TEXT("yes") : TEXT("no"), + CachedCoachSchedulePolicySummary.bHasExpiredDeferrals ? TEXT("yes") : TEXT("no"), + CachedCoachSchedulePolicySummary.bNeedsManualRescheduleReview ? TEXT("yes") : TEXT("no"), + CachedCoachSchedulePolicySummary.bHasArchiveAwareDeferrals ? TEXT("yes") : TEXT("no"), + CachedCoachSchedulePolicySummary.bHasBlockedFollowUpDeferrals ? TEXT("yes") : TEXT("no") ))); } if (VerificationStatusTextBlock != nullptr) { VerificationStatusTextBlock->SetText(FText::FromString(CachedCoachPanelState.VerificationStatusLine)); } + if (VerificationHeaderTextBlock != nullptr) + { + VerificationHeaderTextBlock->SetText(FText::FromString(TEXT("[Repository Verification]"))); + } + if (VerificationDetailTextBlock != nullptr) + { + VerificationDetailTextBlock->SetText(FText::FromString(FString::Printf( + TEXT("Path: %s\nRound-trip pass: %s | Save: %s | Load: %s | Count parity: %s | State parity: %s | Integrity parity: %s\nRuns %d/%d | Learners %d/%d | Reviews %d/%d | Plans %d/%d | Queues %d/%d | History %d/%d | Notes: %s"), + CachedRepositoryRoundTripVerification.ResolvedPath.IsEmpty() + ? TEXT("not yet verified") + : *CachedRepositoryRoundTripVerification.ResolvedPath, + CachedRepositoryRoundTripVerification.bDidRoundTripPass ? TEXT("yes") : TEXT("no"), + CachedRepositoryRoundTripVerification.bSaveSucceeded ? TEXT("yes") : TEXT("no"), + CachedRepositoryRoundTripVerification.bLoadSucceeded ? TEXT("yes") : TEXT("no"), + CachedRepositoryRoundTripVerification.bRepositoryCountsMatch ? TEXT("yes") : TEXT("no"), + CachedRepositoryRoundTripVerification.bSerializedStateMatch ? TEXT("yes") : TEXT("no"), + CachedRepositoryRoundTripVerification.bIntegrityReportMatch ? TEXT("yes") : TEXT("no"), + CachedRepositoryRoundTripVerification.LoadedRunRecordCount, + CachedRepositoryRoundTripVerification.ExpectedRunRecordCount, + CachedRepositoryRoundTripVerification.LoadedLearnerStateCount, + CachedRepositoryRoundTripVerification.ExpectedLearnerStateCount, + CachedRepositoryRoundTripVerification.LoadedReviewPlanCount, + CachedRepositoryRoundTripVerification.ExpectedReviewPlanCount, + CachedRepositoryRoundTripVerification.LoadedCoachActionPlanCount, + CachedRepositoryRoundTripVerification.ExpectedCoachActionPlanCount, + CachedRepositoryRoundTripVerification.LoadedCoachSessionQueueCount, + CachedRepositoryRoundTripVerification.ExpectedCoachSessionQueueCount, + CachedRepositoryRoundTripVerification.LoadedCoachSessionQueueHistoryCount, + CachedRepositoryRoundTripVerification.ExpectedCoachSessionQueueHistoryCount, + *VerificationNotePreview + ))); + } FString PrimaryLabel = CachedCoachPanelState.PrimaryActionLabel; if (PrimaryLabel.IsEmpty()) @@ -667,6 +896,22 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation() { PromoteButton->SetIsEnabled(!FindFirstDeferredQueueEntryId().IsEmpty()); } + if (CompleteRunButtonLabel != nullptr) + { + CompleteRunButtonLabel->SetText(FText::FromString(TEXT("Complete Run"))); + } + if (CompleteRunButton != nullptr) + { + CompleteRunButton->SetIsEnabled(bHasActiveRun); + } + if (ClearRunButtonLabel != nullptr) + { + ClearRunButtonLabel->SetText(FText::FromString(TEXT("Clear Run"))); + } + if (ClearRunButton != nullptr) + { + ClearRunButton->SetIsEnabled(bHasActiveRun); + } if (PreviousQueueButtonLabel != nullptr) { PreviousQueueButtonLabel->SetText(FText::FromString(TEXT("Prev Queue"))); diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h index 98a672b..45b12cc 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h @@ -59,12 +59,36 @@ protected: UPROPERTY(Transient) TObjectPtr FocusTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr ActiveRunHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr ActiveRunStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr ActiveRunSummaryTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr QueueStatusTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr ScheduleHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr ScheduleStatusTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr SchedulePolicyTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr VerificationStatusTextBlock = nullptr; + UPROPERTY(Transient) + TObjectPtr VerificationHeaderTextBlock = nullptr; + + UPROPERTY(Transient) + TObjectPtr VerificationDetailTextBlock = nullptr; + UPROPERTY(Transient) TObjectPtr QueueCountsTextBlock = nullptr; @@ -104,6 +128,18 @@ protected: UPROPERTY(Transient) TObjectPtr PromoteButtonLabel = nullptr; + UPROPERTY(Transient) + TObjectPtr CompleteRunButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr CompleteRunButtonLabel = nullptr; + + UPROPERTY(Transient) + TObjectPtr ClearRunButton = nullptr; + + UPROPERTY(Transient) + TObjectPtr ClearRunButtonLabel = nullptr; + UPROPERTY(Transient) TObjectPtr PreviousQueueButton = nullptr; @@ -134,6 +170,12 @@ protected: UFUNCTION() void HandlePromoteClicked(); + UFUNCTION() + void HandleCompleteRunClicked(); + + UFUNCTION() + void HandleClearRunClicked(); + UFUNCTION() void HandlePreviousQueueClicked();