Harden website proof surfaces and native input diagnostics
This commit is contained in:
parent
a1d40d1232
commit
2967c6ee44
41 changed files with 4940 additions and 188 deletions
1949
Content/Browser/package-lock.json
generated
Normal file
1949
Content/Browser/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -12791,6 +12791,15 @@ UHyperTwistCoachDashboardWidget::GetDisplayedBrowserRuntimeInspectSurface() cons
|
|||
return Surface;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingControlInputReadinessInspectSurface
|
||||
UHyperTwistCoachDashboardWidget::GetDisplayedControlInputReadinessInspectSurface() const
|
||||
{
|
||||
FHyperTwistTrainingControlInputReadinessInspectSurface Surface =
|
||||
UHyperTwistTrainingPanelWidget::GetDisplayedControlInputReadinessInspectSurface();
|
||||
Surface.Headline = TEXT("Control and input readiness");
|
||||
return Surface;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingCoachDashboardGuidanceRationaleInspectSurface
|
||||
UHyperTwistCoachDashboardWidget::GetDisplayedGuidanceRationaleInspectSurface() const
|
||||
{
|
||||
|
|
@ -19109,6 +19118,24 @@ void UHyperTwistCoachDashboardWidget::EnsureDefaultDashboardBuilt()
|
|||
TEXT("CoachBrowserRuntimeDetail"),
|
||||
8
|
||||
);
|
||||
ControlInputReadinessHeaderTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
|
||||
WidgetTree,
|
||||
RootLayout,
|
||||
TEXT("CoachControlInputReadinessHeader"),
|
||||
2
|
||||
);
|
||||
ControlInputReadinessStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
|
||||
WidgetTree,
|
||||
RootLayout,
|
||||
TEXT("CoachControlInputReadinessStatus"),
|
||||
2
|
||||
);
|
||||
ControlInputReadinessDetailTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
|
||||
WidgetTree,
|
||||
RootLayout,
|
||||
TEXT("CoachControlInputReadinessDetail"),
|
||||
8
|
||||
);
|
||||
RecognitionStatusTextBlock = HyperTwistCoachDashboardWidgetInternal::AddTextRow(
|
||||
WidgetTree,
|
||||
RootLayout,
|
||||
|
|
@ -30163,6 +30190,23 @@ void UHyperTwistCoachDashboardWidget::UpdateDashboardPresentation()
|
|||
{
|
||||
BrowserRuntimeDetailTextBlock->SetText(FText::FromString(BrowserRuntimeSurface.DetailLine));
|
||||
}
|
||||
const FHyperTwistTrainingControlInputReadinessInspectSurface ControlInputSurface =
|
||||
GetDisplayedControlInputReadinessInspectSurface();
|
||||
if (ControlInputReadinessHeaderTextBlock != nullptr)
|
||||
{
|
||||
ControlInputReadinessHeaderTextBlock->SetText(
|
||||
FText::FromString(TEXT("[Control and Input Readiness]")));
|
||||
}
|
||||
if (ControlInputReadinessStatusTextBlock != nullptr)
|
||||
{
|
||||
ControlInputReadinessStatusTextBlock->SetText(
|
||||
FText::FromString(ControlInputSurface.StatusLine));
|
||||
}
|
||||
if (ControlInputReadinessDetailTextBlock != nullptr)
|
||||
{
|
||||
ControlInputReadinessDetailTextBlock->SetText(
|
||||
FText::FromString(ControlInputSurface.DetailLine));
|
||||
}
|
||||
if (VerificationStatusTextBlock != nullptr)
|
||||
{
|
||||
DisplayedVerificationStatusLine = CachedCoachPanelState.VerificationStatusLine;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,118 @@
|
|||
#include "HyperTwistTraining/HyperTwistTrainingPanelWidget.h"
|
||||
|
||||
#include "HyperTwistAlgorithm/HyperTwistAlgorithmKeyboard.h"
|
||||
#include "HyperTwistBrowser/HyperTwistBrowserWidget.h"
|
||||
#include "HyperTwistSimulation/HyperTwistClassicCubePlayerController.h"
|
||||
#include "HyperTwistSimulation/HyperTwistVirtual3333ProjectionPlayerController.h"
|
||||
#include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h"
|
||||
#include "Misc/ConfigCacheIni.h"
|
||||
|
||||
namespace HyperTwistTrainingPanelWidgetInternal
|
||||
{
|
||||
struct FProjectInputGroundworkFacts
|
||||
{
|
||||
bool bEnhancedInputProjectGroundworkPresent = false;
|
||||
bool bMotionControllerGroundworkPresent = false;
|
||||
int32 MotionControllerFamilyCount = 0;
|
||||
TArray<FString> MotionControllerFamilies;
|
||||
};
|
||||
|
||||
FString DescribeBool(const bool bValue)
|
||||
{
|
||||
return bValue ? TEXT("yes") : TEXT("no");
|
||||
}
|
||||
|
||||
bool AxisConfigEntriesContainToken(
|
||||
const TArray<FString>& AxisConfigEntries,
|
||||
const TCHAR* Token
|
||||
)
|
||||
{
|
||||
for (const FString& Entry : AxisConfigEntries)
|
||||
{
|
||||
if (Entry.Contains(Token))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
FProjectInputGroundworkFacts BuildProjectInputGroundworkFacts()
|
||||
{
|
||||
FProjectInputGroundworkFacts Facts;
|
||||
|
||||
if (GConfig == nullptr)
|
||||
{
|
||||
return Facts;
|
||||
}
|
||||
|
||||
static constexpr TCHAR InputSettingsSection[] = TEXT("/Script/Engine.InputSettings");
|
||||
|
||||
FString DefaultPlayerInputClass;
|
||||
FString DefaultInputComponentClass;
|
||||
GConfig->GetString(
|
||||
InputSettingsSection,
|
||||
TEXT("DefaultPlayerInputClass"),
|
||||
DefaultPlayerInputClass,
|
||||
GInputIni
|
||||
);
|
||||
GConfig->GetString(
|
||||
InputSettingsSection,
|
||||
TEXT("DefaultInputComponentClass"),
|
||||
DefaultInputComponentClass,
|
||||
GInputIni
|
||||
);
|
||||
|
||||
Facts.bEnhancedInputProjectGroundworkPresent =
|
||||
DefaultPlayerInputClass == TEXT("/Script/EnhancedInput.EnhancedPlayerInput")
|
||||
&& DefaultInputComponentClass == TEXT("/Script/EnhancedInput.EnhancedInputComponent");
|
||||
|
||||
GConfig->GetBool(
|
||||
InputSettingsSection,
|
||||
TEXT("bEnableMotionControls"),
|
||||
Facts.bMotionControllerGroundworkPresent,
|
||||
GInputIni
|
||||
);
|
||||
|
||||
TArray<FString> AxisConfigEntries;
|
||||
GConfig->GetArray(
|
||||
InputSettingsSection,
|
||||
TEXT("AxisConfig"),
|
||||
AxisConfigEntries,
|
||||
GInputIni
|
||||
);
|
||||
|
||||
const struct
|
||||
{
|
||||
const TCHAR* Token;
|
||||
const TCHAR* Label;
|
||||
} MotionControllerFamilies[] = {
|
||||
{TEXT("Vive_"), TEXT("Vive")},
|
||||
{TEXT("MixedReality_"), TEXT("Mixed Reality")},
|
||||
{TEXT("OculusTouch_"), TEXT("Oculus Touch")},
|
||||
{TEXT("ValveIndex_"), TEXT("Valve Index")}
|
||||
};
|
||||
|
||||
for (const auto& Family : MotionControllerFamilies)
|
||||
{
|
||||
if (AxisConfigEntriesContainToken(AxisConfigEntries, Family.Token))
|
||||
{
|
||||
Facts.MotionControllerFamilies.Add(Family.Label);
|
||||
}
|
||||
}
|
||||
|
||||
Facts.MotionControllerFamilyCount = Facts.MotionControllerFamilies.Num();
|
||||
return Facts;
|
||||
}
|
||||
|
||||
const FProjectInputGroundworkFacts& GetProjectInputGroundworkFacts()
|
||||
{
|
||||
static const FProjectInputGroundworkFacts CachedFacts =
|
||||
BuildProjectInputGroundworkFacts();
|
||||
return CachedFacts;
|
||||
}
|
||||
|
||||
FString ResolveCoachHandoffStartActionSourceLabel(
|
||||
const FHyperTwistTrainingCoachPanelState& PanelState,
|
||||
const EHyperTwistTrainingCoachHandoffKind HandoffKind,
|
||||
|
|
@ -657,6 +765,137 @@ UHyperTwistTrainingPanelWidget::GetDisplayedBrowserRuntimeInspectSurface() const
|
|||
return Surface;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingControlInputReadinessInspectSurface
|
||||
UHyperTwistTrainingPanelWidget::GetDisplayedControlInputReadinessInspectSurface() const
|
||||
{
|
||||
FHyperTwistTrainingControlInputReadinessInspectSurface Surface;
|
||||
Surface.Headline = TEXT("Control and input readiness");
|
||||
Surface.KeyboardProfileName =
|
||||
UHyperTwistAlgorithmKeyboardLibrary::GetDefaultKeyboardProfileName();
|
||||
Surface.bClassicKeyboardProfileReady = !Surface.KeyboardProfileName.IsEmpty();
|
||||
|
||||
const AHyperTwistClassicCubePlayerController* ClassicCubeDefaults =
|
||||
GetDefault<AHyperTwistClassicCubePlayerController>();
|
||||
Surface.bClassicCubeMouseInputReady = ClassicCubeDefaults != nullptr
|
||||
&& ClassicCubeDefaults->bShowMouseCursor;
|
||||
Surface.bClassicCubeTouchInputReady =
|
||||
ClassicCubeDefaults != nullptr && ClassicCubeDefaults->bEnableTouchTurnInput;
|
||||
Surface.bClassicCubeShortcutReady = ClassicCubeDefaults != nullptr
|
||||
&& ClassicCubeDefaults->bBindFreshAttemptShortcut
|
||||
&& ClassicCubeDefaults->bBindHintShortcut
|
||||
&& ClassicCubeDefaults->bBindSubmitSolveShortcut
|
||||
&& ClassicCubeDefaults->bBindModeToggleShortcut
|
||||
&& ClassicCubeDefaults->bBindVoiceHoldShortcut
|
||||
&& ClassicCubeDefaults->bBindVoiceCycleShortcut;
|
||||
|
||||
const AHyperTwistVirtual3333ProjectionPlayerController* HigherDimensionalDefaults =
|
||||
GetDefault<AHyperTwistVirtual3333ProjectionPlayerController>();
|
||||
Surface.bHigherDimensionalKeyboardInputReady =
|
||||
HigherDimensionalDefaults != nullptr && HigherDimensionalDefaults->bUseGameAndUiInputMode;
|
||||
|
||||
const FHyperTwistTrainingHigherDimensionalRuntimeHostCatalog HostCatalog =
|
||||
UHyperTwistTrainingRuntimeLibrary::GetBundledHigherDimensionalRuntimeHostCatalog();
|
||||
const FHyperTwistTrainingHigherDimensionalRuntimeViewContextCatalog ViewContextCatalog =
|
||||
UHyperTwistTrainingRuntimeLibrary::GetBundledHigherDimensionalRuntimeViewContextCatalog();
|
||||
const FHyperTwistTrainingHigherDimensionalRuntimeSessionCatalog SessionCatalog =
|
||||
UHyperTwistTrainingRuntimeLibrary::GetBundledHigherDimensionalRuntimeSessionCatalog();
|
||||
const FHyperTwistTrainingHigherDimensionalInteractiveSceneCatalog SceneCatalog =
|
||||
UHyperTwistTrainingRuntimeLibrary::GetBundledHigherDimensionalInteractiveSceneCatalog();
|
||||
Surface.bHigherDimensionalDedicatedFamilyReady =
|
||||
HostCatalog.IsStructurallyValid()
|
||||
&& ViewContextCatalog.IsStructurallyValid()
|
||||
&& SessionCatalog.IsStructurallyValid()
|
||||
&& SceneCatalog.IsStructurallyValid();
|
||||
|
||||
const HyperTwistTrainingPanelWidgetInternal::FProjectInputGroundworkFacts& InputGroundworkFacts =
|
||||
HyperTwistTrainingPanelWidgetInternal::GetProjectInputGroundworkFacts();
|
||||
Surface.bEnhancedInputProjectGroundworkPresent =
|
||||
InputGroundworkFacts.bEnhancedInputProjectGroundworkPresent;
|
||||
Surface.bMotionControllerGroundworkPresent =
|
||||
InputGroundworkFacts.bMotionControllerGroundworkPresent
|
||||
&& InputGroundworkFacts.MotionControllerFamilyCount > 0;
|
||||
Surface.MotionControllerFamilyCount = InputGroundworkFacts.MotionControllerFamilyCount;
|
||||
|
||||
FHyperTwistTrainingImmersivePresenceControlContract PresenceControlContract;
|
||||
Surface.bImmersivePresenceContractReady =
|
||||
UHyperTwistTrainingRuntimeLibrary::TryGetBundledImmersivePresenceControlContract(
|
||||
TEXT("immersive-training-presence-control-contract"),
|
||||
PresenceControlContract
|
||||
);
|
||||
|
||||
Surface.bFinishedXrRuntimeReady = false;
|
||||
Surface.bFinishedPreferencesReady = false;
|
||||
|
||||
Surface.SummaryLine =
|
||||
TEXT("Keyboard and mouse lanes are ready today; XR/controller groundwork exists but remains unfinished.");
|
||||
Surface.ClassicCubeStatusLine = FString::Printf(
|
||||
TEXT("Classic cube: mouse %s | touch %s | shortcuts %s."),
|
||||
*HyperTwistTrainingPanelWidgetInternal::DescribeBool(
|
||||
Surface.bClassicCubeMouseInputReady),
|
||||
*HyperTwistTrainingPanelWidgetInternal::DescribeBool(
|
||||
Surface.bClassicCubeTouchInputReady),
|
||||
*HyperTwistTrainingPanelWidgetInternal::DescribeBool(
|
||||
Surface.bClassicCubeShortcutReady)
|
||||
);
|
||||
Surface.KeyboardProfileLine = FString::Printf(
|
||||
TEXT("Keyboard profile: %s is the shipped classic mapping."),
|
||||
*Surface.KeyboardProfileName
|
||||
);
|
||||
Surface.HigherDimensionalStatusLine = FString::Printf(
|
||||
TEXT("Higher-dimensional: keyboard-driven slice/layer control %s | dedicated-family runtime catalogs %s | scene surfaces %d."),
|
||||
*HyperTwistTrainingPanelWidgetInternal::DescribeBool(
|
||||
Surface.bHigherDimensionalKeyboardInputReady),
|
||||
*HyperTwistTrainingPanelWidgetInternal::DescribeBool(
|
||||
Surface.bHigherDimensionalDedicatedFamilyReady),
|
||||
SceneCatalog.SceneSurfaces.Num()
|
||||
);
|
||||
const FString MotionControllerFamilyLabels =
|
||||
InputGroundworkFacts.MotionControllerFamilies.Num() > 0
|
||||
? FString::Join(InputGroundworkFacts.MotionControllerFamilies, TEXT(", "))
|
||||
: FString(TEXT("none"));
|
||||
Surface.XrStatusLine = FString::Printf(
|
||||
TEXT("XR groundwork: EnhancedInput config %s | motion controls %s | controller families %d (%s) | immersive presence contract %s | finished XR runtime not yet shipped."),
|
||||
*HyperTwistTrainingPanelWidgetInternal::DescribeBool(
|
||||
Surface.bEnhancedInputProjectGroundworkPresent),
|
||||
*HyperTwistTrainingPanelWidgetInternal::DescribeBool(
|
||||
Surface.bMotionControllerGroundworkPresent),
|
||||
Surface.MotionControllerFamilyCount,
|
||||
*MotionControllerFamilyLabels,
|
||||
*HyperTwistTrainingPanelWidgetInternal::DescribeBool(
|
||||
Surface.bImmersivePresenceContractReady)
|
||||
);
|
||||
Surface.PreferencesStatusLine =
|
||||
TEXT("Preferences: polished user-facing rebinding and broader control-settings ownership are not yet shipped.");
|
||||
Surface.NextPacketLine =
|
||||
TEXT("Next native packet: explicit XR host/plugin decision, dedicated headset/controller runtime owners, user-facing settings/rebinding, and Windows package validation.");
|
||||
Surface.StatusLine = FString::Printf(
|
||||
TEXT("classic cube %s | higher-dimensional %s | XR groundwork %s | finished VR/preferences not yet shipped"),
|
||||
Surface.bClassicCubeMouseInputReady
|
||||
&& Surface.bClassicCubeTouchInputReady
|
||||
&& Surface.bClassicCubeShortcutReady
|
||||
? TEXT("ready")
|
||||
: TEXT("partial"),
|
||||
Surface.bHigherDimensionalKeyboardInputReady
|
||||
&& Surface.bHigherDimensionalDedicatedFamilyReady
|
||||
? TEXT("ready")
|
||||
: TEXT("partial"),
|
||||
Surface.bEnhancedInputProjectGroundworkPresent
|
||||
&& Surface.bMotionControllerGroundworkPresent
|
||||
&& Surface.bImmersivePresenceContractReady
|
||||
? TEXT("present")
|
||||
: TEXT("limited")
|
||||
);
|
||||
Surface.DetailLine = FString::Printf(
|
||||
TEXT("%s | %s | %s | %s | %s"),
|
||||
*Surface.ClassicCubeStatusLine,
|
||||
*Surface.KeyboardProfileLine,
|
||||
*Surface.HigherDimensionalStatusLine,
|
||||
*Surface.XrStatusLine,
|
||||
*Surface.PreferencesStatusLine
|
||||
);
|
||||
return Surface;
|
||||
}
|
||||
|
||||
FHyperTwistTrainingDeck UHyperTwistTrainingPanelWidget::BuildActiveCoachRecommendedDeck(const int32 MaxCases)
|
||||
{
|
||||
CachedCoachRecommendedDeck = UHyperTwistTrainingRuntimeLibrary::BuildActiveCoachRecommendedDeck(
|
||||
|
|
|
|||
|
|
@ -935,6 +935,9 @@ public:
|
|||
FHyperTwistTrainingBrowserRuntimeInspectSurface
|
||||
GetDisplayedBrowserRuntimeInspectSurface() const override;
|
||||
|
||||
FHyperTwistTrainingControlInputReadinessInspectSurface
|
||||
GetDisplayedControlInputReadinessInspectSurface() const override;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach|Dashboard")
|
||||
FHyperTwistTrainingCoachDashboardGuidanceRationaleInspectSurface
|
||||
GetDisplayedGuidanceRationaleInspectSurface() const;
|
||||
|
|
@ -1646,6 +1649,15 @@ protected:
|
|||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextBlock> BrowserRuntimeDetailTextBlock = nullptr;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextBlock> ControlInputReadinessHeaderTextBlock = nullptr;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextBlock> ControlInputReadinessStatusTextBlock = nullptr;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextBlock> ControlInputReadinessDetailTextBlock = nullptr;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UTextBlock> RecognitionStatusTextBlock = nullptr;
|
||||
|
||||
|
|
|
|||
|
|
@ -291,6 +291,10 @@ public:
|
|||
virtual FHyperTwistTrainingBrowserRuntimeInspectSurface
|
||||
GetDisplayedBrowserRuntimeInspectSurface() const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Input")
|
||||
virtual FHyperTwistTrainingControlInputReadinessInspectSurface
|
||||
GetDisplayedControlInputReadinessInspectSurface() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
|
||||
FHyperTwistTrainingDeck BuildActiveCoachRecommendedDeck(int32 MaxCases);
|
||||
|
||||
|
|
|
|||
|
|
@ -9977,6 +9977,95 @@ struct FHyperTwistTrainingBrowserRuntimeInspectSurface
|
|||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingControlInputReadinessInspectSurface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString Headline;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString SummaryLine;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString StatusLine;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString DetailLine;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString ClassicCubeStatusLine;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString KeyboardProfileLine;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString HigherDimensionalStatusLine;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString XrStatusLine;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString PreferencesStatusLine;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString NextPacketLine;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
FString KeyboardProfileName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
int32 MotionControllerFamilyCount = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bClassicCubeMouseInputReady = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bClassicCubeTouchInputReady = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bClassicCubeShortcutReady = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bClassicKeyboardProfileReady = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bHigherDimensionalKeyboardInputReady = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bHigherDimensionalDedicatedFamilyReady = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bEnhancedInputProjectGroundworkPresent = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bMotionControllerGroundworkPresent = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bImmersivePresenceContractReady = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bFinishedXrRuntimeReady = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist")
|
||||
bool bFinishedPreferencesReady = false;
|
||||
|
||||
bool IsStructurallyValid() const
|
||||
{
|
||||
return !SummaryLine.IsEmpty()
|
||||
|| !StatusLine.IsEmpty()
|
||||
|| !ClassicCubeStatusLine.IsEmpty()
|
||||
|| !HigherDimensionalStatusLine.IsEmpty()
|
||||
|| !XrStatusLine.IsEmpty()
|
||||
|| bClassicCubeMouseInputReady
|
||||
|| bHigherDimensionalKeyboardInputReady
|
||||
|| bEnhancedInputProjectGroundworkPresent
|
||||
|| bMotionControllerGroundworkPresent
|
||||
|| bImmersivePresenceContractReady;
|
||||
}
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FHyperTwistTrainingCoachDashboardQueueRecoveryStatusInspectSurface
|
||||
{
|
||||
|
|
|
|||
|
|
@ -995,6 +995,111 @@ bool FHyperTwistBrowserCoachDashboardRuntimeInspectSurfaceTest::RunTest(
|
|||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistTrainingPanelControlInputReadinessInspectSurfaceTest,
|
||||
"HyperTwist.Browser.TrainingPanel.ControlInputReadinessInspectSurface",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistTrainingPanelControlInputReadinessInspectSurfaceTest::RunTest(
|
||||
const FString& Parameters
|
||||
)
|
||||
{
|
||||
UHyperTwistTrainingPanelWidget* TrainingPanel = NewObject<UHyperTwistTrainingPanelWidget>();
|
||||
TestNotNull(TEXT("The training panel widget must be constructible."), TrainingPanel);
|
||||
if (TrainingPanel == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FHyperTwistTrainingControlInputReadinessInspectSurface Surface =
|
||||
TrainingPanel->GetDisplayedControlInputReadinessInspectSurface();
|
||||
TestTrue(
|
||||
TEXT("The control/input readiness surface must be structurally valid."),
|
||||
Surface.IsStructurallyValid()
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("The control/input readiness surface must keep a stable headline."),
|
||||
Surface.Headline,
|
||||
TEXT("Control and input readiness")
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("The control/input readiness surface must expose the shipped classic keyboard profile."),
|
||||
Surface.KeyboardProfileName,
|
||||
TEXT("classic-wca-keyboard/v1")
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The control/input readiness surface must report the shipped classic cube mouse/touch/shortcut lane."),
|
||||
Surface.bClassicCubeMouseInputReady
|
||||
&& Surface.bClassicCubeTouchInputReady
|
||||
&& Surface.bClassicCubeShortcutReady
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The control/input readiness surface must report the higher-dimensional keyboard lane and dedicated-family runtime catalogs."),
|
||||
Surface.bHigherDimensionalKeyboardInputReady
|
||||
&& Surface.bHigherDimensionalDedicatedFamilyReady
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The control/input readiness surface must report the current project-level input groundwork."),
|
||||
Surface.bEnhancedInputProjectGroundworkPresent
|
||||
&& Surface.bMotionControllerGroundworkPresent
|
||||
&& Surface.bImmersivePresenceContractReady
|
||||
);
|
||||
TestFalse(
|
||||
TEXT("The control/input readiness surface must keep unfinished XR/runtime and preferences truth explicit."),
|
||||
Surface.bFinishedXrRuntimeReady || Surface.bFinishedPreferencesReady
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The control/input readiness surface must say plainly that finished XR runtime is not yet shipped."),
|
||||
Surface.XrStatusLine.Contains(TEXT("not yet shipped"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The control/input readiness surface must point to the later settings/rebinding packet."),
|
||||
Surface.NextPacketLine.Contains(TEXT("settings/rebinding"))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistCoachDashboardControlInputReadinessInspectSurfaceTest,
|
||||
"HyperTwist.Browser.CoachDashboard.ControlInputReadinessInspectSurface",
|
||||
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
|
||||
)
|
||||
|
||||
bool FHyperTwistCoachDashboardControlInputReadinessInspectSurfaceTest::RunTest(
|
||||
const FString& Parameters
|
||||
)
|
||||
{
|
||||
UHyperTwistCoachDashboardWidget* CoachDashboard = NewObject<UHyperTwistCoachDashboardWidget>();
|
||||
TestNotNull(TEXT("The coach dashboard widget must be constructible."), CoachDashboard);
|
||||
if (CoachDashboard == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
CoachDashboard->RefreshCoachDashboardView();
|
||||
const FHyperTwistTrainingControlInputReadinessInspectSurface Surface =
|
||||
CoachDashboard->GetDisplayedControlInputReadinessInspectSurface();
|
||||
TestTrue(
|
||||
TEXT("The coach dashboard control/input readiness surface must stay structurally valid after a view refresh."),
|
||||
Surface.IsStructurallyValid()
|
||||
);
|
||||
TestEqual(
|
||||
TEXT("The coach dashboard control/input readiness surface must preserve the diagnostics headline."),
|
||||
Surface.Headline,
|
||||
TEXT("Control and input readiness")
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The coach dashboard control/input readiness surface must retain the keyboard profile fact in its detail line."),
|
||||
Surface.DetailLine.Contains(TEXT("classic-wca-keyboard/v1"))
|
||||
);
|
||||
TestTrue(
|
||||
TEXT("The coach dashboard control/input readiness surface must retain the unfinished XR/runtime truth in its detail line."),
|
||||
Surface.DetailLine.Contains(TEXT("not yet shipped"))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FHyperTwistBrowserWidgetAuthoritativeShellArtifactsTest,
|
||||
"HyperTwist.Browser.Widget.AuthoritativeShellArtifacts",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"reportVersion": "ht-higher-dimensional-package-validation/v1",
|
||||
"generatedAtUtc": "2026-06-22T01:43:08.7625247Z",
|
||||
"generatedAtUtc": "2026-06-23T10:27:31.6439999Z",
|
||||
"projectRoot": "C:/HyperTwist_worktrees/phase10validate",
|
||||
"archiveDirectory": "C:/HyperTwist_worktrees/phase10validate_packaged_phase6c_higherdim",
|
||||
"archiveDirectory": "C:/HyperTwist_worktrees/phase10validate_packaged_phase6c_higherdim_refresh_20260623",
|
||||
"configuration": "Development",
|
||||
"cookMaps": [
|
||||
"/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining",
|
||||
|
|
@ -50,13 +50,13 @@
|
|||
]
|
||||
},
|
||||
"result": "passed",
|
||||
"packagedExecutablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim\\Windows\\UnrealHyperTwist.exe",
|
||||
"packagedExecutablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim_refresh_20260623\\Windows\\UnrealHyperTwist.exe",
|
||||
"smokeReports": [
|
||||
{
|
||||
"reportVersion": "ht-higher-dimensional-package-smoke/v1",
|
||||
"generatedAtUtc": "2026-06-22T01:45:12.6335785Z",
|
||||
"packageRoot": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim",
|
||||
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim\\Windows\\UnrealHyperTwist.exe",
|
||||
"generatedAtUtc": "2026-06-23T10:28:44.7973129Z",
|
||||
"packageRoot": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim_refresh_20260623",
|
||||
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim_refresh_20260623\\Windows\\UnrealHyperTwist.exe",
|
||||
"mapUrl": "/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining",
|
||||
"smokeSeconds": 10,
|
||||
"resolution": {
|
||||
|
|
@ -65,16 +65,16 @@
|
|||
},
|
||||
"keepRunning": false,
|
||||
"result": "passed",
|
||||
"processId": 29436,
|
||||
"processId": 20320,
|
||||
"processStopped": true,
|
||||
"exitCode": null,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"reportVersion": "ht-higher-dimensional-package-smoke/v1",
|
||||
"generatedAtUtc": "2026-06-22T01:45:23.3808621Z",
|
||||
"packageRoot": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim",
|
||||
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim\\Windows\\UnrealHyperTwist.exe",
|
||||
"generatedAtUtc": "2026-06-23T10:28:55.0662545Z",
|
||||
"packageRoot": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim_refresh_20260623",
|
||||
"executablePath": "C:\\HyperTwist_worktrees\\phase10validate_packaged_phase6c_higherdim_refresh_20260623\\Windows\\UnrealHyperTwist.exe",
|
||||
"mapUrl": "/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining",
|
||||
"smokeSeconds": 10,
|
||||
"resolution": {
|
||||
|
|
@ -83,7 +83,7 @@
|
|||
},
|
||||
"keepRunning": false,
|
||||
"result": "passed",
|
||||
"processId": 35616,
|
||||
"processId": 13584,
|
||||
"processStopped": true,
|
||||
"exitCode": null,
|
||||
"error": null
|
||||
|
|
|
|||
|
|
@ -154,6 +154,24 @@ Use these as the current governing docs:
|
|||
- Validation evidence on `2026-06-19`: the same primary reverse-SSH `localhost:22022` lane rebuilt isolated worktree `C:\HyperTwist_worktrees\phase10validate` with `Result: Succeeded` and UnrealBuildTool `Total execution time: 3258.26 seconds`, then `Automation RunTests HyperTwist.Browser` exported `Saved\AutomationReports\Browser-NativeOperatorDiagnosticsPanel-Verify\index.json` with `14` `HyperTwist.Browser.*` tests succeeded and `0` failed, including `CoachDashboard.RuntimeInspectSurface` and `TrainingPanel.RuntimeInspectSurface`
|
||||
- that same shipping/browser posture now also carries typed browser bootstrap/runtime-ready timestamps, last command and shell-state receipt timestamps, and fallback reason through the native operator and training/dashboard inspect seams, so Unreal-side diagnostics mirror the authoritative browser runtime status surface more faithfully without reopening topology
|
||||
- Validation evidence on `2026-06-19`: the same primary reverse-SSH `localhost:22022` lane rebuilt isolated worktree `C:\HyperTwist_worktrees\phase10validate` with `Result: Succeeded` and UnrealBuildTool `Total execution time: 3261.17 seconds`, then `Automation RunTests HyperTwist.Browser` exported `Saved\AutomationReports\Browser-NativeOperatorTimelineFidelity-Verify\index.json` with `14` `HyperTwist.Browser.*` tests succeeded and `0` failed, including `CoachDashboard.RuntimeInspectSurface`, `TrainingPanel.RuntimeInspectSurface`, and `Widget.OperatorStatusSurfaceLive`
|
||||
- that same shipping/browser posture now also carries a native control/input
|
||||
readiness inspect surface through `UHyperTwistTrainingPanelWidget` and
|
||||
`UHyperTwistCoachDashboardWidget`, so the operator can see the shipped
|
||||
classic keyboard profile, classic-cube input readiness,
|
||||
higher-dimensional dedicated-family readiness, project-level
|
||||
`EnhancedInput`/motion-control groundwork, and explicit unfinished
|
||||
XR/preferences truth without opening the browser shell
|
||||
- Validation evidence on `2026-06-23`: the same recovered primary reverse-SSH
|
||||
`localhost:22022` lane rebuilt maintained isolated worktree
|
||||
`C:\HyperTwist_worktrees\phase10validate` with `Result: Succeeded` and
|
||||
UnrealBuildTool `Total execution time: 1563.53 seconds`, then
|
||||
`Automation RunTests HyperTwist.Browser` exported
|
||||
`Saved\AutomationReports\Browser-ControlInputReadiness-Verify\index.json`
|
||||
with `16` `HyperTwist.Browser.*` tests succeeded and `0` failed, including
|
||||
`CoachDashboard.ControlInputReadinessInspectSurface`,
|
||||
`TrainingPanel.ControlInputReadinessInspectSurface`,
|
||||
`CoachDashboard.RuntimeInspectSurface`, and
|
||||
`TrainingPanel.RuntimeInspectSurface`
|
||||
- the same optional full-browser-client branch is now also tightened by a backend-contract matrix packet that fixes transport-neutral operation labels, minimum session or host or bridge identity facts, request or result correlation posture, freshness or reconnect rules, and the owned state/runtime payload anchors a later browser-client implementation must preserve
|
||||
- `MagicTile` `Phase 7C` is now landed separately through the bundled tiling native-behavior
|
||||
proof contract/probe seam, runtime-library proof helpers, and focused Windows validation on
|
||||
|
|
|
|||
|
|
@ -71,11 +71,14 @@ thin adjacent placeholders.
|
|||
The later continuation also hardened:
|
||||
|
||||
- `/app/browser-access` with live auth-health posture, browser-to-desktop
|
||||
handoff guidance, bounded branch truth, and release-reference links
|
||||
handoff guidance, bounded branch truth, release-reference links, and the same
|
||||
bounded operator escalation map used by the public support lane
|
||||
- `/app/account` with live release-manifest viewer posture, configured target
|
||||
visibility, and explicit operator follow-through links
|
||||
- `/app/notices` with protected release-target notice posture, operator duties,
|
||||
and linked protected/public notices plus corresponding-source references
|
||||
linked protected/public notices plus corresponding-source references, and a
|
||||
protected release follow-through plus escalation surface that keeps
|
||||
entitlement, package, runtime, and rollout/compliance issues separated
|
||||
|
||||
This keeps the protected app shell aligned with the public manual packet so the
|
||||
operator surface remains honest and useful after sign-in rather than becoming a
|
||||
|
|
@ -134,3 +137,215 @@ The same validation hardening also gives the live-spawned
|
|||
`website/server` bootstrap proof an explicit `15s` timeout so real child-process
|
||||
boot plus same-origin HTTP verification does not fail spuriously on a loaded
|
||||
host while the proof scope remains unchanged.
|
||||
|
||||
Latest same-lane follow-up on `2026-06-23` stayed green under the widened
|
||||
public/protected/auth stack:
|
||||
|
||||
- `npm run type-check` in `website/`
|
||||
- `npm test -- --run` in `website/`
|
||||
- `37` test files passed
|
||||
- `137` tests passed
|
||||
- `npm run build` in `website/`
|
||||
- `npm run type-check` in `website/server`
|
||||
- `npm test -- --run` in `website/server`
|
||||
- `10` test files passed
|
||||
- `36` tests passed
|
||||
- the current server suite again covered the production-shaped bootstrap lane,
|
||||
including:
|
||||
- same-origin health plus built-site serving
|
||||
- webhook/billing-state reflection
|
||||
- bounded testing-session resolution for `/api/auth/me` and
|
||||
`/api/auth/desktop-link`
|
||||
- the embedded browser runtime also stayed green under:
|
||||
- `npm run verify:shell` in `Content/Browser/`
|
||||
- `npm run build` in `Content/Browser/`
|
||||
- the same-day public-manual follow-up also widened the docs/resources manual
|
||||
depth with a more concrete runtime control guide covering:
|
||||
- current classic-cube desktop interaction posture
|
||||
- current higher-dimensional desktop interaction posture
|
||||
- explicit “supported now” versus “later XR/preferences packet” control
|
||||
boundaries
|
||||
- the same-lane continuation then widened the public product surface again with:
|
||||
- a first-launch and desktop-setup guide on the public `/download` lane
|
||||
plus matching first-launch follow-through on the protected `/app/downloads`
|
||||
lane
|
||||
- the same first-launch follow-through mirrored into the protected dashboard
|
||||
overview and protected account surface so post-sign-in operator guidance
|
||||
stays aligned with the public download lane
|
||||
- a support-side escalation map separating account, package, runtime, and
|
||||
rollout/compliance issues
|
||||
- the same escalation separation then mirrored into protected
|
||||
`/app/browser-access` and `/app/notices` so the signed-in operator lane no
|
||||
longer drops back to thinner troubleshooting guidance than the public
|
||||
support surface
|
||||
- matching focused validation coverage on the public marketing-page suite
|
||||
plus the protected download-center suite
|
||||
|
||||
That follow-up also stayed green under:
|
||||
|
||||
- `npm run type-check` in `website/`
|
||||
- `npm test -- --run src/__tests__/public-marketing-pages.test.tsx` in
|
||||
`website/`
|
||||
- `npm run build` in `website/`
|
||||
- `npm test -- --run` in `website/`
|
||||
- `37` test files passed
|
||||
- `137` tests passed
|
||||
- `npm test -- --run src/__tests__/DashboardOverviewPage.test.tsx src/__tests__/download-center-page.test.tsx src/__tests__/protected-app-pages.test.tsx`
|
||||
in `website/`
|
||||
- `3` test files passed
|
||||
- `5` tests passed
|
||||
|
||||
The next same-family protected-surface continuation then stayed green under:
|
||||
|
||||
- `npm run type-check` in `website/`
|
||||
- `npm test -- --run src/__tests__/protected-app-pages.test.tsx` in `website/`
|
||||
- `1` test file passed
|
||||
- `3` tests passed
|
||||
- `npm test -- --run` in `website/`
|
||||
- `37` test files passed
|
||||
- `137` tests passed
|
||||
- `npm run build` in `website/`
|
||||
- `scripts/run-hypertwist-sentrux-source-only.sh`
|
||||
- `Quality: 6112`
|
||||
- `All rules pass`
|
||||
|
||||
Current truthful reading after that follow-up:
|
||||
|
||||
- the public pages are no longer just content-complete; they are backed by a
|
||||
fully green current website/runtime/server validation pass
|
||||
- the shared auth/billing/dashboard/browser-to-desktop handoff surface is now
|
||||
materially better evidenced than a page-only smoke pass
|
||||
- the public manual now reads more like a usage guide for the current desktop
|
||||
runtime instead of only a topology or release-story explanation
|
||||
- the download/support surfaces now act more like an operator-first launch
|
||||
manual than a generic storefront wrapper around the desktop build
|
||||
- the protected dashboard/account/download surfaces now project the same
|
||||
rollout and first-launch posture after sign-in instead of dropping back to a
|
||||
thinner release-only shell
|
||||
- the protected browser-access/notices surfaces now also keep the same
|
||||
escalation and follow-through separation after sign-in instead of flattening
|
||||
access, package, runtime, and rollout questions together
|
||||
|
||||
Latest authority-sync follow-up later on `2026-06-23`:
|
||||
|
||||
- the public manual stayed aligned with the newer native
|
||||
control/input-readiness continuation rather than implying finished VR or
|
||||
preferences ownership
|
||||
- focused website/manual coverage stayed green under:
|
||||
- `npm --prefix website test -- --run src/__tests__/public-marketing-pages.test.tsx src/__tests__/download-center-page.test.tsx src/__tests__/protected-app-pages.test.tsx src/__tests__/DashboardOverviewPage.test.tsx`
|
||||
- `4` test files passed
|
||||
- `13` tests passed
|
||||
- the embedded browser runtime also stayed green again under:
|
||||
- `npm --prefix Content/Browser run verify:shell`
|
||||
- `npm --prefix Content/Browser run build`
|
||||
- the current public/docs/download/account/notices surfaces therefore remain
|
||||
consistent with the authoritative product truth:
|
||||
- desktop simulator is primary
|
||||
- browser shell is complementary and necessary
|
||||
- keyboard/mouse and higher-dimensional desktop interaction are real now
|
||||
- XR groundwork exists, but finished XR/preferences ownership is still a
|
||||
later native packet
|
||||
|
||||
Latest protected-surface resilience follow-up later on `2026-06-23`:
|
||||
|
||||
- the shared release-manifest fallback was tightened so protected browser
|
||||
surfaces now retain the signed-in viewer posture from local session truth
|
||||
when live auth-server manifest authority is temporarily unavailable
|
||||
- the protected download-center lane then also stopped relying only on the
|
||||
local auth snapshot for the actual download action:
|
||||
- when live manifest authority resolves fresher entitled viewer truth, the
|
||||
protected release lane now follows that server-backed viewer posture for
|
||||
download access instead of leaving the operator blocked behind stale local
|
||||
state
|
||||
- when live manifest authority is unavailable, the protected release lane
|
||||
now explicitly tells the already-entitled operator that release authority
|
||||
is temporarily unavailable, instead of mislabeling that state as lack of
|
||||
account entitlement
|
||||
- the shared browser auth layer then also gained a bounded release-authority
|
||||
reconciliation path:
|
||||
- when protected pages receive fresher live manifest viewer truth, the
|
||||
browser session snapshot is now refreshed and persisted from that release
|
||||
authority instead of remaining stale until a later bootstrap
|
||||
- the adjacent protected dashboard and account surfaces now expose a compact
|
||||
live-authority sync notice summarizing what was refreshed, so operators can
|
||||
see the session catch-up instead of only inferring drift after the fact
|
||||
- the same shared auth layer then also tightened stale-session invalidation:
|
||||
- when shared auth is configured and `/api/auth/me` now returns `401`, the
|
||||
provider clears the stale stored browser session instead of preserving a
|
||||
false authenticated protected-shell posture
|
||||
- the local-fallback lane remains intact when shared auth is not configured
|
||||
or the account API is simply unreachable, so the stricter invalidation does
|
||||
not erase the intended offline fallback behavior
|
||||
- that fallback remains intentionally conservative:
|
||||
- raw direct package delivery still stays withheld until live manifest
|
||||
authority returns
|
||||
- public fallback metadata still does not expose direct download URLs
|
||||
- focused current validation stayed green under:
|
||||
- `npm --prefix website run type-check`
|
||||
- `npm --prefix website test -- --run src/__tests__/release-manifest.test.ts src/__tests__/protected-app-pages.test.tsx src/__tests__/download-center-page.test.tsx src/__tests__/public-auth-pages.test.tsx src/__tests__/public-marketing-pages.test.tsx src/__tests__/DashboardOverviewPage.test.tsx`
|
||||
- `7` test files passed
|
||||
- `27` tests passed
|
||||
- `npm --prefix website/server test -- --run`
|
||||
- `10` test files passed
|
||||
- `36` tests passed
|
||||
- `npm --prefix website test -- --run src/__tests__/platform-auth.bootstrap.test.tsx src/__tests__/download-center-page.test.tsx src/__tests__/protected-app-pages.test.tsx src/__tests__/DashboardOverviewPage.test.tsx src/__tests__/app-route-tree.test.tsx src/__tests__/route-shells.test.tsx src/__tests__/release-manifest.test.ts`
|
||||
- `8` test files passed
|
||||
- `34` tests passed
|
||||
- `scripts/run-hypertwist-sentrux-source-only.sh`
|
||||
- `Quality: 6105`
|
||||
- `All rules pass`
|
||||
|
||||
Latest website and dependency hardening follow-up later on `2026-06-23`:
|
||||
|
||||
- the broader website and delivery lane then also stayed green under:
|
||||
- `npm --prefix website run build`
|
||||
- `npm --prefix website/server run type-check`
|
||||
- `npm --prefix website/server test -- --run`
|
||||
- `npm --prefix Content/Browser run verify:shell`
|
||||
- `npm --prefix Content/Browser run build`
|
||||
- the repo now also owns a one-command validation proof for that same public or
|
||||
protected or embedded-browser website family:
|
||||
- `scripts/run-hypertwist-web-surface-validation.sh`
|
||||
- it completed successfully in default mode on `2026-06-23`
|
||||
- it re-proved the current website checks, auth-server checks,
|
||||
`Content/Browser` verify/build lane, clean production audits for
|
||||
`website/` plus `Content/Browser/`, and the documented auth-server
|
||||
residual warning path
|
||||
- the same same-family continuation then also tightened the packaged-proof
|
||||
bridge used by the public/manual website lane:
|
||||
- the website/browser release surfaces no longer depend on a stale
|
||||
hand-maintained Windows packaged-validation summary constant
|
||||
- the current public/protected release surfaces now consume a sanitized
|
||||
generated summary rendered from the checked-in authoritative higher-
|
||||
dimensional package report at
|
||||
`docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`
|
||||
- the new renderer lives at
|
||||
`scripts/render-hypertwist-web-package-validation-summary.mjs`
|
||||
- the owned web-surface validation gate now checks that generated summary
|
||||
for freshness before approving the public/auth/download/browser lane
|
||||
- the browser runtime now also has a checked-in `package-lock.json`, so its
|
||||
dependency truth is no longer implicit or non-auditable
|
||||
- production dependency audit truth is now clearer across the current public
|
||||
and protected website family:
|
||||
- `npm audit --omit=dev --audit-level=high` in `website/` returned
|
||||
`found 0 vulnerabilities`
|
||||
- `npm audit --omit=dev --audit-level=high` in `Content/Browser/` returned
|
||||
`found 0 vulnerabilities`
|
||||
- the auth server dependency lane was hardened by upgrading
|
||||
`supertokens-node` from `21.1.0` to `24.0.2`
|
||||
- that auth-server upgrade remained type-safe and behavior-safe under the
|
||||
existing same-origin, auth-health, release-manifest, billing, and
|
||||
desktop-link server tests
|
||||
- remaining auth-server dependency risk is now explicit:
|
||||
- `npm audit --omit=dev --audit-level=high` in `website/server/` still
|
||||
reports the upstream `supertokens-node -> nodemailer@8.0.11` advisory
|
||||
chain
|
||||
- no unsupported forced major override was landed just to hide that result,
|
||||
because current upstream package metadata still declares `nodemailer`
|
||||
support through the `^8.0.2` range
|
||||
- the current public manual therefore remains strengthened rather than
|
||||
weakened:
|
||||
- browser and desktop surfaces are better validated
|
||||
- current package and auth-server posture is more explicit
|
||||
- unresolved third-party dependency residue is documented honestly instead
|
||||
of being silently ignored
|
||||
|
|
|
|||
|
|
@ -258,6 +258,38 @@ Additional same-lane follow-up on `2026-06-23`:
|
|||
docs/resources/support surfaces now explicitly project current keyboard,
|
||||
higher-dimensional control, and unfinished XR/controller truth instead of
|
||||
only the browser-versus-desktop topology boundary
|
||||
|
||||
Further same-lane validation refresh later on `2026-06-23`:
|
||||
|
||||
- `scripts/run-hypertwist-gitnexus-analyze.sh` re-indexed the bounded
|
||||
source-only mirror successfully at `16,051` nodes, `37,444` edges,
|
||||
`641` clusters, and `300` flows
|
||||
- `scripts/run-hypertwist-gitnexus-status.sh` then reported the bounded mirror
|
||||
`Status: up-to-date`
|
||||
- `scripts/run-hypertwist-sentrux-source-only.sh` improved again to
|
||||
`Quality: 6112` with all `7` checked rules passing
|
||||
- `npm run verify:shell` in `Content/Browser/` passed again
|
||||
- `npm run build` in `Content/Browser/` passed again
|
||||
- the public website/manual follow-up also passed:
|
||||
- `npm run type-check` in `website/`
|
||||
- `npm test -- --run` in `website/`
|
||||
- `37` test files passed
|
||||
- `137` tests passed
|
||||
- `npm test -- --run src/__tests__/public-marketing-pages.test.tsx` in
|
||||
`website/`
|
||||
- `npm run build` in `website/`
|
||||
- the previously interrupted authoritative remote Unreal editor build was then
|
||||
recovered on the maintained validation root
|
||||
`C:\HyperTwist_worktrees\phase10validate` by cleaning the stale
|
||||
`cmd.exe` / `UnrealBuildTool` tail and rerunning the canonical helper in a
|
||||
safer single-worker posture:
|
||||
- `scripts/run-hypertwist-remote-unreal-build.sh --max-parallel-actions 1`
|
||||
- `Result: Succeeded`
|
||||
- `Total time in Parallel executor: 3027.11 seconds`
|
||||
- `Total execution time: 3030.86 seconds`
|
||||
- that recovered proof materially strengthens the current reading that the
|
||||
earlier `C1060` compiler-heap failure was a stale/interrupted-lane or
|
||||
posture issue rather than a stable source regression in the current tree
|
||||
- focused website coverage for
|
||||
`src/__tests__/public-marketing-pages.test.tsx` and
|
||||
`src/__tests__/protected-app-pages.test.tsx` passed after that public-manual
|
||||
|
|
@ -368,6 +400,185 @@ Current highest-signal structural truth after this latest continuation:
|
|||
- the repaired skill-memory fixture lane is now fully green on the real remote
|
||||
Windows Unreal path across all `9` focused `S3` reports
|
||||
|
||||
Most recent same-family continuation later on `2026-06-23`:
|
||||
|
||||
- `scripts/run-hypertwist-gitnexus-analyze.sh` re-indexed the bounded
|
||||
source-only mirror successfully at `16,061` nodes, `37,459` edges,
|
||||
`646` clusters, and `300` flows
|
||||
- `scripts/run-hypertwist-gitnexus-status.sh` again reported the bounded
|
||||
mirror `Status: up-to-date`
|
||||
- `scripts/run-hypertwist-sentrux-source-only.sh` remained green at
|
||||
`Quality: 6111` with all `7` checked rules passing
|
||||
- the embedded browser runtime stayed green under:
|
||||
- `npm --prefix Content/Browser run verify:shell`
|
||||
- `npm --prefix Content/Browser run build`
|
||||
- focused current website/manual validation stayed green under:
|
||||
- `npm --prefix website test -- --run src/__tests__/public-marketing-pages.test.tsx src/__tests__/download-center-page.test.tsx src/__tests__/protected-app-pages.test.tsx src/__tests__/DashboardOverviewPage.test.tsx`
|
||||
- `4` test files passed
|
||||
- `13` tests passed
|
||||
- the recovered authoritative Unreal continuation on maintained validation root
|
||||
`C:\HyperTwist_worktrees\phase10validate` then stayed green under:
|
||||
- `scripts/run-hypertwist-remote-unreal-build.sh --max-parallel-actions 8`
|
||||
- `Result: Succeeded`
|
||||
- `Total time in Parallel executor: 1552.99 seconds`
|
||||
- `Total execution time: 1563.53 seconds`
|
||||
- the same recovered lane then exported
|
||||
`Saved\AutomationReports\Browser-ControlInputReadiness-Verify\index.json`
|
||||
with `16` green `HyperTwist.Browser.*` tests, including:
|
||||
- `HyperTwist.Browser.CoachDashboard.ControlInputReadinessInspectSurface`
|
||||
- `HyperTwist.Browser.TrainingPanel.ControlInputReadinessInspectSurface`
|
||||
|
||||
Latest same-family tool and website resilience follow-up later on
|
||||
`2026-06-23`:
|
||||
|
||||
- `scripts/run-hypertwist-gitnexus-status.sh` again reported the bounded
|
||||
source-only mirror `Status: up-to-date`
|
||||
- `scripts/run-hypertwist-sentrux-source-only.sh` stayed green at
|
||||
`Quality: 6110` with all `7` checked rules passing
|
||||
- that slight score drift from the immediately prior `6111` reading did not
|
||||
reopen any rule failures, cycle debt, or browser/website structural
|
||||
violations
|
||||
- the protected release-manifest fallback was then hardened so dashboard,
|
||||
account, browser-access, and notices surfaces keep signed-in viewer posture
|
||||
from the local session when live auth-server manifest authority is
|
||||
unavailable, while raw download delivery authority still remains withheld
|
||||
- the same bounded website lane stayed green under:
|
||||
- `npm --prefix website run type-check`
|
||||
- `npm --prefix website test -- --run src/__tests__/release-manifest.test.ts src/__tests__/protected-app-pages.test.tsx src/__tests__/download-center-page.test.tsx src/__tests__/public-auth-pages.test.tsx src/__tests__/public-marketing-pages.test.tsx src/__tests__/DashboardOverviewPage.test.tsx`
|
||||
- `7` test files passed
|
||||
- `27` tests passed
|
||||
- `npm --prefix website/server test -- --run`
|
||||
- `10` test files passed
|
||||
- `36` tests passed
|
||||
|
||||
Latest dependency and validation hardening follow-up later on `2026-06-23`:
|
||||
|
||||
- `scripts/run-hypertwist-gitnexus-analyze.sh` re-indexed the bounded
|
||||
source-only mirror successfully at `16,089` nodes, `37,536` edges,
|
||||
`645` clusters, and `300` flows
|
||||
- `scripts/run-hypertwist-gitnexus-status.sh` again reported the bounded
|
||||
mirror `Status: up-to-date`
|
||||
- `scripts/run-hypertwist-sentrux-source-only.sh` stayed green at
|
||||
`Quality: 6105` with all `7` checked rules passing
|
||||
- the broader current website/runtime validation packet stayed green under:
|
||||
- `npm --prefix website run build`
|
||||
- `npm --prefix website/server run type-check`
|
||||
- `npm --prefix website/server test -- --run`
|
||||
- `npm --prefix Content/Browser run verify:shell`
|
||||
- `npm --prefix Content/Browser run build`
|
||||
- the repo now also owns a single bounded validation command for that same
|
||||
browser/public/auth-server surface:
|
||||
- `scripts/run-hypertwist-web-surface-validation.sh`
|
||||
- it runs the current website type-check or focused auth-route tests or
|
||||
website build or website/server type-check or server tests or
|
||||
`Content/Browser` verify/build sequence in one reproducible packet
|
||||
- it also runs production dependency audits for `website/`,
|
||||
`website/server/`, and `Content/Browser/`
|
||||
- it treats the current exact upstream `supertokens-node -> nodemailer`
|
||||
auth-server advisory as a documented residual warning in default mode
|
||||
rather than forcing an unsupported major override
|
||||
- `--strict-auth-server-audit` remains available when the lane should fail on
|
||||
that residual too
|
||||
- that new wrapper was immediately validated in default mode on `2026-06-23`
|
||||
and completed successfully across website checks, website/server checks,
|
||||
`Content/Browser` verification/build, clean production audits for
|
||||
`website/` plus `Content/Browser/`, and the documented auth-server residual
|
||||
warning path
|
||||
- the same same-family continuation then also replaced the stale hand-maintained
|
||||
website packaged-validation constant with a generated sanitized summary
|
||||
rendered from
|
||||
`docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`
|
||||
into
|
||||
`website/src/shared/generated/windows-package-validation-summary.json`
|
||||
- the repo now owns that bridge through:
|
||||
- `scripts/render-hypertwist-web-package-validation-summary.mjs`
|
||||
- `scripts/run-hypertwist-web-surface-validation.sh`, which now fails if the
|
||||
generated website summary drifts stale from the checked-in authoritative
|
||||
higher-dimensional package report
|
||||
- the browser runtime now also has an auditable lockfile at
|
||||
`Content/Browser/package-lock.json`
|
||||
- production dependency audit truth after that lockfile continuation is now:
|
||||
- `npm audit --omit=dev --audit-level=high` in `website/` returned
|
||||
`found 0 vulnerabilities`
|
||||
- `npm audit --omit=dev --audit-level=high` in `Content/Browser/` returned
|
||||
`found 0 vulnerabilities`
|
||||
- full browser-runtime audit still reports one low-severity development-only
|
||||
advisory:
|
||||
- `esbuild@0.27.7` through `vite@7.3.5`
|
||||
- impact is the Windows development server lane rather than shipped browser
|
||||
runtime output
|
||||
- the auth server was then upgraded from `supertokens-node@21.1.0` to
|
||||
`supertokens-node@24.0.2`
|
||||
- that auth-server upgrade stayed green under:
|
||||
- `npm --prefix website/server run type-check`
|
||||
- `npm --prefix website/server test -- --run`
|
||||
- remaining auth-server production audit truth is now explicit rather than
|
||||
hidden:
|
||||
- `npm audit --omit=dev --audit-level=high` in `website/server/` still
|
||||
reports the upstream `supertokens-node -> nodemailer@8.0.11` advisory
|
||||
chain
|
||||
- upstream package metadata currently pins `nodemailer` as `^8.0.2` while
|
||||
the current `nodemailer` latest is `9.0.1`
|
||||
- no unsafe forced major override was landed, because that would cross the
|
||||
supported upstream dependency range without first-party compatibility proof
|
||||
|
||||
Latest refactor-tooling follow-up later on `2026-06-23`:
|
||||
|
||||
- HyperTwist now has a verified repo-local analyzer binary present at:
|
||||
- `tools/sentrux/bin/sentrux`
|
||||
- `scripts/bootstrap-hypertwist-sentrux.sh --if-missing` now truthfully reports
|
||||
that repo-local analyzer as already materialized instead of relying on
|
||||
cross-repo memory
|
||||
- `scripts/run-hypertwist-sentrux-source-only.sh` was re-run again on the
|
||||
bounded source-only mirror and currently reports:
|
||||
- `Quality: 6108`
|
||||
- all `7` checked rules passing
|
||||
- current highest-signal structural reading after that rerun is:
|
||||
- no current browser/public/auth-server structural regression is being
|
||||
reported by the owned source-only gate
|
||||
- the same owned source-only mirror still covers
|
||||
`UnrealHyperTwist/Source`, `Content/Browser/src`, `website/src`,
|
||||
`website/server/src`, and `scripts`
|
||||
- `scripts/run-hypertwist-gitnexus-analyze.sh` was then hardened further for
|
||||
current-host truth:
|
||||
- it still prefers the retained local CLI first
|
||||
- if that real analyze path does not complete cleanly on the current host, it
|
||||
now falls back explicitly to `npx -y gitnexus@latest`
|
||||
- it now suppresses the retained local native-loader stderr spew during that
|
||||
rejected local-analyze attempt so the wrapper output stays operationally
|
||||
useful
|
||||
- it now also distinguishes three states instead of flattening them together:
|
||||
- clean successful analyze exit
|
||||
- timeout before any readable bounded-mirror index exists
|
||||
- timeout after `.gitnexus/meta.json` already proves the current disposable
|
||||
mirror commit was indexed, which the wrapper can now treat as success
|
||||
- short-timeout proof on the current host also established a useful safety
|
||||
truth:
|
||||
- a too-short timeout no longer yields a false success
|
||||
- the wrapper now exits truthfully when no readable bounded-mirror index is
|
||||
yet available
|
||||
- because the current retained local `GitNexus` analyze path still does not
|
||||
complete cleanly on this Linux host, the truthful current operator posture
|
||||
remains:
|
||||
- treat repo-local `sentrux` as the fast owned structural baseline
|
||||
- use the bounded `GitNexus` wrapper when the deeper graph/impact pass is
|
||||
worth the longer or fallback-prone run
|
||||
- do not confuse a locally present retained `GitNexus` checkout with a
|
||||
guaranteed host-compatible native analyze path
|
||||
- a full-length follow-up on the same host then re-proved the bounded
|
||||
`GitNexus` lane through the hardened wrapper:
|
||||
- the wrapper rejected the broken retained local analyze path
|
||||
- the fallback path then completed successfully in `79.2s`
|
||||
- current bounded-mirror graph stats are now:
|
||||
- `16,116` nodes
|
||||
- `37,586` edges
|
||||
- `647` clusters
|
||||
- `300` flows
|
||||
- `scripts/run-hypertwist-sentrux-source-only.sh` was then rerun again after
|
||||
that graph refresh and remained at:
|
||||
- `Quality: 6108`
|
||||
- all `7` checked rules passing
|
||||
|
||||
## Out of scope
|
||||
|
||||
This note does not:
|
||||
|
|
|
|||
|
|
@ -221,6 +221,21 @@ This extends the doctrine from higher-dimensional activation/ownership proof
|
|||
into current structured package/archive/launch proof on the same primary
|
||||
reverse-SSH lane.
|
||||
|
||||
Further higher-dimensional package-refresh proof on `2026-06-23` then
|
||||
revalidated that same maintained helper lane after the recovered authoritative
|
||||
editor rebuild: the isolated Windows worktree
|
||||
`C:\HyperTwist_worktrees\phase10validate` still had the packaged-game target
|
||||
receipt, so `scripts\Invoke-HyperTwistHigherDimensionalPackage.ps1` ran again
|
||||
in `-SkipBuild` mode, completed `BuildCookRun` with `ExitCode=0` and
|
||||
`BuildCookRun time: 70.74 s`, wrote archive output to
|
||||
`C:\HyperTwist_worktrees\phase10validate_packaged_phase6c_higherdim_refresh_20260623`,
|
||||
smoke-launched both higher-dimensional dedicated-family training maps again,
|
||||
and refreshed the checked-in aggregate proof at
|
||||
`docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`.
|
||||
This extends the doctrine from a one-off helper proof into a re-proven
|
||||
maintained package lane that still stays green after later editor-build
|
||||
recovery work on the same primary reverse-SSH path.
|
||||
|
||||
Further `2026-06-13` continuation proof on that same primary lane recovered
|
||||
the classic-cube replay/leaderboard/package slice after a corrupted reverse
|
||||
sync incident: a prior aborted broad tar sync left remote
|
||||
|
|
@ -400,6 +415,24 @@ This extends the doctrine from classic-cube, package, higher-dimensional, and
|
|||
media-export proof into the real remote skill-memory validation lane on the
|
||||
same maintained reverse-SSH path.
|
||||
|
||||
Further live recovery-proof follow-up on `2026-06-23` established:
|
||||
|
||||
- a later interrupted remote editor-build attempt had left a stale
|
||||
`cmd.exe` / `UnrealBuildTool` tail alive on the maintained validation root
|
||||
without active `cl.exe` workers, so the lane was not cleanly post-command
|
||||
- after terminating only that stale toolchain tail, the same maintained
|
||||
validation root `C:\HyperTwist_worktrees\phase10validate` was rerun through
|
||||
the canonical helper in the safer single-worker posture
|
||||
`scripts/run-hypertwist-remote-unreal-build.sh --max-parallel-actions 1`
|
||||
- that recovered authoritative rebuild completed with:
|
||||
- `Result: Succeeded`
|
||||
- `Total time in Parallel executor: 3027.11 seconds`
|
||||
- `Total execution time: 3030.86 seconds`
|
||||
- this recovery materially strengthens the current reading that the earlier
|
||||
`fatal error C1060: compiler is out of heap space` result belonged to the
|
||||
stale/interrupted or high-pressure lane posture rather than to a stable
|
||||
source break in the current HyperTwist tree
|
||||
|
||||
## Closeout wording requirement
|
||||
|
||||
Every Unreal C++ closeout should say one of these explicitly:
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ HyperTwist currently has real native input ownership for:
|
|||
- `EnhancedInput`-based project posture
|
||||
- broad motion-controller axis configuration at the Unreal project-settings
|
||||
level
|
||||
- a native operator/training inspect surface that keeps those truths and the
|
||||
unfinished XR/preferences boundary visible inside Unreal itself
|
||||
|
||||
HyperTwist does **not** yet have enough current first-party runtime evidence to
|
||||
truthfully claim:
|
||||
|
|
@ -47,6 +49,42 @@ truthfully claim:
|
|||
Vive, Mixed Reality, Oculus Touch, and Valve Index families
|
||||
- the same file keeps mouse capture, wheel, and motion-control settings active
|
||||
|
||||
### Native operator/training inspect truth is now real
|
||||
|
||||
- `FHyperTwistTrainingControlInputReadinessInspectSurface` now exists in
|
||||
`UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingTypes.h`
|
||||
- `UHyperTwistTrainingPanelWidget` and `UHyperTwistCoachDashboardWidget` now
|
||||
expose that surface and keep the same truth visible inside native training
|
||||
or operator flows
|
||||
- a same-family native quality follow-up on `2026-06-23` then tightened the
|
||||
project-input-groundwork inspection behind that surface so it now reads
|
||||
Unreal input settings structurally from config instead of reparsing raw
|
||||
`DefaultInput.ini` text on each surface read
|
||||
- the current surface explicitly reports:
|
||||
- shipped `classic-wca-keyboard/v1`
|
||||
- classic-cube mouse/touch/shortcut readiness
|
||||
- higher-dimensional dedicated-family runtime-catalog readiness
|
||||
- `EnhancedInput` plus motion-controller groundwork presence
|
||||
- immersive-presence contract presence
|
||||
- unfinished XR runtime and user-facing preferences/rebinding truth
|
||||
- Windows validation on `2026-06-23` rebuilt the maintained validation root
|
||||
`C:\HyperTwist_worktrees\phase10validate` with `Result: Succeeded`,
|
||||
UnrealBuildTool `Total execution time: 1563.53 seconds`, then exported
|
||||
`Saved\AutomationReports\Browser-ControlInputReadiness-Verify\index.json`
|
||||
with all `16` `HyperTwist.Browser.*` tests green, including the new
|
||||
training-panel and coach-dashboard control/input readiness checks
|
||||
- the later same-day config-inspection hardening follow-up then hash-synced
|
||||
`HyperTwistTrainingPanelWidget.cpp` into that same maintained validation
|
||||
root, rebuilt cleanly again with `Result: Succeeded`, UnrealBuildTool
|
||||
`Total execution time: 158.13 seconds`, and re-exported focused browser
|
||||
automation proof to
|
||||
`Saved\AutomationReports\Browser-ControlInputReadiness-StructuredConfig-Verify`
|
||||
with all `16` `HyperTwist.Browser.*` tests green again, including:
|
||||
- `CoachDashboard.ControlInputReadinessInspectSurface`
|
||||
- `TrainingPanel.ControlInputReadinessInspectSurface`
|
||||
- `CoachDashboard.RuntimeInspectSurface`
|
||||
- `TrainingPanel.RuntimeInspectSurface`
|
||||
|
||||
### Finished XR runtime ownership is not yet proven
|
||||
|
||||
- `UnrealHyperTwist/UnrealHyperTwist.uproject` currently enables
|
||||
|
|
@ -67,6 +105,7 @@ Current truthful product wording should say:
|
|||
|
||||
- desktop Unreal simulator: real and shipping
|
||||
- classic input and higher-dimensional keyboard interaction: real and shipping
|
||||
- native operator/training diagnostics already surface current input truth
|
||||
- immersive/scenic training direction: first-party owned and source-backed
|
||||
- full VR/controller/settings polish lane: not yet complete enough to market as
|
||||
finished
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ HyperTwist now has its own bounded refactor/analyzer entry points:
|
|||
- `scripts/run-hypertwist-sentrux-source-only.sh`
|
||||
- `scripts/run-hypertwist-gitnexus-analyze.sh`
|
||||
- `scripts/run-hypertwist-gitnexus-status.sh`
|
||||
- `scripts/run-hypertwist-web-surface-validation.sh`
|
||||
- `scripts/render-hypertwist-web-package-validation-summary.mjs`
|
||||
- `.sentrux/rules.toml`
|
||||
|
||||
Use them with this posture:
|
||||
|
|
@ -61,6 +63,50 @@ Suggested loop:
|
|||
4. treat `sentrux` failures as structural review signals, then confirm with
|
||||
focused product tests
|
||||
|
||||
Additional dependency-health loop for the current browser and website family:
|
||||
|
||||
1. prefer the owned umbrella command:
|
||||
`scripts/run-hypertwist-web-surface-validation.sh`
|
||||
2. that command covers:
|
||||
- `npm --prefix website run type-check`
|
||||
- focused current website route/auth/release tests
|
||||
- `npm --prefix website run build`
|
||||
- `npm --prefix website/server run type-check`
|
||||
- `npm --prefix website/server test -- --run`
|
||||
- `npm --prefix Content/Browser run verify:shell`
|
||||
- `npm --prefix Content/Browser run build`
|
||||
3. when audits need to be reasoned about directly, run:
|
||||
- `npm audit --omit=dev --audit-level=high` in `website/`
|
||||
- `npm audit --omit=dev --audit-level=high` in `website/server/`
|
||||
- `npm audit --omit=dev --audit-level=high` in `Content/Browser/`
|
||||
|
||||
Current dependency truth after the `2026-06-23` hardening follow-up:
|
||||
|
||||
- `website/` production audit is clean
|
||||
- `Content/Browser/` production audit is clean and now has a checked-in
|
||||
`package-lock.json`
|
||||
- `website/server/` was upgraded to `supertokens-node@24.0.2`, but still
|
||||
carries an upstream `supertokens-node -> nodemailer@8.0.11` advisory in the
|
||||
production audit
|
||||
- `scripts/run-hypertwist-web-surface-validation.sh` accepts that exact
|
||||
documented upstream auth-server residual in default mode, but
|
||||
`--strict-auth-server-audit` turns it back into a blocking failure
|
||||
- do not hide that auth-server advisory with an unsupported forced major
|
||||
override of `nodemailer` unless later upstream compatibility proof exists
|
||||
|
||||
Current packaged-proof bridge truth after the same `2026-06-23` continuation:
|
||||
|
||||
- the website/browser lane no longer relies on a stale hand-maintained Windows
|
||||
packaged-validation summary constant
|
||||
- `scripts/render-hypertwist-web-package-validation-summary.mjs` now renders a
|
||||
sanitized summary from
|
||||
`docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`
|
||||
into
|
||||
`website/src/shared/generated/windows-package-validation-summary.json`
|
||||
- `scripts/run-hypertwist-web-surface-validation.sh` now checks that generated
|
||||
summary for freshness before it validates the current website/auth-server/
|
||||
embedded-browser surface
|
||||
|
||||
## Canonical development authorities
|
||||
|
||||
Use these before widening implementation:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -93,6 +93,15 @@ Current consolidated milestone snapshot:
|
|||
`BuildCookRun time: 118.53 s`, both dedicated-family packaged smoke maps
|
||||
green, and pulled aggregate proof in
|
||||
`docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`
|
||||
- that same maintained higher-dimensional package lane was then refreshed again
|
||||
on `2026-06-23` after the recovered authoritative editor rebuild, still
|
||||
through the primary reverse-SSH `localhost:22022` lane against isolated
|
||||
Windows worktree `C:\HyperTwist_worktrees\phase10validate`, again using
|
||||
`-SkipBuild` on the already-present game receipt, archive output in
|
||||
`C:\HyperTwist_worktrees\phase10validate_packaged_phase6c_higherdim_refresh_20260623`,
|
||||
`BuildCookRun time: 70.74 s`, both dedicated-family packaged smoke maps
|
||||
green again, and refreshed aggregate proof in
|
||||
`docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`
|
||||
- the earlier dedicated-map package proof used the verified fallback reverse-SSH
|
||||
lane on `localhost:22023`, an isolated Windows worktree at
|
||||
`C:\HyperTwist_worktrees\phase3to5`, authored dedicated classic/follow-along
|
||||
|
|
@ -186,6 +195,22 @@ Current consolidated milestone snapshot:
|
|||
including `CoachDashboard.RuntimeInspectSurface`,
|
||||
`TrainingPanel.RuntimeInspectSurface`, and
|
||||
`Widget.OperatorStatusSurfaceLive`
|
||||
- that same embedded-browser lane then gained a native control/input readiness
|
||||
continuation on `2026-06-23`, adding
|
||||
`FHyperTwistTrainingControlInputReadinessInspectSurface` plus native
|
||||
training-panel and coach-dashboard ownership for the shipped classic keyboard
|
||||
profile, classic-cube mouse/touch/shortcut readiness, higher-dimensional
|
||||
dedicated-family readiness, project-level `EnhancedInput` and motion-control
|
||||
groundwork, and explicit unfinished XR/preferences truth; the recovered
|
||||
primary reverse-SSH `localhost:22022` lane rebuilt the maintained validation
|
||||
root `C:\HyperTwist_worktrees\phase10validate` with `Result: Succeeded`,
|
||||
UnrealBuildTool `Total execution time: 1563.53 seconds`, then exported
|
||||
`Saved\AutomationReports\Browser-ControlInputReadiness-Verify\index.json`
|
||||
with `16` `HyperTwist.Browser.*` tests succeeded and `0` failed, including
|
||||
`CoachDashboard.ControlInputReadinessInspectSurface`,
|
||||
`TrainingPanel.ControlInputReadinessInspectSurface`,
|
||||
`CoachDashboard.RuntimeInspectSurface`, and
|
||||
`TrainingPanel.RuntimeInspectSurface`
|
||||
- the public HyperTwist website/distribution lane is now also live through a
|
||||
first-party `website/` app on `2026-06-22`, with rebranded
|
||||
home/about/resources/pricing/download/legal pages, a protected browser
|
||||
|
|
@ -245,7 +270,20 @@ Current consolidated milestone snapshot:
|
|||
panel, and the public preview-versus-launch callouts all consume one shared
|
||||
release-metadata truth for version/channel/build/checksum/docs/source
|
||||
posture while keeping raw download URLs hidden from anonymous viewers and
|
||||
exposing them only to entitled session-backed viewers,
|
||||
exposing them only to entitled session-backed viewers, while the protected
|
||||
fallback path now also preserves signed-in viewer plan/access posture from
|
||||
local session truth when live manifest authority is temporarily unavailable
|
||||
without promoting that fallback into raw package-delivery authority, and the
|
||||
protected download-center lane now also follows the resolved manifest viewer
|
||||
entitlement for the actual download action instead of relying only on the
|
||||
local auth snapshot, so fresher server-backed access truth can unblock an
|
||||
operator even before local session posture has fully caught up, while the
|
||||
shared browser auth layer now also refreshes and persists the local session
|
||||
snapshot from that fresher manifest viewer truth and the protected dashboard
|
||||
plus account surfaces now surface a compact live-authority sync notice
|
||||
summarizing what changed, and the same auth layer now also clears stale
|
||||
stored sessions when shared auth is configured and `/api/auth/me` returns
|
||||
`401` instead of preserving a false authenticated protected-shell posture,
|
||||
and the runtime-readiness verifier now also probes the live anonymous
|
||||
`/api/releases/manifest` route plus the deployed root website shell marker
|
||||
so it can explicitly fail when `hypertwist.app` is still serving the older
|
||||
|
|
@ -295,6 +333,13 @@ Current consolidated milestone snapshot:
|
|||
and protected download surfaces can show real packaged proof for
|
||||
`Magic120Cell` and `MagicCube5D` even before the launch-tier release URLs are
|
||||
populated,
|
||||
and a later same-family hardening continuation then replaced the stale
|
||||
hand-maintained website validation constant with a sanitized generated
|
||||
summary rendered from
|
||||
`docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`,
|
||||
with the owned `scripts/run-hypertwist-web-surface-validation.sh` gate now
|
||||
checking that generated web summary for freshness before approving the
|
||||
public/auth/download/browser surface,
|
||||
and a follow-on same-family marketing-shell continuation now also exposes a
|
||||
compact first-party public-site-status banner across the shared marketing
|
||||
shell, keeps a fuller status section on the homepage, and tightens the
|
||||
|
|
@ -496,6 +541,15 @@ Current consolidated milestone snapshot:
|
|||
`BuildCookRun time: 118.53 s`, both dedicated-family packaged smoke maps
|
||||
green, and pulled aggregate proof in
|
||||
`docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`
|
||||
- that same maintained higher-dimensional package lane was refreshed again on
|
||||
`2026-06-23` after the recovered authoritative editor rebuild, still against
|
||||
isolated Windows worktree `C:\HyperTwist_worktrees\phase10validate` and
|
||||
still using `-SkipBuild` on the already-present game receipt, with archive
|
||||
output in
|
||||
`C:\HyperTwist_worktrees\phase10validate_packaged_phase6c_higherdim_refresh_20260623`,
|
||||
`BuildCookRun time: 70.74 s`, both dedicated-family packaged smoke maps
|
||||
green again, and refreshed aggregate proof in
|
||||
`docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`
|
||||
- the functional `Phase 6C` ownership gap is now closed; the remaining work
|
||||
in this lane is optional further micro-profiling and richer non-headless
|
||||
decorative/manual-authored family map dressing, not missing activation,
|
||||
|
|
|
|||
130
scripts/render-hypertwist-web-package-validation-summary.mjs
Normal file
130
scripts/render-hypertwist-web-package-validation-summary.mjs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
#!/usr/bin/env node
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const repoRoot = path.resolve(__dirname, '..')
|
||||
const sourcePath = path.join(
|
||||
repoRoot,
|
||||
'docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json',
|
||||
)
|
||||
const outputPath = path.join(
|
||||
repoRoot,
|
||||
'website/src/shared/generated/windows-package-validation-summary.json',
|
||||
)
|
||||
|
||||
const FAMILY_LABELS = {
|
||||
magic120cell: 'Magic120Cell dedicated-family training map',
|
||||
magiccube5d: 'MagicCube5D dedicated-family training map',
|
||||
}
|
||||
|
||||
function parseJson(content, label) {
|
||||
try {
|
||||
return JSON.parse(content)
|
||||
} catch (error) {
|
||||
throw new Error(`${label} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
function assertString(value, label) {
|
||||
if (typeof value !== 'string' || value.trim().length === 0) {
|
||||
throw new Error(`${label} must be a non-empty string.`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function assertBoolean(value, label) {
|
||||
if (typeof value !== 'boolean') {
|
||||
throw new Error(`${label} must be a boolean.`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function normalizeSmokeResult(value, label) {
|
||||
if (value === 'passed' || value === 'failed') {
|
||||
return value
|
||||
}
|
||||
throw new Error(`${label} must be "passed" or "failed".`)
|
||||
}
|
||||
|
||||
function buildFallbackLabel(mapUrl) {
|
||||
const mapName = mapUrl.split('/').pop() || mapUrl
|
||||
return `${mapName} packaged smoke map`
|
||||
}
|
||||
|
||||
function buildSummary(report) {
|
||||
const generatedAtUtc = assertString(report.generatedAtUtc, 'generatedAtUtc')
|
||||
const configuration = assertString(report.configuration, 'configuration')
|
||||
const skipBuild = assertBoolean(report.skipBuild, 'skipBuild')
|
||||
const result = normalizeSmokeResult(report.result, 'result')
|
||||
const validatedEntries = Array.isArray(report.authoringManifest?.validatedEntries)
|
||||
? report.authoringManifest.validatedEntries
|
||||
: []
|
||||
const mapKindByUrl = new Map(
|
||||
validatedEntries
|
||||
.filter((entry) => typeof entry?.mapAssetPath === 'string' && typeof entry?.mapKind === 'string')
|
||||
.map((entry) => [entry.mapAssetPath, entry.mapKind]),
|
||||
)
|
||||
const smokeReports = Array.isArray(report.smokeReports) ? report.smokeReports : []
|
||||
|
||||
const smokeMaps = smokeReports.map((smokeReport, index) => {
|
||||
const mapUrl = assertString(smokeReport.mapUrl, `smokeReports[${index}].mapUrl`)
|
||||
const mapKind = mapKindByUrl.get(mapUrl)
|
||||
const label =
|
||||
(mapKind && FAMILY_LABELS[mapKind]) ||
|
||||
buildFallbackLabel(mapUrl)
|
||||
|
||||
return {
|
||||
map_url: mapUrl,
|
||||
label,
|
||||
result: normalizeSmokeResult(smokeReport.result, `smokeReports[${index}].result`),
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
lane: 'Windows Unreal packaged validation',
|
||||
result,
|
||||
generated_at: generatedAtUtc,
|
||||
configuration,
|
||||
skip_build: skipBuild,
|
||||
smoke_map_count: smokeMaps.length,
|
||||
smoke_maps: smokeMaps,
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const checkOnly = process.argv.includes('--check')
|
||||
const sourceContent = await fs.readFile(sourcePath, 'utf8')
|
||||
const summary = buildSummary(parseJson(sourceContent, sourcePath))
|
||||
const serialized = `${JSON.stringify(summary, null, 2)}\n`
|
||||
|
||||
if (checkOnly) {
|
||||
let existing = null
|
||||
try {
|
||||
existing = await fs.readFile(outputPath, 'utf8')
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
||||
console.error(`Generated summary missing: ${outputPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
if (existing !== serialized) {
|
||||
console.error('Generated web package-validation summary is stale.')
|
||||
console.error(`Refresh with: node ${path.relative(repoRoot, __filename)}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`Validated packaged-validation summary freshness: ${path.relative(repoRoot, outputPath)}`)
|
||||
return
|
||||
}
|
||||
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true })
|
||||
await fs.writeFile(outputPath, serialized, 'utf8')
|
||||
console.log(`Wrote ${path.relative(repoRoot, outputPath)}`)
|
||||
}
|
||||
|
||||
await main()
|
||||
|
|
@ -5,6 +5,7 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|||
local_gitnexus_cli="$repo_root/mirrors/GitNexus/gitnexus/dist/cli/index.js"
|
||||
analysis_root="$repo_root/.gitnexus-source-only-root"
|
||||
max_size_kb="${HYPERTWIST_GITNEXUS_MAX_FILE_SIZE_KB:-256}"
|
||||
analysis_timeout_seconds="${HYPERTWIST_GITNEXUS_ANALYZE_TIMEOUT_SECONDS:-300}"
|
||||
|
||||
copy_targets=(
|
||||
"UnrealHyperTwist/Source"
|
||||
|
|
@ -40,12 +41,98 @@ git -C "$analysis_root" commit -q -m "source-only snapshot"
|
|||
|
||||
cd "$analysis_root"
|
||||
|
||||
run_analysis_command() {
|
||||
local -n command_ref="$1"
|
||||
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout "${analysis_timeout_seconds}s" "${command_ref[@]}"
|
||||
else
|
||||
"${command_ref[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
read_indexed_commit() {
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const path = ".gitnexus/meta.json";
|
||||
if (!fs.existsSync(path)) {
|
||||
process.exit(1);
|
||||
}
|
||||
const meta = JSON.parse(fs.readFileSync(path, "utf8"));
|
||||
if (typeof meta.lastCommit !== "string" || meta.lastCommit.length === 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write(meta.lastCommit);
|
||||
' 2>/dev/null || true
|
||||
}
|
||||
|
||||
run_analysis_attempt() {
|
||||
local cli_kind="$1"
|
||||
shift
|
||||
local command=("$@")
|
||||
local analysis_exit_code=0
|
||||
|
||||
set +e
|
||||
if [[ "$cli_kind" == "local" ]]; then
|
||||
run_analysis_command command 2>/dev/null
|
||||
else
|
||||
run_analysis_command command
|
||||
fi
|
||||
analysis_exit_code=$?
|
||||
set -e
|
||||
|
||||
if [[ "$analysis_exit_code" -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local current_commit
|
||||
current_commit="$(git rev-parse HEAD)"
|
||||
local indexed_commit
|
||||
indexed_commit="$(read_indexed_commit)"
|
||||
|
||||
if [[ "$analysis_exit_code" -eq 124 || "$analysis_exit_code" -eq 137 ]]; then
|
||||
if [[ -n "$indexed_commit" && "$indexed_commit" == "$current_commit" ]]; then
|
||||
echo "GitNexus analyze hit the configured timeout after indexing the current bounded mirror; treating the run as successful." >&2
|
||||
echo "Indexed commit: ${indexed_commit:0:7} (${cli_kind} CLI, timeout ${analysis_timeout_seconds}s)." >&2
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$indexed_commit" ]]; then
|
||||
echo "GitNexus analyze exited with code $analysis_exit_code after indexing commit ${indexed_commit:0:7}, which did not match current mirror commit ${current_commit:0:7}." >&2
|
||||
else
|
||||
echo "GitNexus analyze exited with code $analysis_exit_code before a readable bounded-mirror index result was available." >&2
|
||||
fi
|
||||
|
||||
return "$analysis_exit_code"
|
||||
}
|
||||
|
||||
local_cli_command=(
|
||||
node
|
||||
"$local_gitnexus_cli"
|
||||
analyze
|
||||
.
|
||||
--skip-agents-md
|
||||
)
|
||||
|
||||
npx_cli_command=(
|
||||
npx
|
||||
-y
|
||||
gitnexus@latest
|
||||
analyze
|
||||
.
|
||||
--skip-agents-md
|
||||
)
|
||||
|
||||
local_cli_command+=("$@")
|
||||
npx_cli_command+=("$@")
|
||||
|
||||
if [[ -f "$local_gitnexus_cli" ]]; then
|
||||
if node "$local_gitnexus_cli" analyze . --skip-agents-md "$@" 2>/dev/null; then
|
||||
if run_analysis_attempt "local" "${local_cli_command[@]}"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Local retained GitNexus CLI was not runnable on this host. Falling back to npx gitnexus@latest." >&2
|
||||
echo "Local retained GitNexus CLI did not complete cleanly on this host. Falling back to npx gitnexus@latest." >&2
|
||||
fi
|
||||
|
||||
exec npx -y gitnexus@latest analyze . --skip-agents-md "$@"
|
||||
run_analysis_attempt "npx" "${npx_cli_command[@]}"
|
||||
|
|
|
|||
168
scripts/run-hypertwist-web-surface-validation.sh
Normal file
168
scripts/run-hypertwist-web-surface-validation.sh
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
strict_auth_server_audit=0
|
||||
skip_audits=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/run-hypertwist-web-surface-validation.sh [--strict-auth-server-audit] [--skip-audits]
|
||||
|
||||
Runs the current first-party HyperTwist web-surface validation packet:
|
||||
- website type-check, focused tests, and production build
|
||||
- website/server type-check and full test suite
|
||||
- Content/Browser shell verification and production build
|
||||
- production dependency audits for website, website/server, and Content/Browser
|
||||
|
||||
Current auth-server nuance:
|
||||
the default mode accepts the known upstream `supertokens-node -> nodemailer`
|
||||
production advisory as a documented residual warning. Pass
|
||||
`--strict-auth-server-audit` to fail on that residual too.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--strict-auth-server-audit)
|
||||
strict_auth_server_audit=1
|
||||
;;
|
||||
--skip-audits)
|
||||
skip_audits=1
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
run_step() {
|
||||
local description="$1"
|
||||
shift
|
||||
printf '\n[%s] %s\n' "$(date -u '+%H:%M:%S')" "$description"
|
||||
(
|
||||
cd "$repo_root"
|
||||
"$@"
|
||||
)
|
||||
}
|
||||
|
||||
run_clean_prod_audit() {
|
||||
local workdir="$1"
|
||||
local label="$2"
|
||||
|
||||
printf '\n[%s] %s production audit\n' "$(date -u '+%H:%M:%S')" "$label"
|
||||
(
|
||||
cd "$workdir"
|
||||
npm audit --omit=dev --audit-level=high
|
||||
)
|
||||
}
|
||||
|
||||
run_auth_server_audit() {
|
||||
local workdir="$repo_root/website/server"
|
||||
local audit_json
|
||||
|
||||
printf '\n[%s] website/server production audit\n' "$(date -u '+%H:%M:%S')"
|
||||
audit_json="$(
|
||||
cd "$workdir"
|
||||
npm audit --omit=dev --json || true
|
||||
)"
|
||||
|
||||
if [[ -z "$audit_json" ]]; then
|
||||
echo "website/server audit returned no output." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
AUDIT_JSON="$audit_json" STRICT_AUTH_SERVER_AUDIT="$strict_auth_server_audit" node <<'EOF'
|
||||
const report = JSON.parse(process.env.AUDIT_JSON || '{}')
|
||||
const strict = process.env.STRICT_AUTH_SERVER_AUDIT === '1'
|
||||
const vulnerabilities = report.vulnerabilities || {}
|
||||
const metadata = report.metadata?.vulnerabilities || {}
|
||||
const total = Number(metadata.total || 0)
|
||||
|
||||
if (total === 0) {
|
||||
console.log('website/server production audit is clean.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const keys = Object.keys(vulnerabilities).sort()
|
||||
const expectedKeys = ['nodemailer', 'supertokens-node']
|
||||
const nodemailer = vulnerabilities.nodemailer
|
||||
const supertokens = vulnerabilities['supertokens-node']
|
||||
const matchesKnownResidual =
|
||||
metadata.info === 0 &&
|
||||
metadata.low === 0 &&
|
||||
metadata.moderate === 0 &&
|
||||
metadata.high === 2 &&
|
||||
metadata.critical === 0 &&
|
||||
keys.length === expectedKeys.length &&
|
||||
expectedKeys.every((key, index) => keys[index] === key) &&
|
||||
nodemailer?.name === 'nodemailer' &&
|
||||
nodemailer?.severity === 'high' &&
|
||||
Array.isArray(nodemailer?.effects) &&
|
||||
nodemailer.effects.length === 1 &&
|
||||
nodemailer.effects[0] === 'supertokens-node' &&
|
||||
Array.isArray(nodemailer?.nodes) &&
|
||||
nodemailer.nodes.length === 1 &&
|
||||
nodemailer.nodes[0] === 'node_modules/nodemailer' &&
|
||||
supertokens?.name === 'supertokens-node' &&
|
||||
supertokens?.severity === 'high' &&
|
||||
Array.isArray(supertokens?.via) &&
|
||||
supertokens.via.length === 1 &&
|
||||
supertokens.via[0] === 'nodemailer'
|
||||
|
||||
if (matchesKnownResidual && !strict) {
|
||||
console.log(
|
||||
'website/server production audit retains the documented upstream residual advisory: supertokens-node -> nodemailer.',
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
console.error('website/server production audit found unexpected or strict-mode-blocking vulnerabilities.')
|
||||
console.error(JSON.stringify(report, null, 2))
|
||||
process.exit(1)
|
||||
EOF
|
||||
}
|
||||
|
||||
run_step "website type-check" npm --prefix website run type-check
|
||||
run_step \
|
||||
"higher-dimensional packaged-validation summary freshness" \
|
||||
node scripts/render-hypertwist-web-package-validation-summary.mjs --check
|
||||
run_step \
|
||||
"website focused route/auth/release validation" \
|
||||
npm --prefix website test -- --run \
|
||||
src/__tests__/package-validation.test.ts \
|
||||
src/__tests__/platform-auth.bootstrap.test.tsx \
|
||||
src/__tests__/download-center-page.test.tsx \
|
||||
src/__tests__/protected-app-pages.test.tsx \
|
||||
src/__tests__/DashboardOverviewPage.test.tsx \
|
||||
src/__tests__/app-route-tree.test.tsx \
|
||||
src/__tests__/route-shells.test.tsx \
|
||||
src/__tests__/release-manifest.test.ts \
|
||||
src/__tests__/public-marketing-pages.test.tsx \
|
||||
src/__tests__/public-auth-pages.test.tsx
|
||||
run_step "website production build" npm --prefix website run build
|
||||
run_step "website/server type-check" npm --prefix website/server run type-check
|
||||
run_step "website/server test suite" npm --prefix website/server test -- --run
|
||||
run_step "Content/Browser shell verification" npm --prefix Content/Browser run verify:shell
|
||||
run_step "Content/Browser production build" npm --prefix Content/Browser run build
|
||||
|
||||
if [[ "$skip_audits" -eq 0 ]]; then
|
||||
run_clean_prod_audit "$repo_root/website" "website"
|
||||
run_auth_server_audit
|
||||
run_clean_prod_audit "$repo_root/Content/Browser" "Content/Browser"
|
||||
else
|
||||
printf '\n[%s] Skipping production audits by request.\n' "$(date -u '+%H:%M:%S')"
|
||||
fi
|
||||
|
||||
printf '\n[%s] HyperTwist web-surface validation completed successfully.\n' "$(date -u '+%H:%M:%S')"
|
||||
if [[ "$skip_audits" -eq 0 && "$strict_auth_server_audit" -eq 0 ]]; then
|
||||
echo "Auth-server note: known upstream supertokens-node -> nodemailer residual is accepted in default mode until upstream-compatible remediation exists."
|
||||
fi
|
||||
|
|
@ -79,12 +79,50 @@ Environment templates:
|
|||
## Validation
|
||||
|
||||
```bash
|
||||
scripts/run-hypertwist-web-surface-validation.sh
|
||||
npm run type-check
|
||||
npm run test
|
||||
npm run build
|
||||
npm run check:runtime-readiness -- --frontend-env .env --server-env server/.env --health-url https://hypertwist.app
|
||||
```
|
||||
|
||||
Related validation and audit commands for the adjacent owned surfaces:
|
||||
|
||||
```bash
|
||||
npm --prefix server run type-check
|
||||
npm --prefix server test -- --run
|
||||
npm --prefix ../Content/Browser run verify:shell
|
||||
npm --prefix ../Content/Browser run build
|
||||
npm audit --omit=dev --audit-level=high
|
||||
npm --prefix server audit --omit=dev --audit-level=high
|
||||
npm --prefix ../Content/Browser audit --omit=dev --audit-level=high
|
||||
```
|
||||
|
||||
The preferred current repo-owned umbrella gate is:
|
||||
|
||||
```bash
|
||||
scripts/run-hypertwist-web-surface-validation.sh
|
||||
```
|
||||
|
||||
That command runs the current website type-check, focused auth or release or
|
||||
route tests, website build, auth-server type-check and tests, browser-shell
|
||||
verify/build, and production audits together. It accepts the current exact
|
||||
upstream `supertokens-node -> nodemailer` auth-server residual in default mode
|
||||
and supports `--strict-auth-server-audit` when that residual should block.
|
||||
It now also checks that the website-facing Windows packaged-validation summary
|
||||
is fresh against the checked-in authoritative higher-dimensional package report.
|
||||
|
||||
Current dependency-health truth from the `2026-06-23` hardening pass:
|
||||
|
||||
- `website/` production audit is clean
|
||||
- `Content/Browser/` production audit is clean and now has a checked-in
|
||||
`package-lock.json`
|
||||
- `website/server/` was upgraded to `supertokens-node@24.0.2`
|
||||
- `website/server/` still carries one upstream production advisory through the
|
||||
supported `supertokens-node -> nodemailer@8.0.11` chain
|
||||
- no unsupported forced major `nodemailer` override was landed just to hide
|
||||
that remaining upstream advisory
|
||||
|
||||
## Release-manifest authority
|
||||
|
||||
The website now treats `GET /api/releases/manifest` from `website/server` as
|
||||
|
|
@ -190,6 +228,12 @@ Use the runtime-readiness command before public launch or deployment approval:
|
|||
- the shared marketing shell now also carries a compact first-party
|
||||
public-site-status banner across public pages, while the homepage keeps a
|
||||
fuller public-site-status section for the same preview-versus-launch truth
|
||||
- the website-facing Windows packaged proof now comes from the sanitized
|
||||
generated file
|
||||
`website/src/shared/generated/windows-package-validation-summary.json`,
|
||||
rendered from
|
||||
`docs/generated/higher_dimensional_training_maps/phase6c_dedicated_family_package_validation_report.json`
|
||||
by `scripts/render-hypertwist-web-package-validation-summary.mjs`
|
||||
- the auth server can now also serve the built `website/dist` bundle directly for same-origin `hypertwist.app` deployment when that build output is present
|
||||
- the repo now also includes first-party same-origin `nginx` and `systemd`
|
||||
handoff templates under `website/deploy/`
|
||||
|
|
|
|||
26
website/server/package-lock.json
generated
26
website/server/package-lock.json
generated
|
|
@ -12,7 +12,7 @@
|
|||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"supertokens-node": "^21.1.0"
|
||||
"supertokens-node": "^24.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "^1.4.8",
|
||||
|
|
@ -1391,9 +1391,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
|
||||
"integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
|
||||
"integrity": "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.7.0"
|
||||
|
|
@ -2217,9 +2217,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "6.10.1",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
|
||||
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
|
||||
"version": "8.0.11",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.11.tgz",
|
||||
"integrity": "sha512-nrO/pDAUKl+wXX+lx16tDLbnm0fW6sK/x8mgohaCpg+CdCEl482bD4tCuAZk2DyliruiNTIZxRCoWkDqJEnAiA==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
|
|
@ -2689,23 +2689,23 @@
|
|||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/supertokens-node": {
|
||||
"version": "21.1.2",
|
||||
"resolved": "https://registry.npmjs.org/supertokens-node/-/supertokens-node-21.1.2.tgz",
|
||||
"integrity": "sha512-YX5qxT/cP/qsuvsH9j8oSsOdkvJhOk4eM+pGpJ0YToj4LZMa8FIUFnh4SDuAO/AC4SuTeIyhysMG4DdDUJdz8Q==",
|
||||
"version": "24.0.2",
|
||||
"resolved": "https://registry.npmjs.org/supertokens-node/-/supertokens-node-24.0.2.tgz",
|
||||
"integrity": "sha512-L4n0zchL3K36aMgUWT5WVuNCw7KWWGtZq//Ti+jWcyRphb/sVaKM76fI8I2/JBqTIGXre9dAQx3myETRSA+etQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"buffer": "^6.0.3",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.2",
|
||||
"cross-fetch": "^3.1.6",
|
||||
"cross-fetch": "^4.1.0",
|
||||
"debug": "^4.3.3",
|
||||
"jose": "^4.13.1",
|
||||
"libphonenumber-js": "^1.9.44",
|
||||
"nodemailer": "^6.7.2",
|
||||
"nodemailer": "^8.0.2",
|
||||
"pako": "^2.1.0",
|
||||
"pkce-challenge": "^3.0.0",
|
||||
"process": "^0.11.10",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"set-cookie-parser": "^2.7.1",
|
||||
"supertokens-js-override": "^0.0.4",
|
||||
"tldts": "^6.1.48",
|
||||
"twilio": "^4.19.3"
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"supertokens-node": "^21.1.0"
|
||||
"supertokens-node": "^24.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "^1.4.8",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
|
|
@ -8,6 +8,7 @@ const mockCreateDesktopLinkToken = vi.fn()
|
|||
const mockGetAuthHealth = vi.fn()
|
||||
const mockBuildAuthApiBaseUrls = vi.fn(() => ['https://hypertwist.app'])
|
||||
const mockGetReleaseManifest = vi.fn()
|
||||
const mockInvalidateSession = vi.fn()
|
||||
|
||||
vi.mock('../auth/platform-auth', () => ({
|
||||
usePlatformAuth: () => mockUsePlatformAuth(),
|
||||
|
|
@ -40,11 +41,13 @@ function renderPage() {
|
|||
|
||||
describe('DashboardOverviewPage', () => {
|
||||
beforeEach(() => {
|
||||
cleanup()
|
||||
mockUsePlatformAuth.mockReset()
|
||||
mockCreateDesktopLinkToken.mockReset()
|
||||
mockGetAuthHealth.mockReset()
|
||||
mockBuildAuthApiBaseUrls.mockReset()
|
||||
mockGetReleaseManifest.mockReset()
|
||||
mockInvalidateSession.mockReset()
|
||||
mockBuildAuthApiBaseUrls.mockReturnValue(['https://hypertwist.app'])
|
||||
mockGetReleaseManifest.mockResolvedValue({
|
||||
ok: true,
|
||||
|
|
@ -120,6 +123,9 @@ describe('DashboardOverviewPage', () => {
|
|||
},
|
||||
},
|
||||
superTokensConfigured: true,
|
||||
invalidateSession: (...args: unknown[]) => mockInvalidateSession(...args),
|
||||
reconcileReleaseAuthority: vi.fn(),
|
||||
releaseAuthoritySyncItems: [],
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -179,8 +185,11 @@ describe('DashboardOverviewPage', () => {
|
|||
expect(screen.getByText(/Public auth runtime still uses local or mixed deployment posture/i)).toBeTruthy()
|
||||
expect(screen.getByText('Packaged validation passed')).toBeTruthy()
|
||||
expect(screen.getByText(/Magic120Cell dedicated-family training map: passed/i)).toBeTruthy()
|
||||
expect(screen.getByText('Desktop rollout follow-through')).toBeTruthy()
|
||||
expect(screen.getByText('Pair desktop access to the browser account')).toBeTruthy()
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /generate desktop-link token/i }))
|
||||
const generateDesktopLinkButtons = screen.getAllByRole('button', { name: /generate desktop-link token/i })
|
||||
await userEvent.click(generateDesktopLinkButtons[generateDesktopLinkButtons.length - 1]!)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText((content) => content === 'desktop-token-123')).toBeTruthy()
|
||||
|
|
@ -188,4 +197,183 @@ describe('DashboardOverviewPage', () => {
|
|||
|
||||
expect(screen.getByText(/https:\/\/hypertwist\.app\/api\/auth\/desktop-link\/verify\?token=desktop-token-123/i)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('invalidates the protected browser session when desktop-link issuance returns unauthorized', async () => {
|
||||
mockGetAuthHealth.mockResolvedValue({
|
||||
ok: true,
|
||||
service: 'hypertwist-auth-server',
|
||||
supertokens: {
|
||||
configured: true,
|
||||
reachable: true,
|
||||
ready: true,
|
||||
apiVersion: '5.4',
|
||||
oauth: {
|
||||
github: false,
|
||||
google: false,
|
||||
},
|
||||
},
|
||||
fallback: {
|
||||
enabled: true,
|
||||
active: false,
|
||||
reason: null,
|
||||
},
|
||||
billing: {
|
||||
statePath: '/tmp/hypertwist-billing.json',
|
||||
processedEventCount: 1,
|
||||
pricePlanMapConfigured: true,
|
||||
productPlanMapConfigured: true,
|
||||
webhookSecretConfigured: true,
|
||||
},
|
||||
runtime: {
|
||||
mode: 'public',
|
||||
public_origin_ready: true,
|
||||
cookie_secure: true,
|
||||
api_domain: 'https://hypertwist.app',
|
||||
website_domain: 'https://hypertwist.app',
|
||||
warnings: [],
|
||||
errors: [],
|
||||
},
|
||||
})
|
||||
|
||||
mockCreateDesktopLinkToken.mockRejectedValue(Object.assign(new Error('unauthorized'), { status: 401 }))
|
||||
|
||||
renderPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/Checking auth server health/i)).toBeNull()
|
||||
})
|
||||
|
||||
const generateDesktopLinkButtons = screen.getAllByRole('button', { name: /generate desktop-link token/i })
|
||||
await userEvent.click(generateDesktopLinkButtons[generateDesktopLinkButtons.length - 1]!)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInvalidateSession).toHaveBeenCalledWith('desktop_link_unauthorized')
|
||||
})
|
||||
})
|
||||
|
||||
it('surfaces live authority sync details after release access has been refreshed from live manifest truth', async () => {
|
||||
mockGetAuthHealth.mockResolvedValue({
|
||||
ok: true,
|
||||
service: 'hypertwist-auth-server',
|
||||
supertokens: {
|
||||
configured: true,
|
||||
reachable: true,
|
||||
ready: true,
|
||||
apiVersion: '5.4',
|
||||
oauth: {
|
||||
github: false,
|
||||
google: false,
|
||||
},
|
||||
},
|
||||
fallback: {
|
||||
enabled: true,
|
||||
active: false,
|
||||
reason: null,
|
||||
},
|
||||
billing: {
|
||||
statePath: '/tmp/hypertwist-billing.json',
|
||||
processedEventCount: 1,
|
||||
pricePlanMapConfigured: true,
|
||||
productPlanMapConfigured: true,
|
||||
webhookSecretConfigured: true,
|
||||
},
|
||||
runtime: {
|
||||
mode: 'public',
|
||||
public_origin_ready: true,
|
||||
cookie_secure: true,
|
||||
api_domain: 'https://hypertwist.app',
|
||||
website_domain: 'https://hypertwist.app',
|
||||
warnings: [],
|
||||
errors: [],
|
||||
},
|
||||
})
|
||||
|
||||
mockUsePlatformAuth.mockReturnValue({
|
||||
user: {
|
||||
id: 'operator-1',
|
||||
name: 'Operator',
|
||||
email: 'operator@hypertwist.app',
|
||||
authMethod: 'email',
|
||||
plan: 'free',
|
||||
role: 'viewer',
|
||||
canDownload: false,
|
||||
billing: {
|
||||
source: 'session',
|
||||
accessStatus: 'session-default',
|
||||
canDownload: false,
|
||||
lastEventType: 'transaction.completed',
|
||||
},
|
||||
},
|
||||
superTokensConfigured: true,
|
||||
reconcileReleaseAuthority: vi.fn(),
|
||||
invalidateSession: (...args: unknown[]) => mockInvalidateSession(...args),
|
||||
releaseAuthoritySyncItems: [
|
||||
'Plan changed from local free to live operator.',
|
||||
'Desktop download access changed from local not entitled to live enabled.',
|
||||
],
|
||||
})
|
||||
|
||||
mockGetReleaseManifest.mockResolvedValue({
|
||||
ok: true,
|
||||
manifest: {
|
||||
generated_at: '2026-06-23T12:00:00.000Z',
|
||||
support_email: 'hello@hypertwist.app',
|
||||
public_docs_url: 'https://docs.hypertwist.app',
|
||||
release_notes_url: 'https://notes.hypertwist.app',
|
||||
corresponding_source_url: 'https://hypertwist.app/open-source/source.zip',
|
||||
open_source_repo_url: 'https://github.com/hypertwist/hypertwist',
|
||||
viewer: {
|
||||
authenticated: true,
|
||||
canDownload: true,
|
||||
plan: 'operator',
|
||||
role: 'operator',
|
||||
accessStatus: 'active',
|
||||
},
|
||||
platforms: [
|
||||
{
|
||||
platform_key: 'windows',
|
||||
platform: 'Windows',
|
||||
subtitle: 'Primary shipping lane',
|
||||
details: 'Current packaged validation is strongest on the Windows Unreal lane.',
|
||||
configured: true,
|
||||
channel: 'candidate',
|
||||
version: '1.0.1',
|
||||
build_id: 'win64-1001',
|
||||
published_at: '2026-06-23T00:00:00.000Z',
|
||||
file_name: 'HyperTwist-Windows.zip',
|
||||
file_size_bytes: 1048576,
|
||||
checksum_sha256: 'abc124',
|
||||
download_url: 'https://downloads.hypertwist.app/windows.exe',
|
||||
download_available: true,
|
||||
validation_summary: {
|
||||
lane: 'Windows Unreal packaged validation',
|
||||
result: 'passed',
|
||||
generated_at: '2026-06-23T01:43:08.7625247Z',
|
||||
configuration: 'Development',
|
||||
skip_build: true,
|
||||
smoke_map_count: 2,
|
||||
smoke_maps: [
|
||||
{
|
||||
map_url: '/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining',
|
||||
label: 'Magic120Cell dedicated-family training map',
|
||||
result: 'passed',
|
||||
},
|
||||
{
|
||||
map_url: '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining',
|
||||
label: 'MagicCube5D dedicated-family training map',
|
||||
result: 'passed',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
renderPage()
|
||||
|
||||
expect(await screen.findByText('Live authority sync')).toBeTruthy()
|
||||
expect(screen.getByText(/plan changed from local free to live operator/i)).toBeTruthy()
|
||||
expect(screen.getByText(/desktop download access changed from local not entitled to live enabled/i)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,20 +1,17 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ROUTER_FUTURE_FLAGS } from '../router/router-future'
|
||||
|
||||
const mockGetReleaseManifest = vi.fn()
|
||||
const mockBuildAuthApiBaseUrls = vi.fn(() => ['https://hypertwist.app'])
|
||||
const mockCreateDesktopLinkToken = vi.fn()
|
||||
const mockGetAuthHealth = vi.fn()
|
||||
const mockUsePlatformAuth = vi.fn()
|
||||
|
||||
vi.mock('../auth/platform-auth', () => ({
|
||||
usePlatformAuth: () => ({
|
||||
user: {
|
||||
canDownload: true,
|
||||
},
|
||||
}),
|
||||
usePlatformAuth: () => mockUsePlatformAuth(),
|
||||
}))
|
||||
|
||||
vi.mock('../auth/auth-api', () => ({
|
||||
|
|
@ -72,21 +69,21 @@ vi.mock('../site-config', () => ({
|
|||
platformKey: 'windows',
|
||||
platform: 'Windows',
|
||||
subtitle: 'Primary shipping lane',
|
||||
href: 'https://downloads.hypertwist.app/windows.exe',
|
||||
configured: true,
|
||||
details: 'Current packaged validation is strongest on the Windows Unreal lane.',
|
||||
},
|
||||
{
|
||||
platformKey: 'macos',
|
||||
platform: 'macOS',
|
||||
subtitle: 'Planned distribution surface',
|
||||
href: 'https://downloads.hypertwist.app/macos.dmg',
|
||||
configured: true,
|
||||
details: 'List a signed desktop build here when the package lane is opened.',
|
||||
},
|
||||
{
|
||||
platformKey: 'linux',
|
||||
platform: 'Linux',
|
||||
subtitle: 'Operator-targeted later lane',
|
||||
href: '',
|
||||
configured: false,
|
||||
details: 'Use for future package publication after the bounded release lane is widened.',
|
||||
},
|
||||
],
|
||||
|
|
@ -108,6 +105,21 @@ vi.mock('../site-config', () => ({
|
|||
import { DownloadCenterPage } from '../pages/app-pages'
|
||||
|
||||
describe('DownloadCenterPage', () => {
|
||||
beforeEach(() => {
|
||||
cleanup()
|
||||
mockUsePlatformAuth.mockReset()
|
||||
mockGetReleaseManifest.mockReset()
|
||||
mockBuildAuthApiBaseUrls.mockReset()
|
||||
mockCreateDesktopLinkToken.mockReset()
|
||||
mockGetAuthHealth.mockReset()
|
||||
mockBuildAuthApiBaseUrls.mockReturnValue(['https://hypertwist.app'])
|
||||
mockUsePlatformAuth.mockReturnValue({
|
||||
user: {
|
||||
canDownload: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('surfaces the requested protected release target when the public site passes a platform hint through auth', async () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
|
|
@ -186,10 +198,123 @@ describe('DownloadCenterPage', () => {
|
|||
expect(screen.getAllByText('Requested target').length).toBeGreaterThan(0)
|
||||
expect((await screen.findByRole('link', { name: /download windows/i })).getAttribute('href')).toBe('https://downloads.hypertwist.app/windows.exe')
|
||||
expect(screen.getByText('Release guide')).toBeTruthy()
|
||||
expect(screen.getByText('First launch follow-through')).toBeTruthy()
|
||||
expect(screen.getByText('Verify the current training lanes')).toBeTruthy()
|
||||
expect(await screen.findByText('Version: 1.0.0')).toBeTruthy()
|
||||
expect(screen.getByText('Build: win64-1000')).toBeTruthy()
|
||||
expect(screen.getByText('Packaged validation passed')).toBeTruthy()
|
||||
expect(screen.getByText(/MagicCube5D dedicated-family training map: passed/i)).toBeTruthy()
|
||||
expect(document.title).toBe('HyperTwist downloads | HyperTwist')
|
||||
})
|
||||
|
||||
it('trusts the live manifest viewer posture when local auth state is stale about download entitlement', async () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
mockUsePlatformAuth.mockReturnValue({
|
||||
user: {
|
||||
canDownload: false,
|
||||
},
|
||||
})
|
||||
|
||||
mockGetReleaseManifest.mockResolvedValue({
|
||||
ok: true,
|
||||
manifest: {
|
||||
generated_at: '2026-06-23T12:00:00.000Z',
|
||||
support_email: 'hello@hypertwist.app',
|
||||
public_docs_url: 'https://docs.hypertwist.app',
|
||||
release_notes_url: 'https://notes.hypertwist.app',
|
||||
corresponding_source_url: 'https://hypertwist.app/open-source/source.zip',
|
||||
open_source_repo_url: 'https://github.com/hypertwist/hypertwist',
|
||||
viewer: {
|
||||
authenticated: true,
|
||||
canDownload: true,
|
||||
plan: 'operator',
|
||||
role: 'operator',
|
||||
accessStatus: 'active',
|
||||
},
|
||||
platforms: [
|
||||
{
|
||||
platform_key: 'windows',
|
||||
platform: 'Windows',
|
||||
subtitle: 'Primary shipping lane',
|
||||
details: 'Current packaged validation is strongest on the Windows Unreal lane.',
|
||||
configured: true,
|
||||
channel: 'candidate',
|
||||
version: '1.0.1',
|
||||
build_id: 'win64-1001',
|
||||
published_at: '2026-06-23T00:00:00.000Z',
|
||||
file_name: 'HyperTwist-Windows.zip',
|
||||
file_size_bytes: 1048576,
|
||||
checksum_sha256: 'abc124',
|
||||
download_url: 'https://downloads.hypertwist.app/windows.exe',
|
||||
download_available: true,
|
||||
validation_summary: {
|
||||
lane: 'Windows Unreal packaged validation',
|
||||
result: 'passed',
|
||||
generated_at: '2026-06-23T01:43:08.7625247Z',
|
||||
configuration: 'Development',
|
||||
skip_build: true,
|
||||
smoke_map_count: 2,
|
||||
smoke_maps: [
|
||||
{
|
||||
map_url: '/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining',
|
||||
label: 'Magic120Cell dedicated-family training map',
|
||||
result: 'passed',
|
||||
},
|
||||
{
|
||||
map_url: '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining',
|
||||
label: 'MagicCube5D dedicated-family training map',
|
||||
result: 'passed',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={['/app/downloads?platform=windows']} future={ROUTER_FUTURE_FLAGS}>
|
||||
<DownloadCenterPage />
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
expect((await screen.findByRole('link', { name: /download windows/i })).getAttribute('href')).toBe('https://downloads.hypertwist.app/windows.exe')
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(/does not yet have desktop download entitlement/i)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps entitled fallback posture honest when live release authority is temporarily unavailable', async () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
mockGetReleaseManifest.mockRejectedValue(new Error('release manifest unavailable'))
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={['/app/downloads?platform=windows']} future={ROUTER_FUTURE_FLAGS}>
|
||||
<DownloadCenterPage />
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
expect(await screen.findByText(/fallback release posture/i)).toBeTruthy()
|
||||
expect(screen.getByText(/your signed-in account still resolves to desktop access/i)).toBeTruthy()
|
||||
expect(screen.getAllByText('Live release authority temporarily unavailable').length).toBeGreaterThan(0)
|
||||
expect(screen.queryByText(/account not entitled yet/i)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
31
website/src/__tests__/package-validation.test.ts
Normal file
31
website/src/__tests__/package-validation.test.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getReleasePlatformValidationSummary } from '../shared/package-validation'
|
||||
|
||||
describe('getReleasePlatformValidationSummary', () => {
|
||||
it('reads the current Windows packaged-validation truth from the generated higher-dimensional report summary', () => {
|
||||
expect(getReleasePlatformValidationSummary('windows')).toMatchObject({
|
||||
lane: 'Windows Unreal packaged validation',
|
||||
result: 'passed',
|
||||
generated_at: '2026-06-23T10:27:31.6439999Z',
|
||||
smoke_map_count: 2,
|
||||
smoke_maps: [
|
||||
{
|
||||
map_url: '/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining',
|
||||
label: 'Magic120Cell dedicated-family training map',
|
||||
result: 'passed',
|
||||
},
|
||||
{
|
||||
map_url: '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining',
|
||||
label: 'MagicCube5D dedicated-family training map',
|
||||
result: 'passed',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps non-Windows release lanes free of fake packaged-validation summaries', () => {
|
||||
expect(getReleasePlatformValidationSummary('macos')).toBeNull()
|
||||
expect(getReleasePlatformValidationSummary('linux')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useEffect, type ReactNode } from 'react'
|
||||
|
||||
const mockGetCurrentUser = vi.fn()
|
||||
const mockLogoutCurrentUser = vi.fn()
|
||||
|
|
@ -35,7 +35,7 @@ import { PlatformAuthProvider, usePlatformAuth } from '../auth/platform-auth'
|
|||
const AUTH_STORAGE_KEY = 'hypertwist.platform.user.v1'
|
||||
|
||||
function AuthProbe() {
|
||||
const { user, isLoading } = usePlatformAuth()
|
||||
const { user, isLoading, releaseAuthoritySyncItems } = usePlatformAuth()
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -46,10 +46,33 @@ function AuthProbe() {
|
|||
<div data-testid="role">{user?.role ?? 'none'}</div>
|
||||
<div data-testid="can-download">{String(user?.canDownload ?? false)}</div>
|
||||
<div data-testid="billing-source">{user?.billing?.source ?? 'none'}</div>
|
||||
<div data-testid="sync-items">{releaseAuthoritySyncItems.join(' | ') || 'none'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ReleaseAuthorityReconcileProbe({
|
||||
viewer,
|
||||
}: {
|
||||
viewer: {
|
||||
authenticated: boolean
|
||||
canDownload: boolean
|
||||
plan: string | null
|
||||
role: string | null
|
||||
accessStatus: string | null
|
||||
}
|
||||
}) {
|
||||
const { isLoading, reconcileReleaseAuthority } = usePlatformAuth()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
reconcileReleaseAuthority(viewer)
|
||||
}
|
||||
}, [isLoading, reconcileReleaseAuthority, viewer])
|
||||
|
||||
return <AuthProbe />
|
||||
}
|
||||
|
||||
function renderWithProvider(children: ReactNode) {
|
||||
return render(<PlatformAuthProvider>{children}</PlatformAuthProvider>)
|
||||
}
|
||||
|
|
@ -160,4 +183,84 @@ describe('PlatformAuthProvider bootstrap', () => {
|
|||
expect(screen.getByTestId('billing-source').textContent).toBe('local-fallback')
|
||||
expect(mockEnsureSuperTokensInit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('clears a stale stored session when shared auth is configured and bootstrap returns unauthorized', async () => {
|
||||
seedStoredUser({
|
||||
id: 'stale-1',
|
||||
email: 'operator@hypertwist.app',
|
||||
name: 'Operator',
|
||||
authMethod: 'supertokens',
|
||||
plan: 'operator',
|
||||
role: 'operator',
|
||||
canDownload: true,
|
||||
billing: {
|
||||
source: 'session',
|
||||
accessStatus: 'active',
|
||||
canDownload: true,
|
||||
},
|
||||
})
|
||||
|
||||
mockGetCurrentUser.mockResolvedValue(createJsonResponse(401, { error: 'unauthorized' }))
|
||||
|
||||
renderWithProvider(<AuthProbe />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('loading').textContent).toBe('false')
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('user-id').textContent).toBe('none')
|
||||
expect(screen.getByTestId('plan').textContent).toBe('none')
|
||||
expect(window.localStorage.getItem(AUTH_STORAGE_KEY)).toBeNull()
|
||||
expect(mockEnsureSuperTokensInit).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('reconciles stored browser auth state from live release-authority viewer truth and persists the update', async () => {
|
||||
mockIsSuperTokensConfigured.mockReturnValue(false)
|
||||
seedStoredUser({
|
||||
id: 'local-1',
|
||||
email: 'operator@hypertwist.app',
|
||||
name: 'Operator',
|
||||
authMethod: 'email',
|
||||
plan: 'free',
|
||||
role: 'viewer',
|
||||
canDownload: false,
|
||||
billing: {
|
||||
source: 'local-fallback',
|
||||
accessStatus: 'session-default',
|
||||
canDownload: false,
|
||||
},
|
||||
})
|
||||
|
||||
mockGetCurrentUser.mockRejectedValue(new Error('offline'))
|
||||
|
||||
renderWithProvider(
|
||||
<ReleaseAuthorityReconcileProbe
|
||||
viewer={{
|
||||
authenticated: true,
|
||||
canDownload: true,
|
||||
plan: 'operator',
|
||||
role: 'operator',
|
||||
accessStatus: 'active',
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('plan').textContent).toBe('operator')
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('role').textContent).toBe('operator')
|
||||
expect(screen.getByTestId('can-download').textContent).toBe('true')
|
||||
expect(screen.getByTestId('sync-items').textContent).toContain('Plan changed from local free to live operator.')
|
||||
expect(screen.getByTestId('sync-items').textContent).toContain('Desktop download access changed from local not entitled to live enabled.')
|
||||
expect(JSON.parse(window.localStorage.getItem(AUTH_STORAGE_KEY) || '{}')).toMatchObject({
|
||||
plan: 'operator',
|
||||
role: 'operator',
|
||||
canDownload: true,
|
||||
billing: {
|
||||
accessStatus: 'active',
|
||||
canDownload: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { ROUTER_FUTURE_FLAGS } from '../router/router-future'
|
||||
|
|
@ -7,6 +7,7 @@ import { ROUTER_FUTURE_FLAGS } from '../router/router-future'
|
|||
const mockUsePlatformAuth = vi.fn()
|
||||
const mockGetAuthHealth = vi.fn()
|
||||
const mockGetReleaseManifest = vi.fn()
|
||||
const mockInvalidateSession = vi.fn()
|
||||
|
||||
vi.mock('../auth/platform-auth', () => ({
|
||||
usePlatformAuth: () => mockUsePlatformAuth(),
|
||||
|
|
@ -52,6 +53,7 @@ describe('protected app pages', () => {
|
|||
mockUsePlatformAuth.mockReset()
|
||||
mockGetAuthHealth.mockReset()
|
||||
mockGetReleaseManifest.mockReset()
|
||||
mockInvalidateSession.mockReset()
|
||||
|
||||
mockUsePlatformAuth.mockReturnValue({
|
||||
user: {
|
||||
|
|
@ -70,6 +72,9 @@ describe('protected app pages', () => {
|
|||
},
|
||||
},
|
||||
superTokensConfigured: true,
|
||||
invalidateSession: (...args: unknown[]) => mockInvalidateSession(...args),
|
||||
reconcileReleaseAuthority: vi.fn(),
|
||||
releaseAuthoritySyncItems: [],
|
||||
})
|
||||
|
||||
mockGetAuthHealth.mockResolvedValue({
|
||||
|
|
@ -188,8 +193,11 @@ describe('protected app pages', () => {
|
|||
expect(await screen.findByText('Current browser posture')).toBeTruthy()
|
||||
expect(await screen.findByText(byExactTextContent('Auth runtime mode: public', 'LI'))).toBeTruthy()
|
||||
expect(await screen.findByText(byExactTextContent('Configured release targets: 1/2', 'LI'))).toBeTruthy()
|
||||
expect(screen.getByText('Operator escalation map')).toBeTruthy()
|
||||
expect(screen.getByText('Account and entitlement issues')).toBeTruthy()
|
||||
expect(screen.getByRole('link', { name: 'Open dashboard' }).getAttribute('href')).toBe('/app')
|
||||
expect(screen.getByRole('link', { name: 'Protected notices' }).getAttribute('href')).toBe('/app/notices')
|
||||
expect(screen.getByRole('link', { name: 'Open support' }).getAttribute('href')).toBe('/support?topic=operator-access')
|
||||
})
|
||||
|
||||
it('surfaces account release access and protected follow-through links', async () => {
|
||||
|
|
@ -201,6 +209,8 @@ describe('protected app pages', () => {
|
|||
expect(await screen.findByText(byExactTextContent('Windows: Primary shipping lane (Version 1.0.0, Channel candidate)', 'LI'))).toBeTruthy()
|
||||
expect(screen.getByRole('link', { name: 'Open downloads' }).getAttribute('href')).toBe('/app/downloads')
|
||||
expect(screen.getByRole('link', { name: 'Get help' }).getAttribute('href')).toBe('/support?topic=operator-access')
|
||||
expect(screen.getByText('Desktop access follow-through')).toBeTruthy()
|
||||
expect(screen.getByText('Verify the current training lanes')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('surfaces protected notices references and release-target notice posture', async () => {
|
||||
|
|
@ -211,6 +221,121 @@ describe('protected app pages', () => {
|
|||
expect(screen.getByText(byExactTextContent('Public repository/notices URL: https://github.com/hypertwist/hypertwist', 'LI'))).toBeTruthy()
|
||||
expect(screen.getByText(byExactTextContent('Corresponding-source URL: https://hypertwist.app/open-source/source.zip', 'LI'))).toBeTruthy()
|
||||
expect(screen.getByText(byExactTextContent('Windows: configured, validation passed', 'LI'))).toBeTruthy()
|
||||
expect(screen.getByText('Escalation and release follow-through')).toBeTruthy()
|
||||
expect(screen.getByText('Download and install issues')).toBeTruthy()
|
||||
expect(screen.getByRole('link', { name: 'Public notices' }).getAttribute('href')).toBe('/open-source-notices')
|
||||
expect(screen.getByRole('link', { name: 'Open downloads' }).getAttribute('href')).toBe('/app/downloads')
|
||||
})
|
||||
|
||||
it('keeps local signed-in viewer posture when protected pages fall back from live release-manifest authority', async () => {
|
||||
mockGetReleaseManifest.mockRejectedValue(new Error('release manifest unavailable'))
|
||||
|
||||
renderPage(<AccountPage />, '/app/account')
|
||||
|
||||
expect(await screen.findByText('Session profile')).toBeTruthy()
|
||||
expect(await screen.findByText(byExactTextContent('Manifest viewer authenticated: yes', 'LI'))).toBeTruthy()
|
||||
expect(await screen.findByText(byExactTextContent('Manifest viewer plan: operator', 'LI'))).toBeTruthy()
|
||||
expect(await screen.findByText(byExactTextContent('Manifest viewer access: active', 'LI'))).toBeTruthy()
|
||||
expect(screen.getByText(/bounded fallback metadata until auth-server release authority returns/i)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('invalidates the protected browser session when live release-manifest authority comes back anonymous', async () => {
|
||||
mockGetReleaseManifest.mockResolvedValue({
|
||||
ok: true,
|
||||
manifest: {
|
||||
generated_at: '2026-06-23T12:00:00.000Z',
|
||||
support_email: 'hello@hypertwist.app',
|
||||
public_docs_url: 'https://docs.hypertwist.app',
|
||||
release_notes_url: 'https://notes.hypertwist.app',
|
||||
corresponding_source_url: 'https://hypertwist.app/open-source/source.zip',
|
||||
open_source_repo_url: 'https://github.com/hypertwist/hypertwist',
|
||||
viewer: {
|
||||
authenticated: false,
|
||||
canDownload: false,
|
||||
plan: null,
|
||||
role: null,
|
||||
accessStatus: null,
|
||||
},
|
||||
platforms: [
|
||||
{
|
||||
platform_key: 'windows',
|
||||
platform: 'Windows',
|
||||
subtitle: 'Primary shipping lane',
|
||||
details: 'Current packaged validation is strongest on the Windows Unreal lane.',
|
||||
configured: true,
|
||||
channel: 'candidate',
|
||||
version: '1.0.0',
|
||||
build_id: 'win64-1000',
|
||||
published_at: '2026-06-22T00:00:00.000Z',
|
||||
file_name: 'HyperTwist-Windows.zip',
|
||||
file_size_bytes: 1048576,
|
||||
checksum_sha256: 'abc123',
|
||||
download_url: null,
|
||||
download_available: false,
|
||||
validation_summary: {
|
||||
lane: 'Windows Unreal packaged validation',
|
||||
result: 'passed',
|
||||
generated_at: '2026-06-22T01:43:08.7625247Z',
|
||||
configuration: 'Development',
|
||||
skip_build: true,
|
||||
smoke_map_count: 2,
|
||||
smoke_maps: [
|
||||
{
|
||||
map_url: '/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining',
|
||||
label: 'Magic120Cell dedicated-family training map',
|
||||
result: 'passed',
|
||||
},
|
||||
{
|
||||
map_url: '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining',
|
||||
label: 'MagicCube5D dedicated-family training map',
|
||||
result: 'passed',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
renderPage(<AccountPage />, '/app/account')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInvalidateSession).toHaveBeenCalledWith('release_manifest_unauthorized')
|
||||
})
|
||||
})
|
||||
|
||||
it('surfaces live authority sync details after the protected account lane refreshes from manifest truth', async () => {
|
||||
mockUsePlatformAuth.mockReturnValue({
|
||||
user: {
|
||||
id: 'operator-1',
|
||||
name: 'Operator',
|
||||
email: 'operator@hypertwist.app',
|
||||
authMethod: 'email',
|
||||
plan: 'free',
|
||||
role: 'viewer',
|
||||
canDownload: false,
|
||||
billing: {
|
||||
source: 'session',
|
||||
accessStatus: 'session-default',
|
||||
canDownload: false,
|
||||
lastEventType: 'transaction.completed',
|
||||
},
|
||||
},
|
||||
superTokensConfigured: true,
|
||||
invalidateSession: (...args: unknown[]) => mockInvalidateSession(...args),
|
||||
reconcileReleaseAuthority: vi.fn(),
|
||||
releaseAuthoritySyncItems: [
|
||||
'Plan changed from local free to live operator.',
|
||||
'Desktop download access changed from local not entitled to live enabled.',
|
||||
'Access status changed from local session-default to live active.',
|
||||
],
|
||||
})
|
||||
|
||||
renderPage(<AccountPage />, '/app/account')
|
||||
|
||||
expect(await screen.findByText('Live authority sync')).toBeTruthy()
|
||||
expect(screen.getByText(/plan changed from local free to live operator/i)).toBeTruthy()
|
||||
expect(screen.getByText(/desktop download access changed from local not entitled to live enabled/i)).toBeTruthy()
|
||||
expect(screen.getByText(/access status changed from local session-default to live active/i)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -40,21 +40,21 @@ vi.mock('../site-config', async () => {
|
|||
platformKey: 'windows',
|
||||
platform: 'Windows',
|
||||
subtitle: 'Primary shipping lane',
|
||||
href: 'https://downloads.hypertwist.app/windows.exe',
|
||||
configured: true,
|
||||
details: 'Current packaged validation is strongest on the Windows Unreal lane.',
|
||||
},
|
||||
{
|
||||
platformKey: 'macos',
|
||||
platform: 'macOS',
|
||||
subtitle: 'Planned distribution surface',
|
||||
href: '',
|
||||
configured: false,
|
||||
details: 'List a signed desktop build here when the package lane is opened.',
|
||||
},
|
||||
{
|
||||
platformKey: 'linux',
|
||||
platform: 'Linux',
|
||||
subtitle: 'Operator-targeted later lane',
|
||||
href: '',
|
||||
configured: false,
|
||||
details: 'Use for future package publication after the bounded release lane is widened.',
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import { render, screen, within } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
|
@ -37,21 +37,21 @@ vi.mock('../site-config', () => ({
|
|||
platformKey: 'windows',
|
||||
platform: 'Windows',
|
||||
subtitle: 'Primary shipping lane',
|
||||
href: 'https://downloads.hypertwist.app/windows.exe',
|
||||
configured: true,
|
||||
details: 'Current packaged validation is strongest on the Windows Unreal lane.',
|
||||
},
|
||||
{
|
||||
platformKey: 'macos',
|
||||
platform: 'macOS',
|
||||
subtitle: 'Planned distribution surface',
|
||||
href: '',
|
||||
configured: false,
|
||||
details: 'List a signed desktop build here when the package lane is opened.',
|
||||
},
|
||||
{
|
||||
platformKey: 'linux',
|
||||
platform: 'Linux',
|
||||
subtitle: 'Operator-targeted later lane',
|
||||
href: '',
|
||||
configured: false,
|
||||
details: 'Use for future package publication after the bounded release lane is widened.',
|
||||
},
|
||||
],
|
||||
|
|
@ -238,6 +238,8 @@ describe('public marketing pages', () => {
|
|||
expect(screen.getAllByText('Preview posture').length).toBeGreaterThan(0)
|
||||
expect(screen.getByText('Operator checkout: missing')).toBeTruthy()
|
||||
expect(screen.getByText('How the release lane works')).toBeTruthy()
|
||||
expect(screen.getByText('First launch and desktop setup')).toBeTruthy()
|
||||
expect(screen.getByText('Pair desktop access to the browser account')).toBeTruthy()
|
||||
expect(await screen.findByText('Version: 1.0.0')).toBeTruthy()
|
||||
expect(screen.getByText('SHA-256: abc123')).toBeTruthy()
|
||||
expect(screen.getByText('Packaged validation passed')).toBeTruthy()
|
||||
|
|
@ -550,6 +552,8 @@ describe('public marketing pages', () => {
|
|||
expect(screen.getByText('If the desktop app is primary, why keep the web version?')).toBeTruthy()
|
||||
expect(screen.getByText('Is VR/controller support already fully finished?')).toBeTruthy()
|
||||
expect(screen.getByText('Support lanes')).toBeTruthy()
|
||||
expect(screen.getByText('Escalation map')).toBeTruthy()
|
||||
expect(screen.getByText('Runtime and training issues')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('surfaces current packaged desktop proof on the public resources page', () => {
|
||||
|
|
@ -647,9 +651,14 @@ describe('public marketing pages', () => {
|
|||
})
|
||||
renderWithProviders(<ResourcesPage />, ['/resources'])
|
||||
|
||||
expect(screen.getByText('Current packaged desktop proof')).toBeTruthy()
|
||||
expect(screen.getByText('Windows Unreal packaged validation')).toBeTruthy()
|
||||
expect(screen.getAllByText(/MagicCube5D dedicated-family training map: passed/i).length).toBeGreaterThan(0)
|
||||
const packagedProofHeading = screen.getByRole('heading', { name: 'Current packaged desktop proof' })
|
||||
const packagedProofSection = packagedProofHeading.closest('section')
|
||||
|
||||
expect(packagedProofSection).toBeTruthy()
|
||||
expect(within(packagedProofSection as HTMLElement).getByText('Packaged validation passed')).toBeTruthy()
|
||||
expect(within(packagedProofSection as HTMLElement).getByText(/The Windows lane has current first-party package proof/i)).toBeTruthy()
|
||||
expect(within(packagedProofSection as HTMLElement).getByText(/MagicCube5D dedicated-family training map: passed/i)).toBeTruthy()
|
||||
expect(within(packagedProofSection as HTMLElement).getByText(/Build step during latest proof: skipped and reused existing target receipt/i)).toBeTruthy()
|
||||
expect(screen.getByText('Operator playbooks')).toBeTruthy()
|
||||
expect(screen.getByText('Simulator use today')).toBeTruthy()
|
||||
expect(screen.getByText('Higher-dimensional runtime guide')).toBeTruthy()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ describe('buildFallbackReleaseManifest', () => {
|
|||
platformKey: 'windows',
|
||||
platform: 'Windows',
|
||||
subtitle: 'Primary shipping lane',
|
||||
href: '',
|
||||
configured: false,
|
||||
details: 'Current packaged validation is strongest on the Windows Unreal lane.',
|
||||
},
|
||||
],
|
||||
|
|
@ -31,4 +31,70 @@ describe('buildFallbackReleaseManifest', () => {
|
|||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps fallback release metadata while withholding raw download delivery authority', () => {
|
||||
const manifest = buildFallbackReleaseManifest({
|
||||
downloadTargets: [
|
||||
{
|
||||
platformKey: 'windows',
|
||||
platform: 'Windows',
|
||||
subtitle: 'Primary shipping lane',
|
||||
configured: true,
|
||||
details: 'Current packaged validation is strongest on the Windows Unreal lane.',
|
||||
},
|
||||
],
|
||||
publicDocsUrl: 'https://hypertwist.app/resources',
|
||||
releaseNotesUrl: 'https://hypertwist.app/changelog',
|
||||
correspondingSourceUrl: 'https://git.scriptoriumai.io/scriptoriumadmin/hypertwist',
|
||||
openSourceRepoUrl: 'https://git.scriptoriumai.io/scriptoriumadmin/hypertwist',
|
||||
supportEmail: 'hello@hypertwist.app',
|
||||
})
|
||||
|
||||
expect(manifest.platforms[0]).toMatchObject({
|
||||
platform_key: 'windows',
|
||||
configured: true,
|
||||
download_available: false,
|
||||
download_url: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('can keep protected viewer posture in fallback metadata without exposing download authority', () => {
|
||||
const manifest = buildFallbackReleaseManifest({
|
||||
downloadTargets: [
|
||||
{
|
||||
platformKey: 'windows',
|
||||
platform: 'Windows',
|
||||
subtitle: 'Primary shipping lane',
|
||||
configured: true,
|
||||
details: 'Current packaged validation is strongest on the Windows Unreal lane.',
|
||||
},
|
||||
],
|
||||
publicDocsUrl: 'https://hypertwist.app/resources',
|
||||
releaseNotesUrl: 'https://hypertwist.app/changelog',
|
||||
correspondingSourceUrl: 'https://git.scriptoriumai.io/scriptoriumadmin/hypertwist',
|
||||
openSourceRepoUrl: 'https://git.scriptoriumai.io/scriptoriumadmin/hypertwist',
|
||||
supportEmail: 'operator@hypertwist.app',
|
||||
viewer: {
|
||||
authenticated: true,
|
||||
canDownload: true,
|
||||
plan: 'operator',
|
||||
role: 'operator',
|
||||
accessStatus: 'active',
|
||||
},
|
||||
})
|
||||
|
||||
expect(manifest.viewer).toMatchObject({
|
||||
authenticated: true,
|
||||
canDownload: true,
|
||||
plan: 'operator',
|
||||
role: 'operator',
|
||||
accessStatus: 'active',
|
||||
})
|
||||
expect(manifest.platforms[0]).toMatchObject({
|
||||
platform_key: 'windows',
|
||||
configured: true,
|
||||
download_available: false,
|
||||
download_url: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@ function buildTimeoutSignal(timeoutMs: number) {
|
|||
}
|
||||
}
|
||||
|
||||
function createAuthApiError(status: number, code: string) {
|
||||
const error = new Error(code) as Error & { status?: number; code?: string }
|
||||
error.status = status
|
||||
error.code = code
|
||||
return error
|
||||
}
|
||||
|
||||
export interface AuthApiFetchResult {
|
||||
response: Response
|
||||
baseUrl: string
|
||||
|
|
@ -229,7 +236,10 @@ export async function createDesktopLinkToken() {
|
|||
})
|
||||
const payload = await result.response.json() as DesktopLinkPayload | { error?: string }
|
||||
if (!result.response.ok || payload == null || (payload as DesktopLinkPayload).ok !== true) {
|
||||
throw new Error((payload as { error?: string }).error || `desktop_link_http_${result.response.status}`)
|
||||
throw createAuthApiError(
|
||||
result.response.status,
|
||||
(payload as { error?: string }).error || `desktop_link_http_${result.response.status}`,
|
||||
)
|
||||
}
|
||||
return payload as DesktopLinkPayload
|
||||
}
|
||||
|
|
@ -238,7 +248,10 @@ export async function verifyDesktopLinkToken(token: string) {
|
|||
const result = await authApiFetch(`/api/auth/desktop-link/verify?token=${encodeURIComponent(token)}`)
|
||||
const payload = await result.response.json() as DesktopLinkVerifyPayload | { error?: string }
|
||||
if (!result.response.ok || payload == null || (payload as DesktopLinkVerifyPayload).ok !== true) {
|
||||
throw new Error((payload as { error?: string }).error || `desktop_link_verify_http_${result.response.status}`)
|
||||
throw createAuthApiError(
|
||||
result.response.status,
|
||||
(payload as { error?: string }).error || `desktop_link_verify_http_${result.response.status}`,
|
||||
)
|
||||
}
|
||||
return payload as DesktopLinkVerifyPayload
|
||||
}
|
||||
|
|
@ -247,7 +260,7 @@ export async function getAuthHealth() {
|
|||
const result = await authApiFetch('/api/auth/health')
|
||||
const payload = await result.response.json() as AuthHealthPayload
|
||||
if (!result.response.ok) {
|
||||
throw new Error(`auth_health_http_${result.response.status}`)
|
||||
throw createAuthApiError(result.response.status, `auth_health_http_${result.response.status}`)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
|
@ -256,7 +269,10 @@ export async function getReleaseManifest() {
|
|||
const result = await authApiFetch('/api/releases/manifest')
|
||||
const payload = await result.response.json() as ReleaseManifestPayload | { error?: string }
|
||||
if (!result.response.ok || payload == null || (payload as ReleaseManifestPayload).ok !== true) {
|
||||
throw new Error((payload as { error?: string }).error || `release_manifest_http_${result.response.status}`)
|
||||
throw createAuthApiError(
|
||||
result.response.status,
|
||||
(payload as { error?: string }).error || `release_manifest_http_${result.response.status}`,
|
||||
)
|
||||
}
|
||||
return payload as ReleaseManifestPayload
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
import {
|
||||
createContext,
|
||||
type Dispatch,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
type SetStateAction,
|
||||
} from 'react'
|
||||
import { signIn as superTokensSignIn, signUp as superTokensSignUp } from 'supertokens-auth-react/recipe/emailpassword'
|
||||
import { redirectToThirdPartyLogin } from 'supertokens-auth-react/recipe/thirdparty'
|
||||
import { signOut as superTokensSignOut } from 'supertokens-auth-react/recipe/session'
|
||||
import { getCurrentUser, logoutCurrentUser, type ApiBootstrapUserPayload } from './auth-api'
|
||||
import {
|
||||
getCurrentUser,
|
||||
logoutCurrentUser,
|
||||
type ApiBootstrapUserPayload,
|
||||
type ReleaseManifestPayload,
|
||||
} from './auth-api'
|
||||
import { ensureSuperTokensInit, isSuperTokensConfigured } from './supertokens-client'
|
||||
import { validateStrongPassword } from './password-policy'
|
||||
|
||||
|
|
@ -54,22 +61,29 @@ export interface PlatformRegisterInput {
|
|||
name?: string
|
||||
}
|
||||
|
||||
export type ReleaseAuthorityViewer = ReleaseManifestPayload['manifest']['viewer']
|
||||
|
||||
interface PlatformAuthContextValue {
|
||||
user: PlatformUser | null
|
||||
isAuthenticated: boolean
|
||||
isLoading: boolean
|
||||
colorMode: ColorMode
|
||||
superTokensConfigured: boolean
|
||||
releaseAuthoritySyncItems: string[]
|
||||
login: (input: PlatformLoginInput) => Promise<{ ok: boolean; error?: string }>
|
||||
register: (input: PlatformRegisterInput) => Promise<{ ok: boolean; error?: string }>
|
||||
loginWithProvider: (provider: 'github' | 'google' | 'orcid') => Promise<void>
|
||||
logout: () => Promise<void>
|
||||
toggleColorMode: () => void
|
||||
reconcileReleaseAuthority: (viewer: ReleaseAuthorityViewer) => boolean
|
||||
invalidateSession: (reason?: string) => void
|
||||
}
|
||||
|
||||
const AUTH_STORAGE_KEY = 'hypertwist.platform.user.v1'
|
||||
const COLOR_MODE_STORAGE_KEY = 'hypertwist.platform.color-mode.v1'
|
||||
const PlatformAuthContext = createContext<PlatformAuthContextValue | null>(null)
|
||||
type PlatformUserSetter = Dispatch<SetStateAction<PlatformUser | null>>
|
||||
type ReleaseAuthoritySyncSetter = Dispatch<SetStateAction<string[]>>
|
||||
|
||||
function normalizeEmail(value: unknown) {
|
||||
return String(value || '').trim().toLowerCase()
|
||||
|
|
@ -166,6 +180,31 @@ function writeStoredColorMode(mode: ColorMode) {
|
|||
document.documentElement.dataset.theme = mode
|
||||
}
|
||||
|
||||
function persistSessionUser(
|
||||
setUser: PlatformUserSetter,
|
||||
setReleaseAuthoritySyncItems: ReleaseAuthoritySyncSetter,
|
||||
nextUser: PlatformUser | null,
|
||||
) {
|
||||
setUser(nextUser)
|
||||
setReleaseAuthoritySyncItems([])
|
||||
writeStoredUser(nextUser)
|
||||
}
|
||||
|
||||
function restoreStoredSession(
|
||||
setUser: PlatformUserSetter,
|
||||
setReleaseAuthoritySyncItems: ReleaseAuthoritySyncSetter,
|
||||
) {
|
||||
setUser(readStoredUser())
|
||||
setReleaseAuthoritySyncItems([])
|
||||
}
|
||||
|
||||
function clearSessionState(
|
||||
setUser: PlatformUserSetter,
|
||||
setReleaseAuthoritySyncItems: ReleaseAuthoritySyncSetter,
|
||||
) {
|
||||
persistSessionUser(setUser, setReleaseAuthoritySyncItems, null)
|
||||
}
|
||||
|
||||
function normalizeApiUser(input: ApiBootstrapUserPayload['user'] | undefined, fallbackMethod: AuthMethod): PlatformUser | null {
|
||||
if (!input) return null
|
||||
const id = String(input.id || '').trim()
|
||||
|
|
@ -197,6 +236,74 @@ function normalizeApiUser(input: ApiBootstrapUserPayload['user'] | undefined, fa
|
|||
}
|
||||
}
|
||||
|
||||
function computeReleaseAuthoritySyncItems(user: PlatformUser, viewer: ReleaseAuthorityViewer) {
|
||||
const syncItems: string[] = []
|
||||
const localRole = user.role || 'operator'
|
||||
const viewerRole = normalizeRole(viewer.role) || localRole
|
||||
const localAccessStatus = user.billing?.accessStatus || 'session-default'
|
||||
const viewerAccessStatus = String(viewer.accessStatus || '').trim() || localAccessStatus
|
||||
|
||||
if (viewer.plan && user.plan !== viewer.plan) {
|
||||
syncItems.push(`Plan changed from local ${user.plan} to live ${viewer.plan}.`)
|
||||
}
|
||||
|
||||
if (localRole !== viewerRole) {
|
||||
syncItems.push(`Role changed from local ${localRole} to live ${viewerRole}.`)
|
||||
}
|
||||
|
||||
if (Boolean(user.canDownload) !== viewer.canDownload) {
|
||||
syncItems.push(
|
||||
`Desktop download access changed from local ${user.canDownload ? 'enabled' : 'not entitled'} to live ${viewer.canDownload ? 'enabled' : 'not entitled'}.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (localAccessStatus !== viewerAccessStatus) {
|
||||
syncItems.push(`Access status changed from local ${localAccessStatus} to live ${viewerAccessStatus}.`)
|
||||
}
|
||||
|
||||
return syncItems
|
||||
}
|
||||
|
||||
function reconcileUserWithReleaseAuthority(user: PlatformUser | null, viewer: ReleaseAuthorityViewer) {
|
||||
if (!user || !viewer.authenticated) {
|
||||
return { nextUser: user, syncItems: [] as string[] }
|
||||
}
|
||||
|
||||
const syncItems = computeReleaseAuthoritySyncItems(user, viewer)
|
||||
if (syncItems.length === 0) {
|
||||
return { nextUser: user, syncItems }
|
||||
}
|
||||
|
||||
const nextRole = normalizeRole(viewer.role) || user.role
|
||||
const nextPlan = viewer.plan ? normalizePlan(viewer.plan) : user.plan
|
||||
const nextAccessStatus = String(viewer.accessStatus || '').trim() || user.billing?.accessStatus || 'session-default'
|
||||
const existingBilling = user.billing
|
||||
|
||||
return {
|
||||
nextUser: {
|
||||
...user,
|
||||
plan: nextPlan,
|
||||
role: nextRole,
|
||||
isAdmin: user.isAdmin === true || nextRole === 'admin',
|
||||
canDownload: viewer.canDownload,
|
||||
billing: {
|
||||
source: existingBilling?.source || 'session',
|
||||
accessStatus: nextAccessStatus,
|
||||
canDownload: viewer.canDownload,
|
||||
subscriptionId: existingBilling?.subscriptionId || null,
|
||||
customerId: existingBilling?.customerId || null,
|
||||
transactionId: existingBilling?.transactionId || null,
|
||||
lastEventId: existingBilling?.lastEventId || null,
|
||||
lastEventType: existingBilling?.lastEventType || null,
|
||||
lastEventAt: existingBilling?.lastEventAt || null,
|
||||
updatedAt: existingBilling?.updatedAt || null,
|
||||
statePath: existingBilling?.statePath || null,
|
||||
},
|
||||
},
|
||||
syncItems,
|
||||
}
|
||||
}
|
||||
|
||||
function createLocalFallbackUser(email: string, password: string, name?: string): PlatformUser {
|
||||
const safeEmail = normalizeEmail(email)
|
||||
return {
|
||||
|
|
@ -227,16 +334,23 @@ async function parseSignUpError(response: { status?: string; formFields?: Array<
|
|||
return 'Unable to create your account.'
|
||||
}
|
||||
|
||||
export function PlatformAuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<PlatformUser | null>(() => readStoredUser())
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [colorMode, setColorMode] = useState<ColorMode>(() => readStoredColorMode())
|
||||
const superTokensConfigured = isSuperTokensConfigured()
|
||||
|
||||
function usePersistedColorMode(colorMode: ColorMode) {
|
||||
useEffect(() => {
|
||||
writeStoredColorMode(colorMode)
|
||||
}, [colorMode])
|
||||
}
|
||||
|
||||
function useBootstrapPlatformSession({
|
||||
superTokensConfigured,
|
||||
setIsLoading,
|
||||
setUser,
|
||||
setReleaseAuthoritySyncItems,
|
||||
}: {
|
||||
superTokensConfigured: boolean
|
||||
setIsLoading: Dispatch<SetStateAction<boolean>>
|
||||
setUser: PlatformUserSetter
|
||||
setReleaseAuthoritySyncItems: ReleaseAuthoritySyncSetter
|
||||
}) {
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
|
|
@ -250,7 +364,11 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
|
|||
const response = await getCurrentUser()
|
||||
if (!response.ok) {
|
||||
if (!cancelled) {
|
||||
setUser(readStoredUser())
|
||||
if (superTokensConfigured && response.status === 401) {
|
||||
clearSessionState(setUser, setReleaseAuthoritySyncItems)
|
||||
} else {
|
||||
restoreStoredSession(setUser, setReleaseAuthoritySyncItems)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -258,12 +376,11 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
|
|||
const payload = await response.json() as ApiBootstrapUserPayload
|
||||
const nextUser = normalizeApiUser(payload.user, deriveBootstrapAuthMethod(payload, 'supertokens'))
|
||||
if (!cancelled) {
|
||||
setUser(nextUser)
|
||||
writeStoredUser(nextUser)
|
||||
persistSessionUser(setUser, setReleaseAuthoritySyncItems, nextUser)
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setUser(readStoredUser())
|
||||
restoreStoredSession(setUser, setReleaseAuthoritySyncItems)
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
|
|
@ -277,7 +394,23 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
|
|||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [superTokensConfigured])
|
||||
}, [setIsLoading, setReleaseAuthoritySyncItems, setUser, superTokensConfigured])
|
||||
}
|
||||
|
||||
export function PlatformAuthProvider({ children }: { children: ReactNode }) {
|
||||
const [user, setUser] = useState<PlatformUser | null>(() => readStoredUser())
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [colorMode, setColorMode] = useState<ColorMode>(() => readStoredColorMode())
|
||||
const [releaseAuthoritySyncItems, setReleaseAuthoritySyncItems] = useState<string[]>([])
|
||||
const superTokensConfigured = isSuperTokensConfigured()
|
||||
|
||||
usePersistedColorMode(colorMode)
|
||||
useBootstrapPlatformSession({
|
||||
superTokensConfigured,
|
||||
setIsLoading,
|
||||
setUser,
|
||||
setReleaseAuthoritySyncItems,
|
||||
})
|
||||
|
||||
const value = useMemo<PlatformAuthContextValue>(() => ({
|
||||
user,
|
||||
|
|
@ -285,6 +418,7 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
|
|||
isLoading,
|
||||
colorMode,
|
||||
superTokensConfigured,
|
||||
releaseAuthoritySyncItems,
|
||||
async login(input) {
|
||||
if (input.method === 'github' || input.method === 'google' || input.method === 'orcid') {
|
||||
await redirectToThirdPartyLogin({ thirdPartyId: input.method })
|
||||
|
|
@ -299,8 +433,7 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
if (!superTokensConfigured) {
|
||||
const localUser = createLocalFallbackUser(safeEmail, input.password || '')
|
||||
setUser(localUser)
|
||||
writeStoredUser(localUser)
|
||||
persistSessionUser(setUser, setReleaseAuthoritySyncItems, localUser)
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
|
|
@ -323,8 +456,7 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
const payload = await bootstrapResponse.json() as ApiBootstrapUserPayload
|
||||
const nextUser = normalizeApiUser(payload.user, deriveBootstrapAuthMethod(payload, 'supertokens'))
|
||||
setUser(nextUser)
|
||||
writeStoredUser(nextUser)
|
||||
persistSessionUser(setUser, setReleaseAuthoritySyncItems, nextUser)
|
||||
} catch {
|
||||
return { ok: false, error: 'Signed in, but the account API is unavailable.' }
|
||||
}
|
||||
|
|
@ -343,8 +475,7 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
if (!superTokensConfigured) {
|
||||
const localUser = createLocalFallbackUser(safeEmail, input.password, input.name)
|
||||
setUser(localUser)
|
||||
writeStoredUser(localUser)
|
||||
persistSessionUser(setUser, setReleaseAuthoritySyncItems, localUser)
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
|
|
@ -367,8 +498,7 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
const payload = await bootstrapResponse.json() as ApiBootstrapUserPayload
|
||||
const nextUser = normalizeApiUser(payload.user, deriveBootstrapAuthMethod(payload, 'supertokens'))
|
||||
setUser(nextUser)
|
||||
writeStoredUser(nextUser)
|
||||
persistSessionUser(setUser, setReleaseAuthoritySyncItems, nextUser)
|
||||
} catch {
|
||||
return { ok: false, error: 'Account created, but the account API is unavailable.' }
|
||||
}
|
||||
|
|
@ -383,8 +513,7 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
|
|||
await redirectToThirdPartyLogin({ thirdPartyId: provider })
|
||||
},
|
||||
async logout() {
|
||||
writeStoredUser(null)
|
||||
setUser(null)
|
||||
clearSessionState(setUser, setReleaseAuthoritySyncItems)
|
||||
if (!superTokensConfigured) {
|
||||
return
|
||||
}
|
||||
|
|
@ -402,7 +531,21 @@ export function PlatformAuthProvider({ children }: { children: ReactNode }) {
|
|||
toggleColorMode() {
|
||||
setColorMode((current) => current === 'dark' ? 'light' : 'dark')
|
||||
},
|
||||
}), [colorMode, isLoading, superTokensConfigured, user])
|
||||
reconcileReleaseAuthority(viewer) {
|
||||
const reconciled = reconcileUserWithReleaseAuthority(user, viewer)
|
||||
if (reconciled.syncItems.length > 0 && reconciled.nextUser) {
|
||||
setUser(reconciled.nextUser)
|
||||
setReleaseAuthoritySyncItems(reconciled.syncItems)
|
||||
writeStoredUser(reconciled.nextUser)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
invalidateSession() {
|
||||
clearSessionState(setUser, setReleaseAuthoritySyncItems)
|
||||
},
|
||||
}), [colorMode, isLoading, releaseAuthoritySyncItems, superTokensConfigured, user])
|
||||
|
||||
return <PlatformAuthContext.Provider value={value}>{children}</PlatformAuthContext.Provider>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ export function PublicLaunchStatus({
|
|||
) : null}
|
||||
{releaseManifestQuery.isError ? (
|
||||
<p className="form-error">
|
||||
Live release-manifest lookup failed. This launch checklist is currently using frontend configuration fallback for the download lane.
|
||||
Live release-manifest lookup failed. This launch checklist is currently using a bounded frontend fallback and intentionally withholds direct download-lane authority until the auth server returns.
|
||||
</p>
|
||||
) : null}
|
||||
{!ready ? (
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { useMemo } from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { Link, useSearchParams } from 'react-router-dom'
|
||||
import { buildAuthApiBaseUrls, createDesktopLinkToken, getAuthHealth, getReleaseManifest } from '../auth/auth-api'
|
||||
import { usePlatformAuth } from '../auth/platform-auth'
|
||||
import { usePlatformAuth, type PlatformUser } from '../auth/platform-auth'
|
||||
import { SiteMetadata } from '../components/seo/SiteMetadata'
|
||||
import { ReleaseValidationSummary } from '../components/ui/ReleaseValidationSummary'
|
||||
import { resolvePublicLaunchReadiness } from '../public-launch'
|
||||
import { downloadTargets, launchReadiness, mplSourceUrl, openSourceRepoUrl, planCatalog, publicDocsUrl, releaseNotesUrl } from '../site-config'
|
||||
import { buildReleaseMetadataItems, resolveReleaseCommerceView, resolveReleaseManifestView, type ReleaseManifestView } from '../release-manifest'
|
||||
import { buildSupportPath, getDownloadPlatformLabel, normalizeDownloadPlatform } from '../site-routes'
|
||||
import { desktopDownloadSteps, roadmapHonestyCards } from '../site-data'
|
||||
import { desktopDownloadSteps, desktopFirstLaunchCards, roadmapHonestyCards, supportEscalationCards } from '../site-data'
|
||||
|
||||
function Panel({
|
||||
title,
|
||||
|
|
@ -38,7 +38,7 @@ const releaseCommerceFallback = {
|
|||
planPriceStudio: studioFallbackPlan.price,
|
||||
}
|
||||
|
||||
function buildProtectedReleaseManifestFallback(supportEmail: string) {
|
||||
function buildProtectedReleaseManifestFallback(user: PlatformUser | null | undefined, supportEmail: string) {
|
||||
return {
|
||||
downloadTargets,
|
||||
publicDocsUrl,
|
||||
|
|
@ -46,6 +46,13 @@ function buildProtectedReleaseManifestFallback(supportEmail: string) {
|
|||
correspondingSourceUrl: mplSourceUrl,
|
||||
openSourceRepoUrl,
|
||||
supportEmail,
|
||||
viewer: user ? {
|
||||
authenticated: true,
|
||||
canDownload: Boolean(user.canDownload),
|
||||
plan: user.plan,
|
||||
role: user.role || null,
|
||||
accessStatus: user.billing?.accessStatus || null,
|
||||
} : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +72,77 @@ function useProtectedReleaseManifestQuery(scope: string, userId: string | undefi
|
|||
})
|
||||
}
|
||||
|
||||
function useProtectedReleaseSurface(scope: string) {
|
||||
const platformAuth = usePlatformAuth()
|
||||
const { user, reconcileReleaseAuthority, invalidateSession, superTokensConfigured } = platformAuth
|
||||
const canDownload = user?.canDownload === true
|
||||
const releaseManifestQuery = useProtectedReleaseManifestQuery(scope, user?.id, canDownload)
|
||||
const releaseManifest = useMemo(
|
||||
() => resolveReleaseManifestView(
|
||||
releaseManifestQuery.data?.manifest,
|
||||
buildProtectedReleaseManifestFallback(user, user?.email || 'hello@hypertwist.app'),
|
||||
),
|
||||
[releaseManifestQuery.data?.manifest, user],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const liveViewer = releaseManifestQuery.data?.manifest?.viewer
|
||||
if (liveViewer?.authenticated && typeof reconcileReleaseAuthority === 'function') {
|
||||
reconcileReleaseAuthority(liveViewer)
|
||||
}
|
||||
}, [reconcileReleaseAuthority, releaseManifestQuery.data?.manifest?.viewer])
|
||||
|
||||
useEffect(() => {
|
||||
const liveViewer = releaseManifestQuery.data?.manifest?.viewer
|
||||
if (
|
||||
user
|
||||
&& superTokensConfigured
|
||||
&& liveViewer
|
||||
&& liveViewer.authenticated === false
|
||||
&& typeof invalidateSession === 'function'
|
||||
) {
|
||||
invalidateSession('release_manifest_unauthorized')
|
||||
}
|
||||
}, [invalidateSession, releaseManifestQuery.data?.manifest?.viewer, superTokensConfigured, user])
|
||||
|
||||
return {
|
||||
...platformAuth,
|
||||
canDownload,
|
||||
releaseManifest,
|
||||
releaseManifestQuery,
|
||||
releaseAuthoritySyncItems: platformAuth.releaseAuthoritySyncItems ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
function getProtectedDownloadActionLabel({
|
||||
configured,
|
||||
viewerCanDownload,
|
||||
releaseAuthorityUnavailable,
|
||||
}: {
|
||||
configured: boolean
|
||||
viewerCanDownload: boolean
|
||||
releaseAuthorityUnavailable: boolean
|
||||
}) {
|
||||
if (!configured) {
|
||||
return 'Release URL not configured yet'
|
||||
}
|
||||
|
||||
if (viewerCanDownload) {
|
||||
return releaseAuthorityUnavailable
|
||||
? 'Live release authority temporarily unavailable'
|
||||
: 'Download temporarily unavailable'
|
||||
}
|
||||
|
||||
return 'Account not entitled yet'
|
||||
}
|
||||
|
||||
function isUnauthorizedAuthApiError(error: unknown) {
|
||||
return typeof error === 'object'
|
||||
&& error !== null
|
||||
&& 'status' in error
|
||||
&& Number((error as { status?: number }).status) === 401
|
||||
}
|
||||
|
||||
function ReleaseAuthorityLinks({
|
||||
releaseManifest,
|
||||
includePublicNoticesLink = false,
|
||||
|
|
@ -123,22 +201,88 @@ function ReleaseAuthorityLinks({
|
|||
)
|
||||
}
|
||||
|
||||
function DesktopFirstLaunchChecklist({ title, kicker }: { title: string; kicker: string }) {
|
||||
return (
|
||||
<Panel title={title} kicker={kicker}>
|
||||
<div className="card-grid">
|
||||
{desktopFirstLaunchCards.map((card) => (
|
||||
<article key={card.title} className="card card--compact">
|
||||
<h3>{card.title}</h3>
|
||||
<p>{card.description}</p>
|
||||
<ul className="list top-gap">
|
||||
{card.bullets.map((bullet) => (
|
||||
<li key={bullet}>{bullet}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
function SupportEscalationChecklist({
|
||||
title,
|
||||
kicker,
|
||||
description,
|
||||
secondaryLink,
|
||||
}: {
|
||||
title: string
|
||||
kicker: string
|
||||
description: string
|
||||
secondaryLink: {
|
||||
to: string
|
||||
label: string
|
||||
}
|
||||
}) {
|
||||
return (
|
||||
<Panel title={title} kicker={kicker}>
|
||||
<p>{description}</p>
|
||||
<div className="card-grid top-gap">
|
||||
{supportEscalationCards.map((card) => (
|
||||
<article key={card.title} className="card card--compact">
|
||||
<h3>{card.title}</h3>
|
||||
<p>{card.description}</p>
|
||||
<ul className="list top-gap">
|
||||
{card.bullets.map((bullet) => (
|
||||
<li key={bullet}>{bullet}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<div className="button-row top-gap">
|
||||
<Link className="button button--ghost" to={buildSupportPath('operator-access')}>
|
||||
Open support
|
||||
</Link>
|
||||
<Link className="button button--ghost" to={secondaryLink.to}>
|
||||
{secondaryLink.label}
|
||||
</Link>
|
||||
</div>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
export function DashboardOverviewPage() {
|
||||
const { user, superTokensConfigured } = usePlatformAuth()
|
||||
const {
|
||||
user,
|
||||
superTokensConfigured,
|
||||
invalidateSession,
|
||||
releaseAuthoritySyncItems,
|
||||
releaseManifest,
|
||||
releaseManifestQuery,
|
||||
} = useProtectedReleaseSurface('dashboard')
|
||||
const healthQuery = useProtectedAuthHealthQuery('dashboard')
|
||||
const releaseManifestQuery = useProtectedReleaseManifestQuery('dashboard', user?.id, user?.canDownload === true)
|
||||
|
||||
const desktopLinkMutation = useMutation({
|
||||
mutationFn: createDesktopLinkToken,
|
||||
onError(error) {
|
||||
if (isUnauthorizedAuthApiError(error) && typeof invalidateSession === 'function') {
|
||||
invalidateSession('desktop_link_unauthorized')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const releaseManifest = useMemo(
|
||||
() => resolveReleaseManifestView(
|
||||
releaseManifestQuery.data?.manifest,
|
||||
buildProtectedReleaseManifestFallback(user?.email || 'hello@hypertwist.app'),
|
||||
),
|
||||
[releaseManifestQuery.data?.manifest, user?.email],
|
||||
)
|
||||
const releaseCommerce = useMemo(
|
||||
() => resolveReleaseCommerceView(releaseManifestQuery.data?.manifest, releaseCommerceFallback),
|
||||
[releaseManifestQuery.data?.manifest],
|
||||
|
|
@ -205,6 +349,19 @@ export function DashboardOverviewPage() {
|
|||
This session is currently using local fallback posture, not fully shared production auth.
|
||||
</p>
|
||||
) : null}
|
||||
{releaseManifestQuery.data?.manifest && releaseAuthoritySyncItems.length > 0 ? (
|
||||
<div className="callout top-gap">
|
||||
<p className="status-pill status-pill--info">Live authority sync</p>
|
||||
<p>
|
||||
The protected browser session was refreshed from live release authority so the local dashboard view catches up to current account-access truth.
|
||||
</p>
|
||||
<ul className="list top-gap">
|
||||
{releaseAuthoritySyncItems.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</Panel>
|
||||
|
||||
<Panel title="Auth and server health" kicker="Browser shell">
|
||||
|
|
@ -331,6 +488,11 @@ export function DashboardOverviewPage() {
|
|||
)}
|
||||
</Panel>
|
||||
|
||||
<DesktopFirstLaunchChecklist
|
||||
title="Desktop rollout follow-through"
|
||||
kicker="Post-download operator manual"
|
||||
/>
|
||||
|
||||
<Panel title="Product boundary" kicker="Roadmap honesty">
|
||||
<ul className="list">
|
||||
{roadmapHonestyCards.map((item) => (
|
||||
|
|
@ -344,18 +506,9 @@ export function DashboardOverviewPage() {
|
|||
}
|
||||
|
||||
export function DownloadCenterPage() {
|
||||
const { user } = usePlatformAuth()
|
||||
const { user, canDownload, releaseManifest, releaseManifestQuery } = useProtectedReleaseSurface('protected')
|
||||
const [searchParams] = useSearchParams()
|
||||
const canDownload = user?.canDownload === true
|
||||
const requestedPlatform = normalizeDownloadPlatform(searchParams.get('platform'))
|
||||
const releaseManifestQuery = useProtectedReleaseManifestQuery('protected', user?.id, canDownload)
|
||||
const releaseManifest = useMemo(
|
||||
() => resolveReleaseManifestView(
|
||||
releaseManifestQuery.data?.manifest,
|
||||
buildProtectedReleaseManifestFallback(user?.email || 'hello@hypertwist.app'),
|
||||
),
|
||||
[canDownload, releaseManifestQuery.data?.manifest, user?.email],
|
||||
)
|
||||
const orderedTargets = useMemo(() => {
|
||||
if (!requestedPlatform) {
|
||||
return releaseManifest.platforms
|
||||
|
|
@ -372,6 +525,8 @@ export function DownloadCenterPage() {
|
|||
]
|
||||
}, [releaseManifest.platforms, requestedPlatform])
|
||||
const requestedPlatformLabel = requestedPlatform ? getDownloadPlatformLabel(requestedPlatform) : null
|
||||
const viewerCanDownload = releaseManifest.viewer.canDownload || canDownload
|
||||
const releaseAuthorityUnavailable = releaseManifestQuery.isError
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -392,18 +547,27 @@ export function DownloadCenterPage() {
|
|||
<Panel title="Release manifest status" kicker="Runtime authority">
|
||||
<p className="form-error">
|
||||
The live release manifest could not be loaded from the auth server right now.
|
||||
This panel is showing bounded fallback site metadata instead of current runtime release authority.
|
||||
This panel is showing bounded fallback site metadata instead of current runtime release authority, and direct package delivery remains intentionally withheld until the auth server returns.
|
||||
</p>
|
||||
</Panel>
|
||||
) : null}
|
||||
|
||||
<Panel title="Release targets" kicker="Desktop distribution">
|
||||
{!canDownload ? (
|
||||
{!viewerCanDownload ? (
|
||||
<p className="form-error">
|
||||
Your current account does not yet have desktop download entitlement.
|
||||
Complete the matching checkout or operator provisioning step, then refresh this dashboard.
|
||||
</p>
|
||||
) : null}
|
||||
{releaseAuthorityUnavailable && viewerCanDownload ? (
|
||||
<div className="callout top-gap">
|
||||
<p className="status-pill status-pill--info">Fallback release posture</p>
|
||||
<p>
|
||||
Your signed-in account still resolves to desktop access, but live release authority is temporarily unavailable.
|
||||
Direct package delivery remains intentionally withheld until the auth server returns.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
{requestedPlatformLabel ? (
|
||||
<div className="callout top-gap">
|
||||
<p className="status-pill status-pill--info">Requested target</p>
|
||||
|
|
@ -438,13 +602,17 @@ export function DownloadCenterPage() {
|
|||
</ul>
|
||||
) : null}
|
||||
<ReleaseValidationSummary platform={target} />
|
||||
{target.download_url && canDownload ? (
|
||||
{target.download_url && viewerCanDownload ? (
|
||||
<a className="button button--primary button--full" href={target.download_url}>
|
||||
Download {target.platform}
|
||||
</a>
|
||||
) : (
|
||||
<div className="button button--ghost button--full is-disabled" aria-disabled="true">
|
||||
{target.configured ? 'Account not entitled yet' : 'Release URL not configured yet'}
|
||||
{getProtectedDownloadActionLabel({
|
||||
configured: target.configured,
|
||||
viewerCanDownload,
|
||||
releaseAuthorityUnavailable,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
|
|
@ -482,22 +650,19 @@ export function DownloadCenterPage() {
|
|||
</div>
|
||||
) : null}
|
||||
</Panel>
|
||||
|
||||
<DesktopFirstLaunchChecklist
|
||||
title="First launch follow-through"
|
||||
kicker="Desktop setup"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function BrowserAccessPage() {
|
||||
const { user } = usePlatformAuth()
|
||||
const { user, releaseManifest, releaseManifestQuery } = useProtectedReleaseSurface('browser-access')
|
||||
const healthQuery = useProtectedAuthHealthQuery('browser-access')
|
||||
const releaseManifestQuery = useProtectedReleaseManifestQuery('browser-access', user?.id, user?.canDownload === true)
|
||||
const releaseManifest = useMemo(
|
||||
() => resolveReleaseManifestView(
|
||||
releaseManifestQuery.data?.manifest,
|
||||
buildProtectedReleaseManifestFallback(user?.email || 'hello@hypertwist.app'),
|
||||
),
|
||||
[releaseManifestQuery.data?.manifest, user?.email],
|
||||
)
|
||||
const configuredPlatformCount = useMemo(
|
||||
() => releaseManifest.platforms.filter((platform) => platform.configured).length,
|
||||
[releaseManifest.platforms],
|
||||
|
|
@ -565,6 +730,12 @@ export function BrowserAccessPage() {
|
|||
</ul>
|
||||
<ReleaseAuthorityLinks releaseManifest={releaseManifest} includeProtectedNoticesLink />
|
||||
</Panel>
|
||||
<SupportEscalationChecklist
|
||||
title="Operator escalation map"
|
||||
kicker="Keep browser, package, runtime, and rollout issues separated"
|
||||
description="Use the same bounded escalation map as the public support lane so protected browser access does not collapse access problems, package problems, runtime regressions, and rollout/compliance questions into one vague bucket."
|
||||
secondaryLink={{ to: '/app/notices', label: 'Review notices' }}
|
||||
/>
|
||||
<Panel title="Optional branches remain bounded" kicker="Not widened here">
|
||||
<ul className="list">
|
||||
<li>The optional full-browser client remains spec-only.</li>
|
||||
|
|
@ -583,15 +754,13 @@ export function BrowserAccessPage() {
|
|||
}
|
||||
|
||||
export function AccountPage() {
|
||||
const { user, superTokensConfigured } = usePlatformAuth()
|
||||
const releaseManifestQuery = useProtectedReleaseManifestQuery('account', user?.id, user?.canDownload === true)
|
||||
const releaseManifest = useMemo(
|
||||
() => resolveReleaseManifestView(
|
||||
releaseManifestQuery.data?.manifest,
|
||||
buildProtectedReleaseManifestFallback(user?.email || 'hello@hypertwist.app'),
|
||||
),
|
||||
[releaseManifestQuery.data?.manifest, user?.email],
|
||||
)
|
||||
const {
|
||||
user,
|
||||
superTokensConfigured,
|
||||
releaseAuthoritySyncItems,
|
||||
releaseManifest,
|
||||
releaseManifestQuery,
|
||||
} = useProtectedReleaseSurface('account')
|
||||
const configuredPlatforms = useMemo(
|
||||
() => releaseManifest.platforms.filter((platform) => platform.configured),
|
||||
[releaseManifest.platforms],
|
||||
|
|
@ -617,6 +786,19 @@ export function AccountPage() {
|
|||
<li>Billing source: {user?.billing?.source || 'session'}</li>
|
||||
<li>Auth stack: {superTokensConfigured ? 'SuperTokens-backed' : 'Local fallback mode'}</li>
|
||||
</ul>
|
||||
{releaseManifestQuery.data?.manifest && releaseAuthoritySyncItems.length > 0 ? (
|
||||
<div className="callout top-gap">
|
||||
<p className="status-pill status-pill--info">Live authority sync</p>
|
||||
<p>
|
||||
The protected browser session was refreshed from live release authority so the local account snapshot matches current release-access truth.
|
||||
</p>
|
||||
<ul className="list top-gap">
|
||||
{releaseAuthoritySyncItems.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</Panel>
|
||||
<Panel title="Entitlement and release access" kicker="Protected release lane">
|
||||
{releaseManifestQuery.isLoading ? <p>Refreshing release-access posture from the live manifest...</p> : null}
|
||||
|
|
@ -665,21 +847,18 @@ export function AccountPage() {
|
|||
</div>
|
||||
<ReleaseAuthorityLinks releaseManifest={releaseManifest} includeProtectedNoticesLink />
|
||||
</Panel>
|
||||
|
||||
<DesktopFirstLaunchChecklist
|
||||
title="Desktop access follow-through"
|
||||
kicker="After sign-in"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function NoticesPage() {
|
||||
const { user } = usePlatformAuth()
|
||||
const releaseManifestQuery = useProtectedReleaseManifestQuery('notices', user?.id, user?.canDownload === true)
|
||||
const releaseManifest = useMemo(
|
||||
() => resolveReleaseManifestView(
|
||||
releaseManifestQuery.data?.manifest,
|
||||
buildProtectedReleaseManifestFallback(user?.email || 'hello@hypertwist.app'),
|
||||
),
|
||||
[releaseManifestQuery.data?.manifest, user?.email],
|
||||
)
|
||||
const { releaseManifest, releaseManifestQuery } = useProtectedReleaseSurface('notices')
|
||||
const configuredPlatforms = useMemo(
|
||||
() => releaseManifest.platforms.filter((platform) => platform.configured),
|
||||
[releaseManifest.platforms],
|
||||
|
|
@ -737,6 +916,12 @@ export function NoticesPage() {
|
|||
includePublicNoticesLink
|
||||
/>
|
||||
</Panel>
|
||||
<SupportEscalationChecklist
|
||||
title="Escalation and release follow-through"
|
||||
kicker="Protected operator manual"
|
||||
description="When distribution or compliance work is underway, support should still separate entitlement, package, runtime, and rollout problems while keeping the protected notices lane tied to the same release story as download, support, and corresponding source."
|
||||
secondaryLink={{ to: '/app/downloads', label: 'Open downloads' }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { buildProtectedDownloadPath, buildSupportPath } from '../site-routes'
|
||||
import {
|
||||
deliverySurfaceCards,
|
||||
desktopFirstLaunchCards,
|
||||
desktopDownloadSteps,
|
||||
desktopReleaseSignals,
|
||||
digitalDeliveryCards,
|
||||
|
|
@ -157,7 +158,7 @@ export function DownloadPage() {
|
|||
<article className="callout">
|
||||
<p>
|
||||
The live release manifest could not be loaded from the auth server right now.
|
||||
This page is showing bounded fallback site metadata instead of current runtime release authority.
|
||||
This page is showing bounded fallback site metadata instead of current runtime release authority, and direct package delivery stays intentionally withheld until that authority returns.
|
||||
</p>
|
||||
</article>
|
||||
</Section>
|
||||
|
|
@ -208,6 +209,25 @@ export function DownloadPage() {
|
|||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="First launch and desktop setup"
|
||||
description="This keeps the download page useful after the archive is in hand: what to verify, how to pair the app, and which current runtime lanes matter first."
|
||||
>
|
||||
<div className="card-grid">
|
||||
{desktopFirstLaunchCards.map((card) => (
|
||||
<article key={card.title} className="card">
|
||||
<h3>{card.title}</h3>
|
||||
<p>{card.description}</p>
|
||||
<ul className="list top-gap">
|
||||
{card.bullets.map((bullet) => (
|
||||
<li key={bullet}>{bullet}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title="Release posture">
|
||||
<PublicLaunchStatus title="Desktop release access stays launch-honest" />
|
||||
</Section>
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import { getReleaseManifest } from '../auth/auth-api'
|
|||
import { MarketingShell } from '../components/layout/MarketingShell'
|
||||
import { SiteMetadata } from '../components/seo/SiteMetadata'
|
||||
import { PublicLaunchStatus } from '../components/ui/PublicLaunchStatus'
|
||||
import { ReleaseValidationSummary } from '../components/ui/ReleaseValidationSummary'
|
||||
import { brandConfig } from '../site-config'
|
||||
import { formatReleasePublishedAt, resolveReleaseManifestView } from '../release-manifest'
|
||||
import { getReleasePlatformValidationSummary } from '../shared/package-validation'
|
||||
import { resolveReleaseManifestView } from '../release-manifest'
|
||||
import {
|
||||
capabilityPillars,
|
||||
changelogEntries,
|
||||
|
|
@ -24,7 +24,9 @@ import {
|
|||
releaseStoryCards,
|
||||
resourceCollections,
|
||||
roadmapHonestyCards,
|
||||
runtimeControlGuideCards,
|
||||
shippingNowCards,
|
||||
supportEscalationCards,
|
||||
simulatorManualCards,
|
||||
supportFaqs,
|
||||
} from '../site-data'
|
||||
|
|
@ -250,7 +252,19 @@ export function AboutPage() {
|
|||
export function ResourcesPage() {
|
||||
const [query, setQuery] = useState('')
|
||||
const deferredQuery = useDeferredValue(query)
|
||||
const windowsValidationSummary = getReleasePlatformValidationSummary('windows')
|
||||
const releaseManifestQuery = useQuery({
|
||||
queryKey: ['release-manifest', 'public-resources'],
|
||||
queryFn: getReleaseManifest,
|
||||
retry: false,
|
||||
})
|
||||
const releaseManifest = useMemo(
|
||||
() => resolveReleaseManifestView(releaseManifestQuery.data?.manifest, buildPublicReleaseManifestFallback()),
|
||||
[releaseManifestQuery.data?.manifest],
|
||||
)
|
||||
const windowsValidationPlatform = useMemo(
|
||||
() => releaseManifest.platforms.find((platform) => platform.platform_key === 'windows' && platform.validation_summary) ?? null,
|
||||
[releaseManifest],
|
||||
)
|
||||
|
||||
const filteredCollections = useMemo(() => {
|
||||
const normalized = deferredQuery.trim().toLowerCase()
|
||||
|
|
@ -393,6 +407,25 @@ export function ResourcesPage() {
|
|||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="Runtime control guide"
|
||||
description="This public-safe guide focuses on how operators and trainees actually drive the current desktop runtime today."
|
||||
>
|
||||
<div className="card-grid">
|
||||
{runtimeControlGuideCards.map((card) => (
|
||||
<article key={card.title} className="card">
|
||||
<h3>{card.title}</h3>
|
||||
<p>{card.description}</p>
|
||||
<ul className="list top-gap">
|
||||
{card.bullets.map((bullet) => (
|
||||
<li key={bullet}>{bullet}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="Deployment readiness snapshot"
|
||||
description="The public site can be detailed without becoming misleading when rollout guidance stays separated into identity, package, and legal lanes."
|
||||
|
|
@ -411,32 +444,17 @@ export function ResourcesPage() {
|
|||
</div>
|
||||
</Section>
|
||||
|
||||
{windowsValidationSummary ? (
|
||||
{windowsValidationPlatform ? (
|
||||
<Section title="Current packaged desktop proof">
|
||||
<article className="card">
|
||||
<h3>{windowsValidationSummary.lane}</h3>
|
||||
<p>
|
||||
Latest public-safe package evidence was generated{' '}
|
||||
{formatReleasePublishedAt(windowsValidationSummary.generated_at) || windowsValidationSummary.generated_at}
|
||||
{' '}in {windowsValidationSummary.configuration} mode and passed across{' '}
|
||||
{windowsValidationSummary.smoke_map_count} dedicated-family higher-dimensional training map{windowsValidationSummary.smoke_map_count === 1 ? '' : 's'}.
|
||||
</p>
|
||||
<ul className="list top-gap">
|
||||
{windowsValidationSummary.smoke_maps.map((map) => (
|
||||
<li key={map.map_url}>
|
||||
{map.label}: {map.result}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="button-row top-gap">
|
||||
<Link className="button button--ghost" to="/download">
|
||||
Open download center
|
||||
</Link>
|
||||
<Link className="button button--ghost" to="/app">
|
||||
Open operator dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
<ReleaseValidationSummary platform={windowsValidationPlatform} />
|
||||
<div className="button-row top-gap">
|
||||
<Link className="button button--ghost" to="/download">
|
||||
Open download center
|
||||
</Link>
|
||||
<Link className="button button--ghost" to="/app">
|
||||
Open operator dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</Section>
|
||||
) : null}
|
||||
</MarketingShell>
|
||||
|
|
@ -586,6 +604,25 @@ export function DocsPage() {
|
|||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="Runtime control guide"
|
||||
description="This manual section explains how to approach the current desktop runtime without pretending a later XR/preferences packet has already landed."
|
||||
>
|
||||
<div className="card-grid">
|
||||
{runtimeControlGuideCards.map((card) => (
|
||||
<article key={card.title} className="card">
|
||||
<h3>{card.title}</h3>
|
||||
<p>{card.description}</p>
|
||||
<ul className="list top-gap">
|
||||
{card.bullets.map((bullet) => (
|
||||
<li key={bullet}>{bullet}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{releaseManifest.public_docs_url ? (
|
||||
<Section title="External docs portal">
|
||||
<a className="button button--primary" href={releaseManifest.public_docs_url} target="_blank" rel="noreferrer">
|
||||
|
|
@ -661,6 +698,25 @@ export function SupportPage() {
|
|||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="Escalation map"
|
||||
description="This keeps support conversations concrete by separating account, package, runtime, and rollout/compliance problems instead of flattening them together."
|
||||
>
|
||||
<div className="card-grid">
|
||||
{supportEscalationCards.map((card) => (
|
||||
<article key={card.title} className="card">
|
||||
<h3>{card.title}</h3>
|
||||
<p>{card.description}</p>
|
||||
<ul className="list top-gap">
|
||||
{card.bullets.map((bullet) => (
|
||||
<li key={bullet}>{bullet}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
</MarketingShell>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,10 +6,18 @@ type FallbackDownloadTarget = {
|
|||
platformKey: DownloadPlatformKey
|
||||
platform: string
|
||||
subtitle: string
|
||||
href: string
|
||||
configured: boolean
|
||||
details: string
|
||||
}
|
||||
|
||||
type FallbackViewer = {
|
||||
authenticated: boolean
|
||||
canDownload: boolean
|
||||
plan: string | null
|
||||
role: string | null
|
||||
accessStatus: string | null
|
||||
}
|
||||
|
||||
export type ReleaseManifestView = ReleaseManifestPayload['manifest']
|
||||
export type ReleaseManifestPlatformView = ReleaseManifestView['platforms'][number]
|
||||
export type ReleaseManifestCommerceView = NonNullable<ReleaseManifestView['commerce']>
|
||||
|
|
@ -21,6 +29,7 @@ export function buildFallbackReleaseManifest({
|
|||
correspondingSourceUrl,
|
||||
openSourceRepoUrl,
|
||||
supportEmail,
|
||||
viewer,
|
||||
}: {
|
||||
downloadTargets: readonly FallbackDownloadTarget[]
|
||||
publicDocsUrl: string
|
||||
|
|
@ -28,6 +37,7 @@ export function buildFallbackReleaseManifest({
|
|||
correspondingSourceUrl: string
|
||||
openSourceRepoUrl: string
|
||||
supportEmail: string
|
||||
viewer?: FallbackViewer
|
||||
}): ReleaseManifestView {
|
||||
return {
|
||||
generated_at: '',
|
||||
|
|
@ -36,7 +46,7 @@ export function buildFallbackReleaseManifest({
|
|||
release_notes_url: releaseNotesUrl || null,
|
||||
corresponding_source_url: correspondingSourceUrl || null,
|
||||
open_source_repo_url: openSourceRepoUrl || null,
|
||||
viewer: {
|
||||
viewer: viewer ?? {
|
||||
authenticated: false,
|
||||
canDownload: false,
|
||||
plan: null,
|
||||
|
|
@ -48,7 +58,7 @@ export function buildFallbackReleaseManifest({
|
|||
platform: target.platform,
|
||||
subtitle: target.subtitle,
|
||||
details: target.details,
|
||||
configured: Boolean(target.href),
|
||||
configured: target.configured,
|
||||
channel: 'preview',
|
||||
version: null,
|
||||
build_id: null,
|
||||
|
|
@ -56,8 +66,8 @@ export function buildFallbackReleaseManifest({
|
|||
file_name: null,
|
||||
file_size_bytes: null,
|
||||
checksum_sha256: null,
|
||||
download_url: target.href || null,
|
||||
download_available: Boolean(target.href),
|
||||
download_url: null,
|
||||
download_available: false,
|
||||
validation_summary: getReleasePlatformValidationSummary(target.platformKey),
|
||||
})),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"lane": "Windows Unreal packaged validation",
|
||||
"result": "passed",
|
||||
"generated_at": "2026-06-23T10:27:31.6439999Z",
|
||||
"configuration": "Development",
|
||||
"skip_build": true,
|
||||
"smoke_map_count": 2,
|
||||
"smoke_maps": [
|
||||
{
|
||||
"map_url": "/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining",
|
||||
"label": "Magic120Cell dedicated-family training map",
|
||||
"result": "passed"
|
||||
},
|
||||
{
|
||||
"map_url": "/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining",
|
||||
"label": "MagicCube5D dedicated-family training map",
|
||||
"result": "passed"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import type { DownloadPlatformKey } from '../site-routes'
|
||||
import generatedWindowsValidationSummary from './generated/windows-package-validation-summary.json'
|
||||
|
||||
export interface ReleasePlatformValidationSmokeMap {
|
||||
map_url: string
|
||||
|
|
@ -16,10 +17,10 @@ export interface ReleasePlatformValidationSummary {
|
|||
smoke_maps: ReleasePlatformValidationSmokeMap[]
|
||||
}
|
||||
|
||||
const WINDOWS_VALIDATION_SUMMARY: ReleasePlatformValidationSummary = {
|
||||
const WINDOWS_VALIDATION_SUMMARY_FALLBACK: ReleasePlatformValidationSummary = {
|
||||
lane: 'Windows Unreal packaged validation',
|
||||
result: 'passed',
|
||||
generated_at: '2026-06-22T01:43:08.7625247Z',
|
||||
generated_at: '2026-06-23T10:27:31.6439999Z',
|
||||
configuration: 'Development',
|
||||
skip_build: true,
|
||||
smoke_map_count: 2,
|
||||
|
|
@ -37,6 +38,43 @@ const WINDOWS_VALIDATION_SUMMARY: ReleasePlatformValidationSummary = {
|
|||
],
|
||||
}
|
||||
|
||||
function isValidationSmokeMap(
|
||||
input: unknown,
|
||||
): input is ReleasePlatformValidationSmokeMap {
|
||||
return Boolean(
|
||||
input
|
||||
&& typeof input === 'object'
|
||||
&& typeof (input as ReleasePlatformValidationSmokeMap).map_url === 'string'
|
||||
&& typeof (input as ReleasePlatformValidationSmokeMap).label === 'string'
|
||||
&& ((input as ReleasePlatformValidationSmokeMap).result === 'passed'
|
||||
|| (input as ReleasePlatformValidationSmokeMap).result === 'failed'),
|
||||
)
|
||||
}
|
||||
|
||||
function isValidationSummary(
|
||||
input: unknown,
|
||||
): input is ReleasePlatformValidationSummary {
|
||||
return Boolean(
|
||||
input
|
||||
&& typeof input === 'object'
|
||||
&& typeof (input as ReleasePlatformValidationSummary).lane === 'string'
|
||||
&& ((input as ReleasePlatformValidationSummary).result === 'passed'
|
||||
|| (input as ReleasePlatformValidationSummary).result === 'failed')
|
||||
&& typeof (input as ReleasePlatformValidationSummary).generated_at === 'string'
|
||||
&& typeof (input as ReleasePlatformValidationSummary).configuration === 'string'
|
||||
&& typeof (input as ReleasePlatformValidationSummary).skip_build === 'boolean'
|
||||
&& typeof (input as ReleasePlatformValidationSummary).smoke_map_count === 'number'
|
||||
&& Array.isArray((input as ReleasePlatformValidationSummary).smoke_maps)
|
||||
&& (input as ReleasePlatformValidationSummary).smoke_maps.every(isValidationSmokeMap),
|
||||
)
|
||||
}
|
||||
|
||||
const WINDOWS_VALIDATION_SUMMARY: ReleasePlatformValidationSummary = isValidationSummary(
|
||||
generatedWindowsValidationSummary,
|
||||
)
|
||||
? generatedWindowsValidationSummary
|
||||
: WINDOWS_VALIDATION_SUMMARY_FALLBACK
|
||||
|
||||
export function getReleasePlatformValidationSummary(
|
||||
platformKey: DownloadPlatformKey,
|
||||
): ReleasePlatformValidationSummary | null {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,22 @@ function readTrimmedEnv(name: string, fallback = '') {
|
|||
return String(env[name] || fallback).trim()
|
||||
}
|
||||
|
||||
function readBooleanEnv(...names: string[]) {
|
||||
for (const name of names) {
|
||||
const value = String(env[name] || '').trim().toLowerCase()
|
||||
if (!value) {
|
||||
continue
|
||||
}
|
||||
if (value === '1' || value === 'true' || value === 'yes' || value === 'on') {
|
||||
return true
|
||||
}
|
||||
if (value === '0' || value === 'false' || value === 'no' || value === 'off') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const brandConfig = {
|
||||
brandName: 'HyperTwist',
|
||||
legalName: 'HyperTwist',
|
||||
|
|
@ -28,7 +44,7 @@ type DownloadTarget = {
|
|||
platformKey: DownloadPlatformKey
|
||||
platform: string
|
||||
subtitle: string
|
||||
href: string
|
||||
configured: boolean
|
||||
details: string
|
||||
}
|
||||
|
||||
|
|
@ -82,21 +98,21 @@ export const downloadTargets = [
|
|||
platformKey: 'windows',
|
||||
platform: 'Windows',
|
||||
subtitle: 'Primary shipping lane',
|
||||
href: readTrimmedEnv('VITE_WINDOWS_DOWNLOAD_URL'),
|
||||
configured: readBooleanEnv('VITE_WINDOWS_RELEASE_CONFIGURED', 'VITE_WINDOWS_DOWNLOAD_CONFIGURED'),
|
||||
details: 'Current packaged validation is strongest on the Windows Unreal lane.',
|
||||
},
|
||||
{
|
||||
platformKey: 'macos',
|
||||
platform: 'macOS',
|
||||
subtitle: 'Planned distribution surface',
|
||||
href: readTrimmedEnv('VITE_MAC_DOWNLOAD_URL'),
|
||||
configured: readBooleanEnv('VITE_MAC_RELEASE_CONFIGURED', 'VITE_MAC_DOWNLOAD_CONFIGURED'),
|
||||
details: 'List a signed desktop build here when the package lane is opened.',
|
||||
},
|
||||
{
|
||||
platformKey: 'linux',
|
||||
platform: 'Linux',
|
||||
subtitle: 'Operator-targeted later lane',
|
||||
href: readTrimmedEnv('VITE_LINUX_DOWNLOAD_URL'),
|
||||
configured: readBooleanEnv('VITE_LINUX_RELEASE_CONFIGURED', 'VITE_LINUX_DOWNLOAD_CONFIGURED'),
|
||||
details: 'Use for future package publication after the bounded release lane is widened.',
|
||||
},
|
||||
] as const satisfies readonly DownloadTarget[]
|
||||
|
|
@ -109,9 +125,9 @@ export const openSourceRepoUrl = readTrimmedEnv('VITE_OPEN_SOURCE_REPO_URL')
|
|||
export const launchReadiness = {
|
||||
operatorCheckoutConfigured: Boolean(operatorCheckoutUrl),
|
||||
studioCheckoutConfigured: Boolean(studioCheckoutUrl),
|
||||
windowsDownloadConfigured: Boolean(downloadTargets.find((target) => target.platform === 'Windows')?.href),
|
||||
macDownloadConfigured: Boolean(downloadTargets.find((target) => target.platform === 'macOS')?.href),
|
||||
linuxDownloadConfigured: Boolean(downloadTargets.find((target) => target.platform === 'Linux')?.href),
|
||||
windowsDownloadConfigured: Boolean(downloadTargets.find((target) => target.platformKey === 'windows')?.configured),
|
||||
macDownloadConfigured: Boolean(downloadTargets.find((target) => target.platformKey === 'macos')?.configured),
|
||||
linuxDownloadConfigured: Boolean(downloadTargets.find((target) => target.platformKey === 'linux')?.configured),
|
||||
mplSourceConfigured: Boolean(mplSourceUrl),
|
||||
openSourceRepoConfigured: Boolean(openSourceRepoUrl),
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { brandConfig, downloadTargets, mplSourceUrl, openSourceRepoUrl, publicDocsUrl, releaseNotesUrl } from './site-config'
|
||||
import { brandConfig, mplSourceUrl, openSourceRepoUrl, publicDocsUrl, releaseNotesUrl } from './site-config'
|
||||
|
||||
export const heroMetrics = [
|
||||
{ label: 'Current runtime center', value: 'Native Unreal' },
|
||||
|
|
@ -228,6 +228,36 @@ export const inputAndDevicePostureCards = [
|
|||
},
|
||||
] as const
|
||||
|
||||
export const runtimeControlGuideCards = [
|
||||
{
|
||||
title: 'Classic-cube desktop session',
|
||||
description: 'This is the current practical control posture for the packaged classic-cube runtime.',
|
||||
bullets: [
|
||||
'Use left click or touch to drive face interaction in the live training lane.',
|
||||
'Use middle-mouse drag to orbit and scroll-wheel zoom to reframe the active cube during practice or replay review.',
|
||||
'Use the shipped fresh-attempt, hint, and hold-to-talk shortcuts when you want restart, guidance, or bounded voice interaction from the native runtime.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Higher-dimensional desktop session',
|
||||
description: 'This is the current practical control posture for the dedicated Magic120Cell and MagicCube5D runtime families.',
|
||||
bullets: [
|
||||
'Launch the dedicated-family desktop maps instead of expecting the public website to host the serious runtime lane.',
|
||||
'Use the current keyboard-driven slice, layer, or rotation interaction together with the family-owned projection and focus defaults.',
|
||||
'Treat symmetry, stereo, visibility, and focus posture as current packaged runtime ownership rather than as a fully generalized preferences suite.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Current control boundary',
|
||||
description: 'This keeps the manual useful by being explicit about what is supported now and what still belongs to a later native completion packet.',
|
||||
bullets: [
|
||||
'Use keyboard and mouse as the current strong operator-facing control lane.',
|
||||
'Do not assume a finished headset-specific VR onboarding, controller rebinding, or broad preferences UI has already landed.',
|
||||
'Treat future XR/controller/settings widening as a separate native completion packet, not as something the public website should overclaim today.',
|
||||
],
|
||||
},
|
||||
] as const
|
||||
|
||||
export const deploymentReadinessTracks = [
|
||||
{
|
||||
title: '1. Identity and access posture',
|
||||
|
|
@ -487,6 +517,11 @@ export const resourceCollections = [
|
|||
] as const
|
||||
|
||||
export const changelogEntries = [
|
||||
{
|
||||
date: 'June 23, 2026',
|
||||
title: 'Higher-dimensional package lane refreshed and public launch manual widened',
|
||||
details: 'The maintained Windows higher-dimensional package helper was revalidated after the recovered editor rebuild, and the public download/support surfaces gained first-launch desktop setup plus clearer operator escalation guidance.',
|
||||
},
|
||||
{
|
||||
date: 'June 22, 2026',
|
||||
title: 'Public HyperTwist website, auth shell, and distribution surfaces landed',
|
||||
|
|
@ -579,6 +614,45 @@ export const desktopDownloadSteps = [
|
|||
'Generate a desktop-link token in the browser dashboard so the installed app can pair to your account without exposing credentials.',
|
||||
] as const
|
||||
|
||||
export const desktopFirstLaunchCards = [
|
||||
{
|
||||
title: 'Install and verify the delivered build',
|
||||
description: 'Treat the first launch as part of release validation, not as a detached post-download afterthought.',
|
||||
bullets: [
|
||||
'Confirm you installed the platform build that matches the dashboard-selected release target.',
|
||||
'Review the release notes and package-validation summary before broad internal rollout.',
|
||||
'Keep the open-source notices and corresponding-source links reachable anywhere the build is redistributed.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Pair desktop access to the browser account',
|
||||
description: 'Identity should pass through the bounded desktop-link handoff instead of through password reuse inside the runtime.',
|
||||
bullets: [
|
||||
'Open the protected dashboard and generate a desktop-link token.',
|
||||
'Use that token during first launch so the installed runtime inherits the correct account and entitlement posture.',
|
||||
'Return to the dashboard if the account, plan, or release lane changes later.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Verify the current training lanes',
|
||||
description: 'First launch should confirm the real simulator posture HyperTwist already claims publicly.',
|
||||
bullets: [
|
||||
'Open the classic-cube training lane to confirm timing, replay, and coaching posture.',
|
||||
'Open the dedicated Magic120Cell or MagicCube5D training maps to confirm the higher-dimensional packaged lane.',
|
||||
'Treat any unfinished XR/controller/preferences behavior as a later native packet, not as a first-launch surprise.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Know when to return to browser surfaces',
|
||||
description: 'The browser shell stays relevant after install because rollout and entitlement work remain there on purpose.',
|
||||
bullets: [
|
||||
'Use the dashboard for account, billing, and release-manifest review.',
|
||||
'Use support and notices pages for rollout, legal, or access questions.',
|
||||
'Use public docs and resources pages when you need the product-safe operator manual again.',
|
||||
],
|
||||
},
|
||||
] as const
|
||||
|
||||
export const desktopReleaseSignals = [
|
||||
{
|
||||
title: 'Package validation truth stays visible',
|
||||
|
|
@ -594,6 +668,45 @@ export const desktopReleaseSignals = [
|
|||
},
|
||||
] as const
|
||||
|
||||
export const supportEscalationCards = [
|
||||
{
|
||||
title: 'Account and entitlement issues',
|
||||
description: 'Use the browser/operator lane first when the problem is access rather than simulator execution.',
|
||||
bullets: [
|
||||
'Missing plan access, absent download buttons, or failed desktop-link pairing belong to the protected dashboard lane.',
|
||||
'Support should confirm auth, billing, and entitlement posture before troubleshooting simulator behavior.',
|
||||
'Do not treat account-state issues as proof that the packaged runtime itself is broken.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Download and install issues',
|
||||
description: 'Use the release lane and package evidence before widening into runtime diagnosis.',
|
||||
bullets: [
|
||||
'Confirm the selected platform, release version, and package validation summary first.',
|
||||
'Recheck notices, release notes, and rollout instructions before redistributing the build internally.',
|
||||
'Escalate with the exact target platform and release channel when the package itself is the issue.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Runtime and training issues',
|
||||
description: 'Desktop runtime issues should be described in simulator terms, not only in website terms.',
|
||||
bullets: [
|
||||
'Call out whether the issue affects classic-cube training, replay/coaching, or higher-dimensional family maps.',
|
||||
'Separate unfinished XR/controller/preferences expectations from actual regressions in the shipped keyboard/mouse lanes.',
|
||||
'Keep MagicTile browser-host behavior and dedicated-family packaged-map behavior distinguished when reporting issues.',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Rollout and compliance issues',
|
||||
description: 'When the question is about public launch safety, support should keep package, pricing, and legal posture tied together.',
|
||||
bullets: [
|
||||
'Route pricing, notices, and corresponding-source questions through the public/support lane instead of improvising local answers.',
|
||||
'Treat public distribution pages as part of the shipped release posture.',
|
||||
'Keep release, payment, and legal guidance synchronized before broader operator rollout.',
|
||||
],
|
||||
},
|
||||
] as const
|
||||
|
||||
export const companyNarrative = {
|
||||
mission: 'HyperTwist closes the gap between physical cubing practice, deep replay analysis, operator-grade coaching, and serious higher-dimensional puzzle study.',
|
||||
posture:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue