Record dashboard template launch provenance

This commit is contained in:
axiomlogicnexus 2026-05-06 23:18:29 +02:00
parent 5be3abea59
commit b2ca2957fe
3 changed files with 584 additions and 31 deletions

View file

@ -2806,6 +2806,7 @@ void UHyperTwistCoachDashboardWidget::RefreshCoachDashboardView()
SyncSelectedAttempt();
SyncSelectedGuidanceDecisionHistoryEntry();
SyncSelectedHandoffHistoryEntry();
SyncSelectedTemplateLaunchHistoryEntry();
SyncSelectedQueueSuppressionHistoryEntry();
SyncSelectedClosureRecoveryHistoryEntry();
SyncSelectedMethodDrillFollowUpHistoryEntry();
@ -3511,21 +3512,26 @@ FHyperTwistTrainingRunState UHyperTwistCoachDashboardWidget::StartPreferredActiv
return FHyperTwistTrainingRunState();
}
const FString ResolvedUserId = PreferredTemplate->UserId.IsEmpty()
const FHyperTwistTrainingSessionTemplate LaunchedTemplate = *PreferredTemplate;
const FString ResolvedUserId = LaunchedTemplate.UserId.IsEmpty()
? DefaultUserId
: PreferredTemplate->UserId;
: LaunchedTemplate.UserId;
const FString LaunchSessionId = FString::Printf(
TEXT("%s_template_%s_%s"),
*DefaultSessionId,
*PreferredTemplate->TemplateId,
*LaunchedTemplate.TemplateId,
*FGuid::NewGuid().ToString(EGuidFormats::Digits)
);
LastStepResult = FHyperTwistTrainingRunStepResult();
const FHyperTwistTrainingRunState RunState = StartTrainingRunFromTemplate(
PreferredTemplate->TemplateId,
LaunchedTemplate.TemplateId,
ResolvedUserId,
LaunchSessionId
);
if (RunState.Session.IsStructurallyValid())
{
CaptureActiveTemplateLaunchContext(RunState.Session.TrainingSessionId, LaunchedTemplate);
}
SyncRetainedRunRecap();
SyncSelectedQueueEntry();
SyncSelectedAttempt();
@ -3689,6 +3695,9 @@ void UHyperTwistCoachDashboardWidget::ClearRetainedRunRecap()
RetainedRunRecapUserId.Reset();
RetainedRunRecapSessionId.Reset();
RetainedRunRecapMode = EHyperTwistTrainingDeliveryMode::Timer;
bHasRetainedTemplateLaunch = false;
RetainedTemplateLaunchSessionId.Reset();
RetainedTemplateLaunchTemplate = FHyperTwistTrainingSessionTemplate();
bHasRetainedGuidanceComparison = false;
RetainedGuidanceComparisonSessionId.Reset();
RetainedGuidanceComparisonLane = EHyperTwistCoachDashboardGuidancePreviewLane::None;
@ -4057,6 +4066,47 @@ bool UHyperTwistCoachDashboardWidget::SelectNextHandoffHistoryEntry()
return true;
}
bool UHyperTwistCoachDashboardWidget::SelectPreviousTemplateLaunchHistoryEntry()
{
if (TemplateLaunchHistoryEntries.Num() <= 0)
{
SelectedTemplateLaunchHistoryIndex = INDEX_NONE;
return false;
}
if (SelectedTemplateLaunchHistoryIndex == INDEX_NONE)
{
SyncSelectedTemplateLaunchHistoryEntry();
return SelectedTemplateLaunchHistoryIndex != INDEX_NONE;
}
SelectedTemplateLaunchHistoryIndex =
(SelectedTemplateLaunchHistoryIndex - 1 + TemplateLaunchHistoryEntries.Num())
% TemplateLaunchHistoryEntries.Num();
UpdateDashboardPresentation();
return true;
}
bool UHyperTwistCoachDashboardWidget::SelectNextTemplateLaunchHistoryEntry()
{
if (TemplateLaunchHistoryEntries.Num() <= 0)
{
SelectedTemplateLaunchHistoryIndex = INDEX_NONE;
return false;
}
if (SelectedTemplateLaunchHistoryIndex == INDEX_NONE)
{
SyncSelectedTemplateLaunchHistoryEntry();
return SelectedTemplateLaunchHistoryIndex != INDEX_NONE;
}
SelectedTemplateLaunchHistoryIndex =
(SelectedTemplateLaunchHistoryIndex + 1) % TemplateLaunchHistoryEntries.Num();
UpdateDashboardPresentation();
return true;
}
bool UHyperTwistCoachDashboardWidget::SelectPreviousQueueSuppressionHistoryEntry()
{
if (QueueSuppressionHistoryEntries.Num() <= 0)
@ -4613,6 +4663,16 @@ void UHyperTwistCoachDashboardWidget::HandleNextHandoffHistoryClicked()
SelectNextHandoffHistoryEntry();
}
void UHyperTwistCoachDashboardWidget::HandlePreviousTemplateLaunchHistoryClicked()
{
SelectPreviousTemplateLaunchHistoryEntry();
}
void UHyperTwistCoachDashboardWidget::HandleNextTemplateLaunchHistoryClicked()
{
SelectNextTemplateLaunchHistoryEntry();
}
void UHyperTwistCoachDashboardWidget::HandlePreviousQueueSuppressionClicked()
{
SelectPreviousQueueSuppressionHistoryEntry();
@ -5018,6 +5078,58 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
TEXT("CoachTemplateLaunchDetail"),
8
);
TemplateLaunchHistoryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachTemplateLaunchHistoryHeader"),
2
);
TemplateLaunchHistoryStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachTemplateLaunchHistoryStatus"),
2
);
TemplateLaunchHistoryDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
TEXT("CoachTemplateLaunchHistoryDetail"),
8
);
UHorizontalBox* TemplateLaunchHistoryNavRow = WidgetTree->ConstructWidget<UHorizontalBox>(
UHorizontalBox::StaticClass(),
TEXT("CoachDashboardTemplateLaunchHistoryNavRow")
);
if (TemplateLaunchHistoryNavRow != nullptr)
{
if (UVerticalBoxSlot* TemplateLaunchHistoryNavRowSlot =
RootLayout->AddChildToVerticalBox(TemplateLaunchHistoryNavRow))
{
TemplateLaunchHistoryNavRowSlot->SetPadding(FMargin(0.0f, 4.0f, 0.0f, 0.0f));
}
UTextBlock* PreviousTemplateLaunchHistoryLabelRaw = nullptr;
PreviousTemplateLaunchHistoryButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
TemplateLaunchHistoryNavRow,
TEXT("PreviousTemplateLaunchHistoryButton"),
TEXT("PreviousTemplateLaunchHistoryButtonLabel"),
TEXT("Prev Template Launch"),
PreviousTemplateLaunchHistoryLabelRaw
);
PreviousTemplateLaunchHistoryButtonLabel = PreviousTemplateLaunchHistoryLabelRaw;
UTextBlock* NextTemplateLaunchHistoryLabelRaw = nullptr;
NextTemplateLaunchHistoryButton = HyperTwistCoachDashboardWidgetInternal::AddButton(
WidgetTree,
TemplateLaunchHistoryNavRow,
TEXT("NextTemplateLaunchHistoryButton"),
TEXT("NextTemplateLaunchHistoryButtonLabel"),
TEXT("Next Template Launch"),
NextTemplateLaunchHistoryLabelRaw
);
NextTemplateLaunchHistoryButtonLabel = NextTemplateLaunchHistoryLabelRaw;
}
HandoffHistoryHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
WidgetTree,
RootLayout,
@ -6277,6 +6389,20 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
{
NextHandoffHistoryButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleNextHandoffHistoryClicked);
}
if (PreviousTemplateLaunchHistoryButton != nullptr)
{
PreviousTemplateLaunchHistoryButton->OnClicked.AddDynamic(
this,
&UHyperTwistCoachDashboardWidget::HandlePreviousTemplateLaunchHistoryClicked
);
}
if (NextTemplateLaunchHistoryButton != nullptr)
{
NextTemplateLaunchHistoryButton->OnClicked.AddDynamic(
this,
&UHyperTwistCoachDashboardWidget::HandleNextTemplateLaunchHistoryClicked
);
}
if (DismissRecapButton != nullptr)
{
DismissRecapButton->OnClicked.AddDynamic(this, &UHyperTwistCoachDashboardWidget::HandleDismissRecapClicked);
@ -6537,6 +6663,15 @@ void UHyperTwistCoachDashboardWidget::SyncRetainedRunRecap()
RetainedRunRecapUserId = CachedRunState.Session.UserId;
RetainedRunRecapSessionId = CachedRunState.Session.TrainingSessionId;
RetainedRunRecapMode = CachedRunState.Session.Mode;
if (!ActiveTemplateLaunchSessionId.IsEmpty()
&& ActiveTemplateLaunchSessionId == RetainedRunRecapSessionId
&& ActiveTemplateLaunchTemplate.IsStructurallyValid())
{
bHasRetainedTemplateLaunch = true;
RetainedTemplateLaunchSessionId = ActiveTemplateLaunchSessionId;
RetainedTemplateLaunchTemplate = ActiveTemplateLaunchTemplate;
RecordTemplateLaunchHistoryEntry(RetainedRunRecapSummary);
}
if (!ActiveGuidanceLaunchSessionId.IsEmpty()
&& ActiveGuidanceLaunchSessionId == RetainedRunRecapSessionId
&& ActiveGuidanceLaunchLane != EHyperTwistCoachDashboardGuidancePreviewLane::None)
@ -6611,6 +6746,21 @@ void UHyperTwistCoachDashboardWidget::SyncSelectedHandoffHistoryEntry()
}
}
void UHyperTwistCoachDashboardWidget::SyncSelectedTemplateLaunchHistoryEntry()
{
if (TemplateLaunchHistoryEntries.Num() <= 0)
{
SelectedTemplateLaunchHistoryIndex = INDEX_NONE;
return;
}
if (SelectedTemplateLaunchHistoryIndex < 0
|| SelectedTemplateLaunchHistoryIndex >= TemplateLaunchHistoryEntries.Num())
{
SelectedTemplateLaunchHistoryIndex = TemplateLaunchHistoryEntries.Num() - 1;
}
}
void UHyperTwistCoachDashboardWidget::SyncSelectedQueueSuppressionHistoryEntry()
{
if (QueueSuppressionHistoryEntries.Num() <= 0)
@ -6783,6 +6933,15 @@ void UHyperTwistCoachDashboardWidget::CaptureActiveGuidanceLaunchContext(
}
}
void UHyperTwistCoachDashboardWidget::CaptureActiveTemplateLaunchContext(
const FString& SessionId,
const FHyperTwistTrainingSessionTemplate& SessionTemplate
)
{
ActiveTemplateLaunchSessionId = SessionId;
ActiveTemplateLaunchTemplate = SessionTemplate;
}
void UHyperTwistCoachDashboardWidget::CaptureActiveMethodDrillFollowUpLaunchContext(
const FString& SessionId
)
@ -9699,6 +9858,65 @@ void UHyperTwistCoachDashboardWidget::RecordClosureRecoverySnapshot()
SelectedClosureRecoveryIndex = ClosureRecoveryHistoryEntries.Num() - 1;
}
void UHyperTwistCoachDashboardWidget::RecordTemplateLaunchHistoryEntry(
const FHyperTwistTrainingSessionSummary& CompletedRunRecap
)
{
if (CompletedRunRecap.TrainingSessionId.IsEmpty()
|| !ActiveTemplateLaunchTemplate.IsStructurallyValid()
|| ActiveTemplateLaunchSessionId != CompletedRunRecap.TrainingSessionId)
{
return;
}
FHyperTwistCoachDashboardTemplateLaunchHistoryEntry Entry;
Entry.RecordedAtUtc = FDateTime::UtcNow().ToIso8601();
Entry.SourceSessionId = CompletedRunRecap.TrainingSessionId;
Entry.TemplateId = ActiveTemplateLaunchTemplate.TemplateId;
Entry.TemplateTitle = ActiveTemplateLaunchTemplate.Title;
Entry.UserId = ActiveTemplateLaunchTemplate.UserId;
Entry.FocusDeckId = ActiveTemplateLaunchTemplate.FocusDeckId;
Entry.SourceReviewPlanId = ActiveTemplateLaunchTemplate.SourceReviewPlanId;
Entry.TemplateKind = ActiveTemplateLaunchTemplate.TemplateKind;
Entry.SessionState = CompletedRunRecap.SessionState;
Entry.SuggestedMode = ActiveTemplateLaunchTemplate.SuggestedMode;
Entry.SuggestedSelectionPolicy = ActiveTemplateLaunchTemplate.SuggestedSelectionPolicy;
Entry.RecommendedCaseCount = ActiveTemplateLaunchTemplate.RecommendedCaseCount;
Entry.FocusCaseCount = ActiveTemplateLaunchTemplate.FocusCaseIds.Num();
Entry.CompletedAttemptCount = CompletedRunRecap.CompletedAttemptCount;
Entry.TotalMistakes = CompletedRunRecap.TotalMistakes;
Entry.SuccessRate = CompletedRunRecap.SuccessRate;
Entry.CompletedAtUtc = CompletedRunRecap.LastCompletedAtUtc;
Entry.bPreferReviewRun = ActiveTemplateLaunchTemplate.bPreferReviewRun;
Entry.bCoachReview = ActiveTemplateLaunchTemplate.bCoachReview;
Entry.bPreferShortReviewSet = ActiveTemplateLaunchTemplate.bPreferShortReviewSet;
if (!Entry.IsStructurallyValid())
{
return;
}
const bool bAlreadyRecorded = TemplateLaunchHistoryEntries.ContainsByPredicate(
[&Entry](const FHyperTwistCoachDashboardTemplateLaunchHistoryEntry& Candidate)
{
return Candidate.SourceSessionId == Entry.SourceSessionId;
}
);
if (bAlreadyRecorded)
{
return;
}
TemplateLaunchHistoryEntries.Add(Entry);
constexpr int32 MaxRetainedTemplateLaunchHistoryEntries = 16;
if (TemplateLaunchHistoryEntries.Num() > MaxRetainedTemplateLaunchHistoryEntries)
{
const int32 ExcessCount =
TemplateLaunchHistoryEntries.Num() - MaxRetainedTemplateLaunchHistoryEntries;
TemplateLaunchHistoryEntries.RemoveAt(0, ExcessCount, EAllowShrinking::No);
}
SelectedTemplateLaunchHistoryIndex = TemplateLaunchHistoryEntries.Num() - 1;
}
void UHyperTwistCoachDashboardWidget::RecordMethodDrillFollowUpHistoryEntry(
const FHyperTwistTrainingSessionSummary& CompletedRunRecap
)
@ -10860,6 +11078,28 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
const FString RunRecapSourceLabel = bHasCurrentRunRecap
? TEXT("current run")
: TEXT("last completed run");
const bool bHasActiveTemplateLaunchForDisplayedRun =
bHasCurrentRunRecap
&& DisplayedRunRecap != nullptr
&& !ActiveTemplateLaunchSessionId.IsEmpty()
&& DisplayedRunRecap->TrainingSessionId == ActiveTemplateLaunchSessionId
&& ActiveTemplateLaunchTemplate.IsStructurallyValid();
const bool bHasDisplayedRetainedTemplateLaunch =
!bHasCurrentRunRecap
&& bHasRetainedTemplateLaunch
&& DisplayedRunRecap != nullptr
&& !RetainedTemplateLaunchSessionId.IsEmpty()
&& DisplayedRunRecap->TrainingSessionId == RetainedTemplateLaunchSessionId
&& RetainedTemplateLaunchTemplate.IsStructurallyValid();
const FHyperTwistTrainingSessionTemplate* DisplayedTemplateLaunch =
bHasActiveTemplateLaunchForDisplayedRun
? &ActiveTemplateLaunchTemplate
: (bHasDisplayedRetainedTemplateLaunch ? &RetainedTemplateLaunchTemplate : nullptr);
const bool bDisplayedTemplateLaunchUsesReviewLane =
DisplayedTemplateLaunch != nullptr
&& (DisplayedTemplateLaunch->bPreferReviewRun
|| DisplayedTemplateLaunch->TemplateKind == EHyperTwistTrainingSessionTemplateKind::ReviewPlan
|| DisplayedTemplateLaunch->TemplateKind == EHyperTwistTrainingSessionTemplateKind::Coach);
const bool bCanRepeatDisplayedRun = !bHasLiveAttemptTimer
&& RetainedRunRecapDeck.IsStructurallyValid()
&& (!bHasActiveRun
@ -10995,6 +11235,58 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
PreferredTemplateLaunch->bPreferShortReviewSet ? TEXT("yes") : TEXT("no")
);
}
const FHyperTwistCoachDashboardTemplateLaunchHistoryEntry* SelectedTemplateLaunchHistoryEntry =
SelectedTemplateLaunchHistoryIndex >= 0
&& SelectedTemplateLaunchHistoryIndex < TemplateLaunchHistoryEntries.Num()
? &TemplateLaunchHistoryEntries[SelectedTemplateLaunchHistoryIndex]
: nullptr;
const FString TemplateLaunchHistoryStatusLine =
SelectedTemplateLaunchHistoryEntry != nullptr
? FString::Printf(
TEXT("Template launch history: %d entries | selected %d/%d | Template: %s | State: %s | Completed: %s"),
TemplateLaunchHistoryEntries.Num(),
SelectedTemplateLaunchHistoryIndex + 1,
TemplateLaunchHistoryEntries.Num(),
SelectedTemplateLaunchHistoryEntry->TemplateId.IsEmpty()
? TEXT("n/a")
: *SelectedTemplateLaunchHistoryEntry->TemplateId,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(
SelectedTemplateLaunchHistoryEntry->SessionState),
SelectedTemplateLaunchHistoryEntry->CompletedAtUtc.IsEmpty()
? TEXT("n/a")
: *SelectedTemplateLaunchHistoryEntry->CompletedAtUtc)
: TEXT("Template launch history: no completed template-backed runs retained yet.");
const FString TemplateLaunchHistoryDetailLine =
SelectedTemplateLaunchHistoryEntry != nullptr
? FString::Printf(
TEXT("Title: %s | kind: %s | mode: %s | selection: %s | deck: %s | user: %s | source plan: %s | recommended %d | focus %d | attempts %d | success %.2f | mistakes %d | review-backed: %s | coach-review: %s | short set: %s"),
SelectedTemplateLaunchHistoryEntry->TemplateTitle.IsEmpty()
? TEXT("n/a")
: *SelectedTemplateLaunchHistoryEntry->TemplateTitle,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(
SelectedTemplateLaunchHistoryEntry->TemplateKind),
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(
SelectedTemplateLaunchHistoryEntry->SuggestedMode),
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(
SelectedTemplateLaunchHistoryEntry->SuggestedSelectionPolicy),
SelectedTemplateLaunchHistoryEntry->FocusDeckId.IsEmpty()
? TEXT("n/a")
: *SelectedTemplateLaunchHistoryEntry->FocusDeckId,
SelectedTemplateLaunchHistoryEntry->UserId.IsEmpty()
? TEXT("n/a")
: *SelectedTemplateLaunchHistoryEntry->UserId,
SelectedTemplateLaunchHistoryEntry->SourceReviewPlanId.IsEmpty()
? TEXT("none")
: *SelectedTemplateLaunchHistoryEntry->SourceReviewPlanId,
SelectedTemplateLaunchHistoryEntry->RecommendedCaseCount,
SelectedTemplateLaunchHistoryEntry->FocusCaseCount,
SelectedTemplateLaunchHistoryEntry->CompletedAttemptCount,
SelectedTemplateLaunchHistoryEntry->SuccessRate,
SelectedTemplateLaunchHistoryEntry->TotalMistakes,
SelectedTemplateLaunchHistoryEntry->bPreferReviewRun ? TEXT("yes") : TEXT("no"),
SelectedTemplateLaunchHistoryEntry->bCoachReview ? TEXT("yes") : TEXT("no"),
SelectedTemplateLaunchHistoryEntry->bPreferShortReviewSet ? TEXT("yes") : TEXT("no"))
: TEXT("Template launch history detail: completed runs started through the dashboard template lane will accumulate here with template id, template kind, and source review-plan provenance.");
const bool bCanUseCoachHandoff = !bHasLiveAttemptTimer && !bHasActiveRun;
FString CoachHandoffStatusLine;
if (bHasLiveAttemptTimer)
@ -14958,16 +15250,35 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
if (bHasDisplayedRunRecap)
{
RunRecapStatusTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Source: %s | Session: %s | Deck: %s | State: %s | Completed at: %s"),
*RunRecapSourceLabel,
*DisplayedRunRecap->TrainingSessionId,
DisplayedRunRecap->DeckId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->DeckId,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedRunRecap->SessionState),
DisplayedRunRecap->LastCompletedAtUtc.IsEmpty()
? TEXT("n/a")
: *DisplayedRunRecap->LastCompletedAtUtc
)));
RunRecapStatusTextBlock->SetText(FText::FromString(
DisplayedTemplateLaunch != nullptr
? FString::Printf(
TEXT("Source: %s | Session: %s | Deck: %s | State: %s | Completed at: %s | Template: %s (%s) | Source plan: %s"),
*RunRecapSourceLabel,
*DisplayedRunRecap->TrainingSessionId,
DisplayedRunRecap->DeckId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->DeckId,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedRunRecap->SessionState),
DisplayedRunRecap->LastCompletedAtUtc.IsEmpty()
? TEXT("n/a")
: *DisplayedRunRecap->LastCompletedAtUtc,
DisplayedTemplateLaunch->TemplateId.IsEmpty()
? TEXT("n/a")
: *DisplayedTemplateLaunch->TemplateId,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(
DisplayedTemplateLaunch->TemplateKind),
DisplayedTemplateLaunch->SourceReviewPlanId.IsEmpty()
? TEXT("none")
: *DisplayedTemplateLaunch->SourceReviewPlanId)
: FString::Printf(
TEXT("Source: %s | Session: %s | Deck: %s | State: %s | Completed at: %s"),
*RunRecapSourceLabel,
*DisplayedRunRecap->TrainingSessionId,
DisplayedRunRecap->DeckId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->DeckId,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(DisplayedRunRecap->SessionState),
DisplayedRunRecap->LastCompletedAtUtc.IsEmpty()
? TEXT("n/a")
: *DisplayedRunRecap->LastCompletedAtUtc)
));
}
else if (bHasActiveRun)
{
@ -14986,23 +15297,51 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
if (bHasDisplayedRunRecap)
{
RunRecapDetailTextBlock->SetText(FText::FromString(FString::Printf(
TEXT("Attempts %d/%d | Success %d | Failure %d | Timeout %d | DNF %d | Aborted %d | Success rate %.2f | Best %d ms | Avg %d ms | Avg raw %d ms | Avg inspection %d ms | Mistakes %d | Last case %s"),
DisplayedRunRecap->CompletedAttemptCount,
DisplayedRunRecap->AttemptCount,
DisplayedRunRecap->SuccessCount,
DisplayedRunRecap->FailureCount,
DisplayedRunRecap->TimeoutCount,
DisplayedRunRecap->DNFCount,
DisplayedRunRecap->AbortedCount,
DisplayedRunRecap->SuccessRate,
DisplayedRunRecap->BestTotalTimeMs,
DisplayedRunRecap->AverageTotalTimeMs,
DisplayedRunRecap->AverageRawSolveTimeMs,
DisplayedRunRecap->AverageInspectionTimeMs,
DisplayedRunRecap->TotalMistakes,
DisplayedRunRecap->LastCaseId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->LastCaseId
)));
RunRecapDetailTextBlock->SetText(FText::FromString(
DisplayedTemplateLaunch != nullptr
? FString::Printf(
TEXT("Attempts %d/%d | Success %d | Failure %d | Timeout %d | DNF %d | Aborted %d | Success rate %.2f | Best %d ms | Avg %d ms | Avg raw %d ms | Avg inspection %d ms | Mistakes %d | Last case %s | Template title %s | Template mode %s | Template selection %s | Recommended %d | Focus %d | Review-backed %s"),
DisplayedRunRecap->CompletedAttemptCount,
DisplayedRunRecap->AttemptCount,
DisplayedRunRecap->SuccessCount,
DisplayedRunRecap->FailureCount,
DisplayedRunRecap->TimeoutCount,
DisplayedRunRecap->DNFCount,
DisplayedRunRecap->AbortedCount,
DisplayedRunRecap->SuccessRate,
DisplayedRunRecap->BestTotalTimeMs,
DisplayedRunRecap->AverageTotalTimeMs,
DisplayedRunRecap->AverageRawSolveTimeMs,
DisplayedRunRecap->AverageInspectionTimeMs,
DisplayedRunRecap->TotalMistakes,
DisplayedRunRecap->LastCaseId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->LastCaseId,
DisplayedTemplateLaunch->Title.IsEmpty()
? TEXT("n/a")
: *DisplayedTemplateLaunch->Title,
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(
DisplayedTemplateLaunch->SuggestedMode),
*HyperTwistCoachDashboardWidgetInternal::EnumDisplayName(
DisplayedTemplateLaunch->SuggestedSelectionPolicy),
DisplayedTemplateLaunch->RecommendedCaseCount,
DisplayedTemplateLaunch->FocusCaseIds.Num(),
bDisplayedTemplateLaunchUsesReviewLane ? TEXT("yes") : TEXT("no"))
: FString::Printf(
TEXT("Attempts %d/%d | Success %d | Failure %d | Timeout %d | DNF %d | Aborted %d | Success rate %.2f | Best %d ms | Avg %d ms | Avg raw %d ms | Avg inspection %d ms | Mistakes %d | Last case %s"),
DisplayedRunRecap->CompletedAttemptCount,
DisplayedRunRecap->AttemptCount,
DisplayedRunRecap->SuccessCount,
DisplayedRunRecap->FailureCount,
DisplayedRunRecap->TimeoutCount,
DisplayedRunRecap->DNFCount,
DisplayedRunRecap->AbortedCount,
DisplayedRunRecap->SuccessRate,
DisplayedRunRecap->BestTotalTimeMs,
DisplayedRunRecap->AverageTotalTimeMs,
DisplayedRunRecap->AverageRawSolveTimeMs,
DisplayedRunRecap->AverageInspectionTimeMs,
DisplayedRunRecap->TotalMistakes,
DisplayedRunRecap->LastCaseId.IsEmpty() ? TEXT("n/a") : *DisplayedRunRecap->LastCaseId)
));
}
else
{
@ -15143,6 +15482,39 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
{
TemplateLaunchDetailTextBlock->SetText(FText::FromString(TemplateLaunchDetailLine));
}
if (TemplateLaunchHistoryHeaderTextBlock != nullptr)
{
TemplateLaunchHistoryHeaderTextBlock->SetText(FText::FromString(
TEXT("[Template Launch History]")));
}
if (TemplateLaunchHistoryStatusTextBlock != nullptr)
{
TemplateLaunchHistoryStatusTextBlock->SetText(FText::FromString(
TemplateLaunchHistoryStatusLine));
}
if (TemplateLaunchHistoryDetailTextBlock != nullptr)
{
TemplateLaunchHistoryDetailTextBlock->SetText(FText::FromString(
TemplateLaunchHistoryDetailLine));
}
if (PreviousTemplateLaunchHistoryButtonLabel != nullptr)
{
PreviousTemplateLaunchHistoryButtonLabel->SetText(FText::FromString(
TEXT("Prev Template Launch")));
}
if (PreviousTemplateLaunchHistoryButton != nullptr)
{
PreviousTemplateLaunchHistoryButton->SetIsEnabled(TemplateLaunchHistoryEntries.Num() > 1);
}
if (NextTemplateLaunchHistoryButtonLabel != nullptr)
{
NextTemplateLaunchHistoryButtonLabel->SetText(FText::FromString(
TEXT("Next Template Launch")));
}
if (NextTemplateLaunchHistoryButton != nullptr)
{
NextTemplateLaunchHistoryButton->SetIsEnabled(TemplateLaunchHistoryEntries.Num() > 1);
}
if (HandoffHistoryHeaderTextBlock != nullptr)
{
HandoffHistoryHeaderTextBlock->SetText(FText::FromString(TEXT("[Handoff History]")));

View file

@ -372,6 +372,40 @@ struct FHyperTwistCoachDashboardHandoffHistoryEntry
}
};
struct FHyperTwistCoachDashboardTemplateLaunchHistoryEntry
{
FString RecordedAtUtc;
FString SourceSessionId;
FString TemplateId;
FString TemplateTitle;
FString UserId;
FString FocusDeckId;
FString SourceReviewPlanId;
EHyperTwistTrainingSessionTemplateKind TemplateKind =
EHyperTwistTrainingSessionTemplateKind::Recovery;
EHyperTwistTrainingSessionState SessionState =
EHyperTwistTrainingSessionState::Created;
EHyperTwistTrainingDeliveryMode SuggestedMode = EHyperTwistTrainingDeliveryMode::Timer;
EHyperTwistTrainingSelectionPolicy SuggestedSelectionPolicy =
EHyperTwistTrainingSelectionPolicy::Weighted;
int32 RecommendedCaseCount = 0;
int32 FocusCaseCount = 0;
int32 CompletedAttemptCount = 0;
int32 TotalMistakes = 0;
float SuccessRate = 0.0f;
FString CompletedAtUtc;
bool bPreferReviewRun = false;
bool bCoachReview = false;
bool bPreferShortReviewSet = false;
bool IsStructurallyValid() const
{
return !RecordedAtUtc.IsEmpty()
&& !SourceSessionId.IsEmpty()
&& !TemplateId.IsEmpty();
}
};
struct FHyperTwistCoachDashboardMethodDrillFollowUpHistoryEntry
{
FString RecordedAtUtc;
@ -560,6 +594,9 @@ public:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard")
int32 SelectedHandoffHistoryIndex = INDEX_NONE;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard")
int32 SelectedTemplateLaunchHistoryIndex = INDEX_NONE;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "HyperTwist|Training|Coach|Dashboard")
int32 SelectedQueueSuppressionIndex = INDEX_NONE;
@ -671,6 +708,12 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool SelectNextHandoffHistoryEntry();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool SelectPreviousTemplateLaunchHistoryEntry();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool SelectNextTemplateLaunchHistoryEntry();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach|Dashboard")
bool SelectPreviousQueueSuppressionHistoryEntry();
@ -839,6 +882,15 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TemplateLaunchDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TemplateLaunchHistoryHeaderTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TemplateLaunchHistoryStatusTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> TemplateLaunchHistoryDetailTextBlock = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> HandoffHistoryHeaderTextBlock = nullptr;
@ -860,6 +912,18 @@ protected:
UPROPERTY(Transient)
TObjectPtr<UTextBlock> NextHandoffHistoryButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> PreviousTemplateLaunchHistoryButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> PreviousTemplateLaunchHistoryButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UButton> NextTemplateLaunchHistoryButton = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> NextTemplateLaunchHistoryButtonLabel = nullptr;
UPROPERTY(Transient)
TObjectPtr<UTextBlock> GuidanceReviewHeaderTextBlock = nullptr;
@ -1409,6 +1473,12 @@ protected:
UFUNCTION()
void HandleNextHandoffHistoryClicked();
UFUNCTION()
void HandlePreviousTemplateLaunchHistoryClicked();
UFUNCTION()
void HandleNextTemplateLaunchHistoryClicked();
UFUNCTION()
void HandlePreviousQueueSuppressionClicked();
@ -1495,6 +1565,7 @@ private:
void SyncSelectedGuidancePreviewCase();
void SyncSelectedGuidanceDecisionHistoryEntry();
void SyncSelectedHandoffHistoryEntry();
void SyncSelectedTemplateLaunchHistoryEntry();
void SyncSelectedQueueSuppressionHistoryEntry();
void SyncSelectedClosureRecoveryHistoryEntry();
void SyncSelectedMethodDrillFollowUpHistoryEntry();
@ -1578,6 +1649,7 @@ private:
);
void RecordQueueSuppressionSnapshot();
void RecordClosureRecoverySnapshot();
void RecordTemplateLaunchHistoryEntry(const FHyperTwistTrainingSessionSummary& CompletedRunRecap);
void RecordMethodDrillFollowUpHistoryEntry(const FHyperTwistTrainingSessionSummary& CompletedRunRecap);
void RecordMethodDrillRecoveryMemoryHistorySnapshot(
const FHyperTwistCoachDashboardQueueRecoveryWeighting& QueueRecoveryWeighting,
@ -1603,6 +1675,10 @@ private:
bool bSingleCaseLaunch,
const FString& SelectedCaseId
);
void CaptureActiveTemplateLaunchContext(
const FString& SessionId,
const FHyperTwistTrainingSessionTemplate& SessionTemplate
);
void CaptureActiveMethodDrillFollowUpLaunchContext(const FString& SessionId);
bool IsLiveAttemptInspectionPhase() const;
int32 GetMaxManualSplitCaptureCount() const;
@ -1619,6 +1695,11 @@ private:
FString RetainedRunRecapUserId;
FString RetainedRunRecapSessionId;
EHyperTwistTrainingDeliveryMode RetainedRunRecapMode = EHyperTwistTrainingDeliveryMode::Timer;
FString ActiveTemplateLaunchSessionId;
FHyperTwistTrainingSessionTemplate ActiveTemplateLaunchTemplate;
bool bHasRetainedTemplateLaunch = false;
FString RetainedTemplateLaunchSessionId;
FHyperTwistTrainingSessionTemplate RetainedTemplateLaunchTemplate;
FString ActiveGuidanceLaunchSessionId;
EHyperTwistCoachDashboardGuidancePreviewLane ActiveGuidanceLaunchLane =
EHyperTwistCoachDashboardGuidancePreviewLane::None;
@ -1642,6 +1723,7 @@ private:
FString RetainedGuidanceComparisonSelectedCaseId;
TArray<FHyperTwistCoachDashboardDecisionHistoryEntry> GuidanceDecisionHistoryEntries;
TArray<FHyperTwistCoachDashboardHandoffHistoryEntry> HandoffHistoryEntries;
TArray<FHyperTwistCoachDashboardTemplateLaunchHistoryEntry> TemplateLaunchHistoryEntries;
TArray<FHyperTwistCoachDashboardMethodDrillFollowUpHistoryEntry> MethodDrillFollowUpHistoryEntries;
TArray<FHyperTwistCoachDashboardMethodDrillRecoveryMemoryHistoryEntry>
MethodDrillRecoveryMemoryHistoryEntries;

View file

@ -0,0 +1,99 @@
# HyperTwist Phase 4 template launch provenance packet
Created on `2026-05-06`
Status:
- first-party HyperTwist packet
- bounded Phase `4` dashboard template provenance slice
## Purpose
This packet captures and surfaces template-launch provenance for completed runs started through the dashboard template lane.
The open tasks were:
- remember which active session template launched a completed dashboard run
- retain the template `TemplateId`, `TemplateKind`, and `SourceReviewPlanId` alongside the run recap
- add a first-party dashboard history surface so repeated template launches can be audited
It is not:
- a repository schema migration packet
- a template browser or template editing packet
- a new run-restart or replay semantics packet
- a broader dashboard history redesign
## Scope
Bounded lane:
- capture active template launch context when `StartPreferredActiveSessionTemplateRun()` succeeds
- carry that context through `SyncRetainedRunRecap()` when the run completes or aborts
- record retained template-launch history snapshots with run outcome fields
- append template provenance to the run recap display when the displayed recap came from a template-backed launch
- add a dedicated `[Template Launch History]` section with simple previous/next navigation
Out of scope:
- changing repository persistence for run summaries
- altering template selection priority
- changing method-drill follow-up weighting logic
- changing coach handoff recommendation rules
## Why this was the right next packet
The previous packet made the template-backed launch lane visible and clickable from the first-party dashboard.
That still left one practical gap:
- once a template-backed run finished, the recap no longer explained which template produced it
- repeated template launches had no retained audit trail on the dashboard itself
- review-preferring template outcomes could not be compared from the same first-party surface that launched them
The next honest move was therefore:
- capture template provenance at launch time
- retain it only through the dashboard recap/history path
- and keep the change narrowly scoped to first-party dashboard state and presentation
## What landed
Primary code changes:
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistCoachDashboardWidget.h`
- added retained template-launch history entry shape and selected-history index
- added active/retained template launch context members
- added dashboard text/button members and handlers for a new template-launch history section
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistCoachDashboardWidget.cpp`
- captures template launch context when the preferred dashboard template run starts
- records template launch provenance when the matching run recap completes or aborts
- retains up to sixteen template launch history snapshots
- appends template provenance into the displayed run recap when applicable
- adds a `[Template Launch History]` status/detail section with previous/next navigation
## Product effect
The first-party dashboard now keeps template-backed run provenance attached to the run outcome:
- run recap shows the template id, template kind, and source review-plan id for matching template launches
- the dashboard retains a small history of completed template-backed runs and their outcome summaries
- operators can page through recent template launches without leaving the dashboard
- review-preferring template runs are now auditable from launch through recap on the same first-party surface
## Acceptance criteria
- successful dashboard template launches capture template provenance
- completed or aborted template-backed runs retain that provenance in recap state
- dashboard shows template provenance in run recap when the displayed run came from a template launch
- dashboard exposes retained template-launch history with previous/next navigation
- full product build succeeds
## Validation checklist
1. build `UnrealHyperTwist.sln` / `UnrealHyperTwistEditor`
2. launch a run through the dashboard template button
3. complete or abort the run and confirm run recap shows template provenance
4. confirm the dashboard `[Template Launch History]` section records the completed run
That is the packet.