Refine coach dashboard operations surface

This commit is contained in:
axiomlogicnexus 2026-04-27 05:23:22 +02:00
parent 23fa842616
commit 9c46be27b9
2 changed files with 289 additions and 2 deletions

View file

@ -38,6 +38,7 @@ namespace HyperTwistCoachDashboardWidgetInternal
{
Slot->SetPadding(FMargin(0.0f, 0.0f, 0.0f, static_cast<float>(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>(
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>(
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")));

View file

@ -59,12 +59,36 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> FocusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ActiveRunHeaderTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ActiveRunStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ActiveRunSummaryTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> QueueStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ScheduleHeaderTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ScheduleStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> SchedulePolicyTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> VerificationStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> VerificationHeaderTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> VerificationDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> QueueCountsTextBlock = nullptr;
@ -104,6 +128,18 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PromoteButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> CompleteRunButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> CompleteRunButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> ClearRunButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> ClearRunButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PreviousQueueButton = nullptr;
@ -134,6 +170,12 @@ protected:
UFUNCTION()
void HandlePromoteClicked();
UFUNCTION()
void HandleCompleteRunClicked();
UFUNCTION()
void HandleClearRunClicked();
UFUNCTION()
void HandlePreviousQueueClicked();