feat: finalize restore create ux productization #403
@ -12,6 +12,8 @@
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Resources\Pages\Concerns\HasWizard;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Schemas\Components\Component;
|
||||
use Filament\Schemas\Components\Wizard;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Livewire\Attributes\On;
|
||||
|
||||
@ -48,8 +50,24 @@ public function getSteps(): array
|
||||
return RestoreRunResource::getWizardSteps();
|
||||
}
|
||||
|
||||
public function getWizardComponent(): Component
|
||||
{
|
||||
return Wizard::make($this->getSteps())
|
||||
->startOnStep($this->getStartStep())
|
||||
->cancelAction($this->getCancelFormAction())
|
||||
->submitAction($this->getSubmitFormAction())
|
||||
->alpineSubmitHandler("\$wire.{$this->getSubmitFormLivewireMethodName()}()")
|
||||
->skippable($this->hasSkippableSteps())
|
||||
->contained(false)
|
||||
->extraAttributes(['class' => 'restore-run-create-wizard']);
|
||||
}
|
||||
|
||||
protected function afterFill(): void
|
||||
{
|
||||
if (($this->data['is_dry_run'] ?? null) !== false) {
|
||||
$this->data['is_dry_run'] = true;
|
||||
}
|
||||
|
||||
$backupSetIdRaw = request()->query('backup_set_id');
|
||||
|
||||
if (! is_numeric($backupSetIdRaw)) {
|
||||
@ -141,7 +159,9 @@ private function normalizeBackupItemIds(mixed $raw): array
|
||||
protected function getSubmitFormAction(): Action
|
||||
{
|
||||
return parent::getSubmitFormAction()
|
||||
->label('Create restore run')
|
||||
->label(fn (): string => RestoreRunResource::restoreWizardSubmitLabel(
|
||||
is_array($this->data ?? null) ? $this->data : [],
|
||||
))
|
||||
->icon('heroicon-o-check-circle');
|
||||
}
|
||||
|
||||
|
||||
@ -166,9 +166,16 @@ public static function contract(
|
||||
: false;
|
||||
$scopeDefined = $backupSetId !== null && ($scopeMode === 'all' || $selectedItemCount > 0);
|
||||
$scopeDependencyResolved = $scopeDefined
|
||||
&& $mappedGroupCount >= $unresolvedGroupCount
|
||||
&& ($checksAreCurrent || $previewIsCurrent);
|
||||
&& $mappedGroupCount >= $unresolvedGroupCount;
|
||||
$executionAvailableAfterConfirmation = $executionTechnicallyAllowed && $checksAreCurrent && $previewIsCurrent;
|
||||
$providerCredentialBlocked = is_array($providerResolution) && array_key_exists('resolved', $providerResolution)
|
||||
? ! (bool) ($providerResolution['resolved'] ?? true)
|
||||
: false;
|
||||
$checkSummary = is_array($draft['check_summary'] ?? null) ? $draft['check_summary'] : [];
|
||||
$checkResults = is_array($draft['check_results'] ?? null) ? $draft['check_results'] : [];
|
||||
$previewSummaryDraft = is_array($draft['preview_summary'] ?? null) ? $draft['preview_summary'] : [];
|
||||
$previewDiffsDraft = is_array($draft['preview_diffs'] ?? null) ? $draft['preview_diffs'] : [];
|
||||
$validationBlockingCount = (int) ($checkSummary['blocking'] ?? ($checksIntegrity['blocking_count'] ?? 0));
|
||||
|
||||
$scopeDescription = self::restoreWizardScopeDescription(
|
||||
scopeMode: $scopeMode,
|
||||
@ -199,11 +206,31 @@ public static function contract(
|
||||
$executionGateSummary = match (true) {
|
||||
! $scopeDefined => 'Define the restore scope before validation can run.',
|
||||
$unresolvedGroupCount > $mappedGroupCount => 'Resolve required mappings before validation can run.',
|
||||
! $executionTechnicallyAllowed => 'Restore execution is blocked until required prerequisites are healthy again. Evidence does not exist yet.',
|
||||
! $executionTechnicallyAllowed => 'Restore execution is blocked until required prerequisites are available. Evidence does not exist yet.',
|
||||
$executionAvailableAfterConfirmation => 'Execution becomes available after explicit confirmation. Post-run evidence starts only after execution.',
|
||||
default => 'Execution and post-run evidence remain unavailable until required safety gates are complete.',
|
||||
};
|
||||
|
||||
$wizardGate = self::wizardGate(
|
||||
currentStep: $currentStep,
|
||||
backupSelected: $backupSet instanceof BackupSet,
|
||||
hasUsableSource: $hasUsableSource,
|
||||
targetSelected: $targetSelected,
|
||||
scopeDefined: $scopeDefined,
|
||||
scopeDependencyResolved: $scopeDependencyResolved,
|
||||
checksAreCurrent: $checksAreCurrent,
|
||||
previewIsCurrent: $previewIsCurrent,
|
||||
executionTechnicallyAllowed: $executionTechnicallyAllowed,
|
||||
validationBlockingCount: $validationBlockingCount,
|
||||
providerCredentialBlocked: $providerCredentialBlocked,
|
||||
executionGateSummary: $executionGateSummary,
|
||||
);
|
||||
$wizardGate = self::withConfirmationActionState(
|
||||
wizardGate: $wizardGate,
|
||||
draft: $draft,
|
||||
tenant: $tenant,
|
||||
);
|
||||
|
||||
$decisionCard = self::restoreWizardDecisionCard(
|
||||
backupSet: $backupSet,
|
||||
backupQuality: $backupQuality,
|
||||
@ -214,23 +241,16 @@ public static function contract(
|
||||
safetyAssessment: is_array($safetyAssessment) ? $safetyAssessment : [],
|
||||
);
|
||||
|
||||
if ($backupSet instanceof BackupSet && $hasUsableSource && (! $scopeDependencyResolved || ! $checksAreCurrent || ! $previewIsCurrent)) {
|
||||
if ($backupSet instanceof BackupSet && $hasUsableSource && $unresolvedGroupCount > $mappedGroupCount) {
|
||||
$decisionCard['nextAction'] = 'Continue to scope and resolve required mappings.';
|
||||
} elseif ($backupSet instanceof BackupSet && ! $executionTechnicallyAllowed) {
|
||||
} elseif ($backupSet instanceof BackupSet && $hasUsableSource && (! $checksAreCurrent || ! $previewIsCurrent)) {
|
||||
$decisionCard['nextAction'] = 'Continue to scope.';
|
||||
} elseif ($backupSet instanceof BackupSet && $hasUsableSource && ! $executionTechnicallyAllowed) {
|
||||
$decisionCard['nextAction'] = 'Review prerequisites before execution.';
|
||||
}
|
||||
|
||||
$backupQualityCard = self::restoreWizardBackupQualityCard($backupQuality, $hasUsableSource);
|
||||
|
||||
$nextGate = match (true) {
|
||||
! $hasUsableSource => 'Usable source selected',
|
||||
! $targetSelected => 'Target selected',
|
||||
! $scopeDependencyResolved => 'Scope/dependency mapping',
|
||||
! $previewIsCurrent => 'Preview',
|
||||
! $executionTechnicallyAllowed => 'Execution prerequisites',
|
||||
default => 'Confirmation',
|
||||
};
|
||||
|
||||
$processFlow = [
|
||||
'compact' => $compactFlow,
|
||||
'title' => 'Restore safety gates',
|
||||
@ -242,9 +262,9 @@ public static function contract(
|
||||
$checksAreCurrent,
|
||||
$previewIsCurrent,
|
||||
])),
|
||||
'nextGate' => $nextGate,
|
||||
'executionLabel' => $executionAvailableAfterConfirmation ? 'Available after confirmation' : 'Unavailable',
|
||||
'executionSummary' => $executionGateSummary,
|
||||
'nextGate' => (string) ($wizardGate['next_gate_label'] ?? 'Unavailable'),
|
||||
'executionLabel' => (string) ($wizardGate['execution_label'] ?? 'Unavailable'),
|
||||
'executionSummary' => (string) ($wizardGate['execution_summary'] ?? $executionGateSummary),
|
||||
'steps' => [
|
||||
[
|
||||
'step' => 1,
|
||||
@ -283,10 +303,12 @@ public static function contract(
|
||||
[
|
||||
'step' => 6,
|
||||
'label' => 'Confirmation',
|
||||
'summary' => $executionAvailableAfterConfirmation
|
||||
? 'Explicit confirmation is the next required gate before real execution.'
|
||||
: 'Confirmation stays unavailable until validation and preview evidence are current.',
|
||||
'status' => self::restoreWizardGateStatus(false, required: $executionAvailableAfterConfirmation),
|
||||
'summary' => match (true) {
|
||||
$checksAreCurrent && $previewIsCurrent && ! $executionTechnicallyAllowed => 'Confirmation can be reviewed, but real execution remains blocked until prerequisites are resolved.',
|
||||
$checksAreCurrent && $previewIsCurrent => 'Explicit confirmation is the next required gate before real execution.',
|
||||
default => 'Confirmation stays unavailable until validation and preview evidence are current.',
|
||||
},
|
||||
'status' => self::restoreWizardGateStatus(false, required: $checksAreCurrent && $previewIsCurrent),
|
||||
],
|
||||
[
|
||||
'step' => 7,
|
||||
@ -294,7 +316,7 @@ public static function contract(
|
||||
'summary' => $executionGateSummary,
|
||||
'status' => self::restoreWizardGateStatus(
|
||||
false,
|
||||
required: $executionAvailableAfterConfirmation,
|
||||
required: $checksAreCurrent && $previewIsCurrent,
|
||||
blocked: ! $executionTechnicallyAllowed,
|
||||
),
|
||||
],
|
||||
@ -350,41 +372,29 @@ public static function contract(
|
||||
];
|
||||
|
||||
$validationSummary = self::validationSummary(
|
||||
checkSummary: is_array($draft['check_summary'] ?? null) ? $draft['check_summary'] : [],
|
||||
checkResults: is_array($draft['check_results'] ?? null) ? $draft['check_results'] : [],
|
||||
checkSummary: $checkSummary,
|
||||
checkResults: $checkResults,
|
||||
checksIntegrity: $checksIntegrity,
|
||||
executionReadiness: is_array($executionReadiness) ? $executionReadiness : [],
|
||||
safetyAssessment: is_array($safetyAssessment) ? $safetyAssessment : [],
|
||||
providerResolution: is_array($providerResolution) ? $providerResolution : [],
|
||||
providerConnectionsUrl: $providerConnectionsUrl,
|
||||
wizardGate: $wizardGate,
|
||||
);
|
||||
|
||||
$previewSummary = self::previewSummary(
|
||||
previewSummary: is_array($draft['preview_summary'] ?? null) ? $draft['preview_summary'] : [],
|
||||
previewDiffs: is_array($draft['preview_diffs'] ?? null) ? $draft['preview_diffs'] : [],
|
||||
previewSummary: $previewSummaryDraft,
|
||||
previewDiffs: $previewDiffsDraft,
|
||||
previewIntegrity: $previewIntegrity,
|
||||
checksIntegrity: $checksIntegrity,
|
||||
executionReadiness: is_array($executionReadiness) ? $executionReadiness : [],
|
||||
safetyAssessment: is_array($safetyAssessment) ? $safetyAssessment : [],
|
||||
scope: $scope,
|
||||
wizardGate: $wizardGate,
|
||||
);
|
||||
|
||||
$canContinue = match ($currentStep) {
|
||||
1 => $backupSet instanceof BackupSet,
|
||||
2 => ! (bool) ($mappingResolver['requiresResolution'] ?? false),
|
||||
3 => (int) ($validationSummary['blockingCount'] ?? 0) <= 0 && ! (bool) ($validationSummary['providerCredentialBlocked'] ?? false),
|
||||
4 => (bool) ($previewSummary['canProceedToConfirm'] ?? false),
|
||||
5 => true,
|
||||
default => true,
|
||||
};
|
||||
|
||||
$blockedReason = match ($currentStep) {
|
||||
1 => $backupSet instanceof BackupSet ? null : 'Select a backup set to continue.',
|
||||
2 => (bool) ($mappingResolver['requiresResolution'] ?? false) ? (string) ($mappingResolver['requirementLabel'] ?? 'Resolve required mappings to continue.') : null,
|
||||
3 => (int) ($validationSummary['blockingCount'] ?? 0) > 0 ? 'Resolve validation blockers before moving to preview.' : null,
|
||||
4 => (string) ($previewSummary['blockedReason'] ?? null),
|
||||
default => null,
|
||||
};
|
||||
$canContinue = (bool) ($wizardGate['can_continue'] ?? true);
|
||||
$blockedReason = $canContinue
|
||||
? null
|
||||
: (string) ($wizardGate['continue_disabled_reason'] ?? 'Complete the required restore gate to continue.');
|
||||
|
||||
return [
|
||||
// Required product UI contract.
|
||||
@ -399,6 +409,7 @@ public static function contract(
|
||||
'mapping_summary' => $mappingResolver,
|
||||
'validation_summary' => $validationSummary,
|
||||
'preview_summary' => $previewSummary,
|
||||
'wizard_gate' => $wizardGate,
|
||||
'can_continue' => $canContinue,
|
||||
'blocked_reason' => $blockedReason,
|
||||
'diagnostics_state' => [
|
||||
@ -408,6 +419,7 @@ public static function contract(
|
||||
],
|
||||
// Component backing data (still passive in views).
|
||||
'decisionCard' => $decisionCard,
|
||||
'backupQualityCard' => $backupQualityCard,
|
||||
'processFlow' => $processFlow,
|
||||
'proofAside' => [
|
||||
'title' => 'Restore Proof',
|
||||
@ -415,6 +427,7 @@ public static function contract(
|
||||
],
|
||||
'validationSummary' => $validationSummary,
|
||||
'previewSummary' => $previewSummary,
|
||||
'wizardGate' => $wizardGate,
|
||||
'mappingResolver' => $mappingResolver,
|
||||
'currentScope' => $scope,
|
||||
'previewIntegrity' => $previewIntegrity,
|
||||
@ -729,6 +742,238 @@ private static function restoreWizardGateStatus(bool $complete, bool $required =
|
||||
return $required ? 'required' : 'unavailable';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function wizardGate(
|
||||
int $currentStep,
|
||||
bool $backupSelected,
|
||||
bool $hasUsableSource,
|
||||
bool $targetSelected,
|
||||
bool $scopeDefined,
|
||||
bool $scopeDependencyResolved,
|
||||
bool $checksAreCurrent,
|
||||
bool $previewIsCurrent,
|
||||
bool $executionTechnicallyAllowed,
|
||||
int $validationBlockingCount,
|
||||
bool $providerCredentialBlocked,
|
||||
string $executionGateSummary,
|
||||
): array {
|
||||
$validationBlocked = ($providerCredentialBlocked && ! $checksAreCurrent)
|
||||
|| ($checksAreCurrent && $validationBlockingCount > 0);
|
||||
$reviewableConfirmation = $checksAreCurrent && $previewIsCurrent && ! $validationBlocked;
|
||||
|
||||
$currentGate = match ($currentStep) {
|
||||
1 => 'source',
|
||||
2 => 'scope_mapping',
|
||||
3 => 'validation',
|
||||
4 => 'preview',
|
||||
5 => 'confirmation',
|
||||
default => 'source',
|
||||
};
|
||||
$currentGateLabel = match ($currentGate) {
|
||||
'scope_mapping' => 'Scope/dependency mapping',
|
||||
'validation' => 'Validation',
|
||||
'preview' => 'Preview',
|
||||
'confirmation' => 'Confirmation',
|
||||
default => 'Source',
|
||||
};
|
||||
|
||||
[$nextGate, $nextGateLabel, $blockingPrerequisite, $blockingPrerequisiteLabel] = match (true) {
|
||||
! $backupSelected => ['source_required', 'Usable source selected', 'source', 'Select a backup set.'],
|
||||
! $hasUsableSource => ['source_required', 'Usable source selected', 'source', 'Select another backup set.'],
|
||||
! $targetSelected => ['target_required', 'Target selected', 'target', 'Target environment is required.'],
|
||||
! $scopeDefined => ['scope_required', 'Scope/dependency mapping', 'scope', 'Define the restore scope.'],
|
||||
! $scopeDependencyResolved => ['scope_dependency_mapping', 'Scope/dependency mapping', 'scope_dependency_mapping', 'Resolve required mappings before validation can run.'],
|
||||
$validationBlocked => ['validation_blocked', 'Validation blocked', 'validation', 'Resolve blocking validation checks before preview or execution.'],
|
||||
! $checksAreCurrent => ['validation_required', 'Validation required', 'validation', 'Run safety checks for the current scope.'],
|
||||
! $previewIsCurrent => ['preview_required', 'Preview required', 'preview', 'Generate a preview for the current scope.'],
|
||||
default => ['confirmation_required', 'Confirmation required', null, null],
|
||||
};
|
||||
|
||||
$requiredAction = match ($nextGate) {
|
||||
'source_required' => $backupSelected ? 'select_usable_source' : 'select_source',
|
||||
'target_required' => 'select_target',
|
||||
'scope_required' => 'define_scope',
|
||||
'scope_dependency_mapping' => 'resolve_required_mappings',
|
||||
'validation_blocked' => 'review_validation_blockers',
|
||||
'validation_required' => 'run_validation',
|
||||
'preview_required' => 'generate_preview',
|
||||
default => $executionTechnicallyAllowed
|
||||
? 'review_preview_continue_confirmation'
|
||||
: 'review_preview_continue_confirmation_resolve_execution',
|
||||
};
|
||||
|
||||
$requiredActionLabel = match ($requiredAction) {
|
||||
'select_source' => 'Select backup set.',
|
||||
'select_usable_source' => 'Select another backup set.',
|
||||
'select_target' => 'Select a target environment.',
|
||||
'define_scope' => 'Define restore scope before validation can run.',
|
||||
'resolve_required_mappings' => 'Resolve required mappings before validation can run.',
|
||||
'review_validation_blockers' => $providerCredentialBlocked
|
||||
? 'Repair the provider connection before validation can run.'
|
||||
: 'Resolve blocking validation checks before preview or execution.',
|
||||
'run_validation' => 'Run the safety checks for the current scope before preview.',
|
||||
'generate_preview' => 'Generate a preview for the current scope before confirmation.',
|
||||
'review_preview_continue_confirmation' => 'Review the preview and continue to confirmation.',
|
||||
default => 'Review preview and continue to confirmation; resolve execution prerequisites before executing.',
|
||||
};
|
||||
|
||||
$canContinue = match ($currentStep) {
|
||||
1 => $backupSelected && $hasUsableSource,
|
||||
2 => $scopeDefined && $scopeDependencyResolved,
|
||||
3 => $checksAreCurrent && ! $validationBlocked,
|
||||
4 => $reviewableConfirmation,
|
||||
5 => true,
|
||||
default => true,
|
||||
};
|
||||
|
||||
$continueDisabledReason = $canContinue ? null : match ($currentStep) {
|
||||
1 => $backupSelected
|
||||
? 'Select a backup set with usable captured items to continue.'
|
||||
: 'Select a backup set to continue.',
|
||||
2 => $scopeDefined
|
||||
? 'Resolve required mappings before validation can run.'
|
||||
: 'Define the restore scope before validation can run.',
|
||||
3 => match (true) {
|
||||
$providerCredentialBlocked => 'Provider credentials are not available for this environment. Restore checks cannot run until the provider connection is repaired.',
|
||||
$validationBlockingCount > 0 => 'Resolve blocking validation checks before moving to preview.',
|
||||
default => 'Run safety checks for the current scope before moving to preview.',
|
||||
},
|
||||
4 => match (true) {
|
||||
! $checksAreCurrent => 'Run safety checks for the current scope before confirmation.',
|
||||
! $previewIsCurrent => 'Generate a preview for the current scope before confirmation.',
|
||||
default => 'Complete required restore gates before confirmation.',
|
||||
},
|
||||
default => null,
|
||||
};
|
||||
|
||||
$executionState = match (true) {
|
||||
! $checksAreCurrent || ! $previewIsCurrent => 'unavailable_until_preview',
|
||||
$executionTechnicallyAllowed => 'unavailable_until_confirmation',
|
||||
default => 'unavailable_until_prerequisites',
|
||||
};
|
||||
$executionLabel = match ($executionState) {
|
||||
'unavailable_until_confirmation' => 'Execution unavailable until confirmation',
|
||||
'unavailable_until_prerequisites' => 'Execution unavailable until prerequisites are resolved',
|
||||
default => 'Execution unavailable',
|
||||
};
|
||||
|
||||
$previewState = match (true) {
|
||||
$previewIsCurrent => 'current',
|
||||
$checksAreCurrent => 'required',
|
||||
default => 'unavailable_until_validation',
|
||||
};
|
||||
$confirmationState = match (true) {
|
||||
! $reviewableConfirmation => 'unavailable',
|
||||
$executionTechnicallyAllowed => 'ready_for_review',
|
||||
default => 'reviewable_execution_blocked',
|
||||
};
|
||||
|
||||
return [
|
||||
'current_gate' => $currentGate,
|
||||
'current_gate_label' => $currentGateLabel,
|
||||
'next_gate' => $nextGate,
|
||||
'next_gate_label' => $nextGateLabel,
|
||||
'blocking_prerequisite' => $blockingPrerequisite,
|
||||
'blocking_prerequisite_label' => $blockingPrerequisiteLabel,
|
||||
'required_action' => $requiredAction,
|
||||
'required_action_label' => $requiredActionLabel,
|
||||
'can_continue' => $canContinue,
|
||||
'continue_label' => 'Next',
|
||||
'continue_disabled_reason' => $continueDisabledReason,
|
||||
'execution_state' => $executionState,
|
||||
'execution_label' => $executionLabel,
|
||||
'execution_summary' => $executionGateSummary,
|
||||
'can_execute' => $reviewableConfirmation && $executionTechnicallyAllowed,
|
||||
'proof_state' => 'pre_execution',
|
||||
'preview_state' => $previewState,
|
||||
'confirmation_state' => $confirmationState,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $wizardGate
|
||||
* @param array<string, mixed> $draft
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function withConfirmationActionState(array $wizardGate, array $draft, ?ManagedEnvironment $tenant): array
|
||||
{
|
||||
$confirmationState = (string) ($wizardGate['confirmation_state'] ?? 'unavailable');
|
||||
$canReviewConfirmation = $confirmationState !== 'unavailable';
|
||||
$canExecute = (bool) ($wizardGate['can_execute'] ?? false);
|
||||
$isDryRun = ($draft['is_dry_run'] ?? true) !== false;
|
||||
$acknowledgedImpact = (bool) ($draft['acknowledged_impact'] ?? false);
|
||||
$expectedTenantLabel = self::tenantConfirmationLabel($tenant);
|
||||
$tenantConfirm = is_string($draft['tenant_confirm'] ?? null)
|
||||
? trim($draft['tenant_confirm'])
|
||||
: '';
|
||||
$tenantConfirmed = $expectedTenantLabel === null
|
||||
? false
|
||||
: hash_equals($expectedTenantLabel, $tenantConfirm);
|
||||
|
||||
$confirmationStateLabel = match (true) {
|
||||
! $canReviewConfirmation => 'Confirmation unavailable',
|
||||
! $canExecute => 'Execution prerequisites blocked',
|
||||
$isDryRun => 'Preview-only run ready',
|
||||
$acknowledgedImpact && $tenantConfirmed => 'Execution ready to queue',
|
||||
default => 'Execution confirmation required',
|
||||
};
|
||||
$confirmationStateTone = match (true) {
|
||||
! $canReviewConfirmation => 'gray',
|
||||
! $canExecute => 'warning',
|
||||
$isDryRun => 'success',
|
||||
$acknowledgedImpact && $tenantConfirmed => 'success',
|
||||
default => 'warning',
|
||||
};
|
||||
$primaryCtaLabel = match (true) {
|
||||
! $canReviewConfirmation => 'Complete required gates',
|
||||
$isDryRun || ! $canExecute => 'Create preview-only run',
|
||||
$acknowledgedImpact && $tenantConfirmed => 'Queue restore execution',
|
||||
default => 'Complete execution confirmation',
|
||||
};
|
||||
$primaryNextStepLabel = match (true) {
|
||||
! $canReviewConfirmation => (string) ($wizardGate['required_action_label'] ?? 'Complete required gates before confirmation.'),
|
||||
! $canExecute => 'Create a preview-only run, or resolve execution prerequisites before queueing real execution.',
|
||||
$isDryRun => 'Create a preview-only restore run.',
|
||||
! $acknowledgedImpact && ! $tenantConfirmed => 'Acknowledge impact and type the environment label before queueing real execution.',
|
||||
! $acknowledgedImpact => 'Acknowledge impact before queueing real execution.',
|
||||
! $tenantConfirmed => 'Type the environment label before queueing real execution.',
|
||||
default => 'Queue restore execution.',
|
||||
};
|
||||
$submitDisabledReason = match (true) {
|
||||
! $canReviewConfirmation => 'Complete validation and preview before creating a restore run.',
|
||||
! $isDryRun && ! $canExecute => 'Resolve execution prerequisites before queueing real execution.',
|
||||
! $isDryRun && ! $acknowledgedImpact => 'Acknowledge impact before queueing real execution.',
|
||||
! $isDryRun && ! $tenantConfirmed => 'Type the environment label before queueing real execution.',
|
||||
default => null,
|
||||
};
|
||||
|
||||
return [
|
||||
...$wizardGate,
|
||||
'confirmation_state_label' => $confirmationStateLabel,
|
||||
'confirmation_state_tone' => $confirmationStateTone,
|
||||
'confirmation_mode' => $isDryRun ? 'preview_only' : 'execution',
|
||||
'confirmation_mode_label' => $isDryRun ? 'Preview-only' : 'Real execution',
|
||||
'primary_cta_label' => $primaryCtaLabel,
|
||||
'primary_next_step_label' => $primaryNextStepLabel,
|
||||
'submit_disabled_reason' => $submitDisabledReason,
|
||||
'real_execution_confirmation_satisfied' => ! $isDryRun && $canExecute && $acknowledgedImpact && $tenantConfirmed,
|
||||
'tenant_confirmation_expected' => $expectedTenantLabel,
|
||||
];
|
||||
}
|
||||
|
||||
private static function tenantConfirmationLabel(?ManagedEnvironment $tenant): ?string
|
||||
{
|
||||
if (! $tenant instanceof ManagedEnvironment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tenantIdentifier = $tenant->managed_environment_id ?? $tenant->external_id;
|
||||
|
||||
return (string) ($tenant->name ?? $tenantIdentifier ?? $tenant->getKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $checksIntegrity
|
||||
* @param array<string, mixed> $previewIntegrity
|
||||
@ -755,7 +1000,8 @@ private static function restoreWizardDecisionCard(
|
||||
|
||||
$status = match (true) {
|
||||
! ($backupSet instanceof BackupSet) => 'Source required',
|
||||
! $hasUsableSource => 'Source unavailable',
|
||||
! $hasUsableSource => 'Source not usable',
|
||||
! $checksAreCurrent || ! $previewIsCurrent => 'Source selected',
|
||||
$safetyState !== null => RestoreSafetyCopy::safetyStateLabel($safetyState),
|
||||
default => 'Unavailable',
|
||||
};
|
||||
@ -764,30 +1010,32 @@ private static function restoreWizardDecisionCard(
|
||||
! ($backupSet instanceof BackupSet) => 'No backup set is selected yet.',
|
||||
$backupQuality instanceof BackupQualitySummary && $backupQuality->totalItems === 0 => $backupQuality->summaryMessage,
|
||||
$backupQuality instanceof BackupQualitySummary && ! $hasUsableSource => 'The selected backup captures metadata only or otherwise lacks a usable payload for restore.',
|
||||
$backupQuality instanceof BackupQualitySummary && $backupQuality->hasDegradations() => 'A usable source backup is selected with input-quality items to review.',
|
||||
! $checksAreCurrent || ! $previewIsCurrent => 'A usable source backup is selected.',
|
||||
! $executionTechnicallyAllowed => (string) ($executionReadiness['display_summary'] ?? 'Execution prerequisites are unavailable.'),
|
||||
! $checksAreCurrent => (string) ($checksIntegrity['display_summary'] ?? 'Checks evidence is not current for the selected scope.'),
|
||||
! $previewIsCurrent => (string) ($previewIntegrity['display_summary'] ?? 'Preview evidence is not current for the selected scope.'),
|
||||
default => (string) ($safetyAssessment['summary'] ?? 'Restore safety state is available for the selected scope.'),
|
||||
};
|
||||
|
||||
$impact = match (true) {
|
||||
! ($backupSet instanceof BackupSet) => 'Restore safety cannot be judged until a source backup is selected.',
|
||||
! $hasUsableSource && $backupQuality instanceof BackupQualitySummary => $backupQuality->positiveClaimBoundary,
|
||||
! $checksAreCurrent || ! $previewIsCurrent => 'Scope, validation, and preview must still prove this draft before confirmation or real execution.',
|
||||
! $executionTechnicallyAllowed => 'Provider readiness or restore prerequisites currently prevent real execution.',
|
||||
! $checksAreCurrent || ! $previewIsCurrent => 'Confirmation and real execution must stay blocked until current validation and preview evidence exist.',
|
||||
($safetyAssessment['state'] ?? null) === 'ready_with_caution' => 'Execution can start, but calm safety claims stay suppressed until warnings are reviewed.',
|
||||
default => 'Execution can move toward confirmation, but recovery is not yet verified before post-run evidence exists.',
|
||||
};
|
||||
|
||||
$primaryNextAction = match (true) {
|
||||
! ($backupSet instanceof BackupSet) => 'Select a backup set to establish the restore source.',
|
||||
! $hasUsableSource && $backupQuality instanceof BackupQualitySummary => $backupQuality->nextAction,
|
||||
! ($backupSet instanceof BackupSet) => 'Select backup set.',
|
||||
! $hasUsableSource && $backupQuality instanceof BackupQualitySummary => 'Select another backup set.',
|
||||
default => RestoreSafetyCopy::primaryNextAction($assessmentNextAction),
|
||||
};
|
||||
|
||||
$tone = match (true) {
|
||||
! ($backupSet instanceof BackupSet) => 'gray',
|
||||
! $hasUsableSource => 'warning',
|
||||
$backupQuality instanceof BackupQualitySummary && $backupQuality->hasDegradations() => 'warning',
|
||||
! $checksAreCurrent || ! $previewIsCurrent => 'success',
|
||||
($safetyAssessment['state'] ?? null) === 'ready' => 'success',
|
||||
($safetyAssessment['state'] ?? null) === 'ready_with_caution' => 'warning',
|
||||
default => 'danger',
|
||||
@ -834,18 +1082,26 @@ private static function restoreWizardBackupQualityCard(?BackupQualitySummary $ba
|
||||
$summary = match (true) {
|
||||
$backupQuality->totalItems === 0 => $backupQuality->summaryMessage,
|
||||
! $hasUsableSource => $backupQuality->summaryMessage,
|
||||
default => 'Backup quality hints describe input strength only.',
|
||||
default => $backupQuality->summaryMessage,
|
||||
};
|
||||
|
||||
$tone = match (true) {
|
||||
$backupQuality->totalItems === 0 => 'gray',
|
||||
! $hasUsableSource => 'warning',
|
||||
$backupQuality->degradedItemCount > 0 => 'warning',
|
||||
default => 'success',
|
||||
};
|
||||
|
||||
return [
|
||||
'available' => true,
|
||||
'status' => $status,
|
||||
'tone' => $tone,
|
||||
'summary' => $summary,
|
||||
'nextAction' => $backupQuality->nextAction,
|
||||
'positiveClaimBoundary' => $backupQuality->positiveClaimBoundary,
|
||||
'counts' => [
|
||||
['label' => 'Items', 'value' => $backupQuality->totalItems],
|
||||
['label' => 'Degraded', 'value' => $backupQuality->degradedItemCount],
|
||||
['label' => 'Items captured', 'value' => $backupQuality->totalItems],
|
||||
['label' => 'Degraded items', 'value' => $backupQuality->degradedItemCount],
|
||||
['label' => 'Metadata-only', 'value' => $backupQuality->metadataOnlyCount],
|
||||
['label' => 'Assignment issues', 'value' => $backupQuality->assignmentIssueCount],
|
||||
['label' => 'Orphaned assignments', 'value' => $backupQuality->orphanedAssignmentCount],
|
||||
@ -860,6 +1116,7 @@ private static function restoreWizardBackupQualityCard(?BackupQualitySummary $ba
|
||||
* @param array<string, mixed> $executionReadiness
|
||||
* @param array<string, mixed> $safetyAssessment
|
||||
* @param array<string, mixed> $providerResolution
|
||||
* @param array<string, mixed> $wizardGate
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function validationSummary(
|
||||
@ -870,6 +1127,7 @@ private static function validationSummary(
|
||||
array $safetyAssessment,
|
||||
array $providerResolution,
|
||||
?string $providerConnectionsUrl,
|
||||
array $wizardGate,
|
||||
): array {
|
||||
$blocking = (int) ($checkSummary['blocking'] ?? ($checksIntegrity['blocking_count'] ?? 0));
|
||||
$warning = (int) ($checkSummary['warning'] ?? ($checksIntegrity['warning_count'] ?? 0));
|
||||
@ -913,7 +1171,7 @@ private static function validationSummary(
|
||||
'blockingCount' => $blocking,
|
||||
'warningCount' => $warning,
|
||||
'safeCount' => $safe,
|
||||
'nextActionLabel' => RestoreSafetyCopy::primaryNextAction($nextAction),
|
||||
'nextActionLabel' => (string) ($wizardGate['required_action_label'] ?? RestoreSafetyCopy::primaryNextAction($nextAction)),
|
||||
'executionAllowed' => (bool) ($executionReadiness['allowed'] ?? false),
|
||||
'executionReadinessSummary' => (string) ($executionReadiness['display_summary'] ?? 'Execution prerequisites are unavailable.'),
|
||||
'executionReadinessTone' => $startabilityTone,
|
||||
@ -931,9 +1189,8 @@ private static function validationSummary(
|
||||
* @param array<int|string, mixed> $previewDiffs
|
||||
* @param array<string, mixed> $previewIntegrity
|
||||
* @param array<string, mixed> $checksIntegrity
|
||||
* @param array<string, mixed> $executionReadiness
|
||||
* @param array<string, mixed> $safetyAssessment
|
||||
* @param array<string, mixed> $scope
|
||||
* @param array<string, mixed> $wizardGate
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function previewSummary(
|
||||
@ -941,9 +1198,8 @@ private static function previewSummary(
|
||||
array $previewDiffs,
|
||||
array $previewIntegrity,
|
||||
array $checksIntegrity,
|
||||
array $executionReadiness,
|
||||
array $safetyAssessment,
|
||||
array $scope,
|
||||
array $wizardGate,
|
||||
): array {
|
||||
$integritySpec = BadgeRenderer::spec(
|
||||
BadgeDomain::RestorePreviewDecision,
|
||||
@ -952,15 +1208,11 @@ private static function previewSummary(
|
||||
|
||||
$previewIsCurrent = ($previewIntegrity['state'] ?? null) === PreviewIntegrityState::STATE_CURRENT;
|
||||
$checksAreCurrent = ($checksIntegrity['state'] ?? null) === ChecksIntegrityState::STATE_CURRENT;
|
||||
$executionAllowed = (bool) ($executionReadiness['allowed'] ?? false);
|
||||
|
||||
$primaryNextAction = is_string($safetyAssessment['primary_next_action'] ?? null)
|
||||
? $safetyAssessment['primary_next_action']
|
||||
: 'generate_preview';
|
||||
|
||||
if ($previewIsCurrent && $checksAreCurrent && $executionAllowed && $primaryNextAction === 'execute') {
|
||||
$primaryNextAction = 'review_and_confirm';
|
||||
}
|
||||
$primaryNextActionLabel = (string) ($wizardGate['required_action_label'] ?? match (true) {
|
||||
! $checksAreCurrent => 'Run the safety checks for the current scope before confirmation.',
|
||||
! $previewIsCurrent => 'Generate a preview for the current scope before confirmation.',
|
||||
default => 'Review the preview and continue to confirmation.',
|
||||
});
|
||||
|
||||
$needsAttention = [];
|
||||
$unchanged = [];
|
||||
@ -980,6 +1232,15 @@ private static function previewSummary(
|
||||
$scopeTagsDelta = (bool) ($entry['scope_tags_changed'] ?? false);
|
||||
|
||||
$isUnchanged = $added === 0 && $removed === 0 && $changed === 0 && ! $assignmentsDelta && ! $scopeTagsDelta;
|
||||
$entry = self::previewRowContract(
|
||||
entry: $entry,
|
||||
unchanged: $isUnchanged,
|
||||
added: $added,
|
||||
removed: $removed,
|
||||
changed: $changed,
|
||||
assignmentsDelta: $assignmentsDelta,
|
||||
scopeTagsDelta: $scopeTagsDelta,
|
||||
);
|
||||
|
||||
if ($isUnchanged) {
|
||||
$unchanged[] = $entry;
|
||||
@ -990,13 +1251,15 @@ private static function previewSummary(
|
||||
$needsAttention[] = $entry;
|
||||
}
|
||||
|
||||
$canProceedToConfirm = $checksAreCurrent && $previewIsCurrent && $executionAllowed;
|
||||
$canProceedToConfirm = $checksAreCurrent
|
||||
&& $previewIsCurrent
|
||||
&& ($wizardGate['next_gate'] ?? null) !== 'validation_blocked';
|
||||
|
||||
$blockedReason = match (true) {
|
||||
$canProceedToConfirm => null,
|
||||
is_string($wizardGate['continue_disabled_reason'] ?? null) => $wizardGate['continue_disabled_reason'],
|
||||
! $checksAreCurrent => 'Run safety checks for the current scope before confirmation.',
|
||||
! $previewIsCurrent => 'Generate a preview for the current scope before confirmation.',
|
||||
! $executionAllowed => 'Review prerequisites before confirmation.',
|
||||
default => 'Complete required gates before confirmation.',
|
||||
};
|
||||
|
||||
@ -1011,9 +1274,67 @@ private static function previewSummary(
|
||||
'unchangedDiffs' => $unchanged,
|
||||
'scopeMode' => $scopeMode,
|
||||
'scopeLabel' => $scopeMode === 'selected' ? 'All selected restore items' : 'All restore items',
|
||||
'primaryNextActionLabel' => RestoreSafetyCopy::primaryNextAction($primaryNextAction),
|
||||
'primaryNextActionLabel' => $primaryNextActionLabel,
|
||||
'nextGateLabel' => (string) ($wizardGate['next_gate_label'] ?? (($previewIsCurrent && $checksAreCurrent) ? 'Confirmation required' : ($checksAreCurrent ? 'Preview required' : 'Validation required'))),
|
||||
'canProceedToConfirm' => $canProceedToConfirm,
|
||||
'blockedReason' => $blockedReason,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $entry
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function previewRowContract(
|
||||
array $entry,
|
||||
bool $unchanged,
|
||||
int $added,
|
||||
int $removed,
|
||||
int $changed,
|
||||
bool $assignmentsDelta,
|
||||
bool $scopeTagsDelta,
|
||||
): array {
|
||||
$action = (string) ($entry['action'] ?? '');
|
||||
$diffOmitted = (bool) ($entry['diff_omitted'] ?? false);
|
||||
$policyDiffChanged = $added > 0 || $removed > 0 || $changed > 0;
|
||||
$changedAreas = array_values(array_filter([
|
||||
$policyDiffChanged ? 'policy settings' : null,
|
||||
$assignmentsDelta ? 'assignments' : null,
|
||||
$scopeTagsDelta ? 'scope tags' : null,
|
||||
]));
|
||||
$changedAreaSummary = match (count($changedAreas)) {
|
||||
1 => $changedAreas[0],
|
||||
2 => $changedAreas[0].' and '.$changedAreas[1],
|
||||
3 => $changedAreas[0].', '.$changedAreas[1].', and '.$changedAreas[2],
|
||||
default => null,
|
||||
};
|
||||
|
||||
$reviewReason = match (true) {
|
||||
$diffOmitted => 'Policy diff details were omitted by preview limits.',
|
||||
$action === 'create' => 'Target policy is missing and will be created if restore proceeds.',
|
||||
$changedAreaSummary !== null => Str::ucfirst($changedAreaSummary).' differ from current target state.',
|
||||
$unchanged => 'No changes detected.',
|
||||
default => 'Review preview evidence before confirmation.',
|
||||
};
|
||||
|
||||
$reviewActionLabel = match (true) {
|
||||
$diffOmitted => 'Narrow scope',
|
||||
$unchanged => 'No action needed',
|
||||
$action === 'create' => 'Review create',
|
||||
$action === 'update' || $policyDiffChanged || $assignmentsDelta || $scopeTagsDelta => 'Review changes',
|
||||
default => 'Review item',
|
||||
};
|
||||
|
||||
return [
|
||||
...$entry,
|
||||
'policy_diff_summary' => [
|
||||
'added' => $added,
|
||||
'removed' => $removed,
|
||||
'changed' => $changed,
|
||||
'available' => ! $diffOmitted,
|
||||
],
|
||||
'review_reason' => $reviewReason,
|
||||
'review_action_label' => $reviewActionLabel,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -334,7 +334,7 @@ public function safetyAssessment(ManagedEnvironment $tenant, User $user, array $
|
||||
positiveClaimSuppressed: true,
|
||||
primaryIssueCode: $executionReadiness->blockingReasons[0] ?? 'execution_blocked',
|
||||
primaryNextAction: 'resolve_blockers',
|
||||
summary: 'Restore execution is blocked until required prerequisites are healthy again.',
|
||||
summary: 'Restore execution is blocked until required prerequisites are available.',
|
||||
);
|
||||
}
|
||||
|
||||
@ -346,7 +346,9 @@ public function safetyAssessment(ManagedEnvironment $tenant, User $user, array $
|
||||
checksIntegrity: $checksIntegrity,
|
||||
positiveClaimSuppressed: true,
|
||||
primaryIssueCode: $previewIntegrity->state,
|
||||
primaryNextAction: 'regenerate_preview',
|
||||
primaryNextAction: $previewIntegrity->state === PreviewIntegrityState::STATE_NOT_GENERATED
|
||||
? 'generate_preview'
|
||||
: 'regenerate_preview',
|
||||
summary: 'Execution could start, but the preview basis is not current enough to support a calm go signal.',
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
@php
|
||||
$processFlow = is_array($processFlow ?? null) ? $processFlow : [];
|
||||
$wizardGate = is_array($wizardGate ?? null) ? $wizardGate : [];
|
||||
$steps = is_array($processFlow['steps'] ?? null) ? $processFlow['steps'] : [];
|
||||
$compact = (bool) ($processFlow['compact'] ?? false);
|
||||
|
||||
@ -32,14 +33,14 @@
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">Next gate:</span>
|
||||
<span class="text-gray-600 dark:text-gray-300">{{ $processFlow['nextGate'] ?? 'Unavailable' }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-300">{{ $wizardGate['next_gate_label'] ?? ($processFlow['nextGate'] ?? 'Unavailable') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">Execution:</span>
|
||||
<span class="text-gray-600 dark:text-gray-300">{{ $processFlow['executionLabel'] ?? 'Unavailable' }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-300">{{ $wizardGate['execution_label'] ?? ($processFlow['executionLabel'] ?? 'Unavailable') }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-300">
|
||||
{{ $processFlow['executionSummary'] ?? 'Execution remains unavailable until required safety gates are complete.' }}
|
||||
{{ $wizardGate['execution_summary'] ?? ($processFlow['executionSummary'] ?? 'Execution remains unavailable until required safety gates are complete.') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,49 +1,89 @@
|
||||
@php
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$backupQualityCard = is_array($backupQualityCard ?? null) ? $backupQualityCard : [];
|
||||
$backupQualityCard = is_array($backupQualityCard ?? null)
|
||||
? $backupQualityCard
|
||||
: (is_array($backup_quality_summary ?? null) ? $backup_quality_summary : []);
|
||||
$counts = is_array($backupQualityCard['counts'] ?? null) ? $backupQualityCard['counts'] : [];
|
||||
$compact = (bool) ($compact ?? false);
|
||||
|
||||
$statusBadgeClasses = static function (bool $available): string {
|
||||
return $available
|
||||
? 'border-warning-200 bg-warning-50 text-warning-700 dark:border-warning-700 dark:bg-warning-950/30 dark:text-warning-300'
|
||||
: 'border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300';
|
||||
$statusBadgeClasses = static function (string $tone): string {
|
||||
return match ($tone) {
|
||||
'success' => 'border-success-200 bg-success-50 text-success-700 dark:border-success-700 dark:bg-success-950/30 dark:text-success-300',
|
||||
'warning' => 'border-warning-200 bg-warning-50 text-warning-700 dark:border-warning-700 dark:bg-warning-950/30 dark:text-warning-300',
|
||||
default => 'border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300',
|
||||
};
|
||||
};
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<x-filament::section>
|
||||
<div data-testid="restore-run-backup-quality-summary" class="space-y-4">
|
||||
<div data-testid="restore-run-backup-quality-summary" class="{{ $compact ? 'space-y-3' : 'space-y-4' }}">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<h2 class="text-base font-semibold text-gray-950 dark:text-white">
|
||||
Backup quality summary
|
||||
</h2>
|
||||
<span class="inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-semibold {{ $statusBadgeClasses((bool) ($backupQualityCard['available'] ?? false)) }}">
|
||||
<span class="inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-semibold {{ $statusBadgeClasses((string) ($backupQualityCard['tone'] ?? (($backupQualityCard['available'] ?? false) ? 'warning' : 'gray'))) }}">
|
||||
{{ $backupQualityCard['status'] ?? 'Unavailable' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ $backupQualityCard['summary'] ?? 'Backup quality hints describe input strength only.' }}
|
||||
</p>
|
||||
@if ($compact)
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<p class="max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ $backupQualityCard['summary'] ?? 'Backup quality hints describe input strength only.' }}
|
||||
</p>
|
||||
|
||||
@if ($counts !== [])
|
||||
<dl class="grid gap-3 md:grid-cols-5">
|
||||
@foreach ($counts as $count)
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $count['label'] ?? 'Count' }}</dt>
|
||||
<dd class="mt-1 text-sm font-medium text-gray-950 dark:text-white">{{ $count['value'] ?? 0 }}</dd>
|
||||
@if ($counts !== [])
|
||||
<dl class="flex shrink-0 flex-wrap gap-2 lg:justify-end">
|
||||
@foreach ($counts as $count)
|
||||
<div class="rounded-full border border-gray-200 bg-gray-50 px-3 py-1.5 text-xs dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="inline font-semibold text-gray-500 dark:text-gray-400">{{ $count['label'] ?? 'Count' }}</dt>
|
||||
<dd class="inline font-medium text-gray-950 dark:text-white">{{ $count['value'] ?? 0 }}</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</dl>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<details class="rounded-lg border border-gray-200 bg-white px-4 py-3 text-sm shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer font-medium text-gray-950 dark:text-white">
|
||||
View quality caveat and detail
|
||||
</summary>
|
||||
|
||||
<div class="mt-3 space-y-3">
|
||||
<div class="rounded-lg border border-warning-200 bg-warning-50 px-4 py-3 text-warning-900 dark:border-warning-700 dark:bg-warning-950/30 dark:text-warning-100">
|
||||
{{ $backupQualityCard['positiveClaimBoundary'] ?? 'Input quality signals do not prove that execution is safe or that recovery is verified.' }}
|
||||
</div>
|
||||
@endforeach
|
||||
</dl>
|
||||
|
||||
<div class="text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $backupQualityCard['nextAction'] ?? 'Inspect item-level backup detail before continuing.' }}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
@else
|
||||
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ $backupQualityCard['summary'] ?? 'Backup quality hints describe input strength only.' }}
|
||||
</p>
|
||||
|
||||
@if ($counts !== [])
|
||||
<dl class="grid gap-3 md:grid-cols-5">
|
||||
@foreach ($counts as $count)
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $count['label'] ?? 'Count' }}</dt>
|
||||
<dd class="mt-1 text-sm font-medium text-gray-950 dark:text-white">{{ $count['value'] ?? 0 }}</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</dl>
|
||||
@endif
|
||||
|
||||
<div class="rounded-lg border border-warning-200 bg-warning-50 px-4 py-3 text-sm text-warning-900 dark:border-warning-700 dark:bg-warning-950/30 dark:text-warning-100">
|
||||
{{ $backupQualityCard['positiveClaimBoundary'] ?? 'Input quality signals do not prove that execution is safe or that recovery is verified.' }}
|
||||
</div>
|
||||
|
||||
<div class="text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $backupQualityCard['nextAction'] ?? 'Inspect item-level backup detail before continuing.' }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="rounded-lg border border-warning-200 bg-warning-50 px-4 py-3 text-sm text-warning-900 dark:border-warning-700 dark:bg-warning-950/30 dark:text-warning-100">
|
||||
{{ $backupQualityCard['positiveClaimBoundary'] ?? 'Input quality signals do not prove that execution is safe or that recovery is verified.' }}
|
||||
</div>
|
||||
|
||||
<div class="text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $backupQualityCard['nextAction'] ?? 'Inspect item-level backup detail before continuing.' }}
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</x-dynamic-component>
|
||||
|
||||
@ -5,19 +5,14 @@
|
||||
$validationSummary = $validationSummary !== []
|
||||
? $validationSummary
|
||||
: (is_array($validation_summary ?? null) ? $validation_summary : []);
|
||||
|
||||
$integritySpec = $validationSummary['integritySpec'] ?? null;
|
||||
|
||||
if (! $integritySpec instanceof \App\Support\Badges\BadgeSpec) {
|
||||
$integritySpec = \App\Support\Badges\BadgeSpec::unknown();
|
||||
}
|
||||
$wizardGate = is_array($wizardGate ?? null) ? $wizardGate : [];
|
||||
|
||||
$blocking = (int) ($validationSummary['blockingCount'] ?? 0);
|
||||
$warning = (int) ($validationSummary['warningCount'] ?? 0);
|
||||
$safe = (int) ($validationSummary['safeCount'] ?? 0);
|
||||
|
||||
$integritySummary = (string) ($validationSummary['integritySummary'] ?? 'Run checks for the current scope before real execution.');
|
||||
$nextActionLabel = (string) ($validationSummary['nextActionLabel'] ?? 'Run the safety checks again for the current scope.');
|
||||
$nextActionLabel = (string) ($wizardGate['required_action_label'] ?? ($validationSummary['nextActionLabel'] ?? 'Run the safety checks again for the current scope.'));
|
||||
|
||||
$startabilitySummary = (string) ($validationSummary['executionReadinessSummary'] ?? 'Execution prerequisites are unavailable.');
|
||||
$startabilityTone = (string) ($validationSummary['executionReadinessTone'] ?? ((bool) ($validationSummary['executionAllowed'] ?? false) ? 'success' : 'warning'));
|
||||
@ -43,129 +38,162 @@
|
||||
$resultsPresent = collect($groupedResults)
|
||||
->filter(fn ($bucket) => is_array($bucket) && $bucket !== [])
|
||||
->isNotEmpty();
|
||||
|
||||
$decisionTitle = match (true) {
|
||||
$providerCredentialBlocked => 'Validation blocked',
|
||||
! $resultsPresent => 'Validation required',
|
||||
$blocking > 0 => 'Validation blocked',
|
||||
$warning > 0 => 'Review validation warnings',
|
||||
default => 'Validation passed',
|
||||
};
|
||||
|
||||
$decisionToneClasses = match (true) {
|
||||
$providerCredentialBlocked || $blocking > 0 => 'border-danger-200 bg-danger-50 text-danger-700 dark:border-danger-700 dark:bg-danger-950/30 dark:text-danger-300',
|
||||
$warning > 0 || ! $resultsPresent => 'border-warning-200 bg-warning-50 text-warning-700 dark:border-warning-700 dark:bg-warning-950/30 dark:text-warning-300',
|
||||
default => 'border-success-200 bg-success-50 text-success-700 dark:border-success-700 dark:bg-success-950/30 dark:text-success-300',
|
||||
};
|
||||
|
||||
$safeResults = is_array($groupedResults['safe'] ?? null) ? $groupedResults['safe'] : [];
|
||||
$safeDetailsCount = $safe > 0 ? $safe : count($safeResults);
|
||||
$safeDetailsLabel = $safeDetailsCount === 1 ? 'View 1 safe check detail' : "View {$safeDetailsCount} safe check details";
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<div class="space-y-4">
|
||||
@if ($providerCredentialBlocked)
|
||||
<x-filament::section>
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Validation blocked
|
||||
<x-filament::section heading="Validation decision" description="Provider access must be repaired before restore checks can run.">
|
||||
<div class="rounded-lg border border-danger-200 bg-danger-50/70 p-4 dark:border-danger-700 dark:bg-danger-950/20">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="min-w-0 space-y-2">
|
||||
<span class="inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold {{ $decisionToneClasses }}">
|
||||
{{ $decisionTitle }}
|
||||
</span>
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Provider credentials are not available for this environment.
|
||||
</div>
|
||||
<p class="text-sm leading-6 text-gray-700 dark:text-gray-200">
|
||||
Restore checks cannot run until the provider connection is repaired.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if ($providerConnectionsUrl)
|
||||
<div class="shrink-0">
|
||||
<x-filament::button
|
||||
tag="a"
|
||||
size="sm"
|
||||
color="primary"
|
||||
:href="$providerConnectionsUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Review provider connection
|
||||
</x-filament::button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
Provider credentials are not available for this environment.
|
||||
</div>
|
||||
<div class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
Restore checks cannot run until the provider connection is repaired.
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@else
|
||||
<x-filament::section
|
||||
heading="Validation decision"
|
||||
:description="$ranAtLabel ? ('Last run: ' . $ranAtLabel) : 'Run checks for the current scope before preview.'"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div data-testid="restore-run-validation-decision-card" class="rounded-lg border border-gray-200 bg-gray-50 p-4 shadow-sm dark:border-gray-800 dark:bg-gray-950/40">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="min-w-0 space-y-2">
|
||||
<span class="inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold {{ $decisionToneClasses }}">
|
||||
{{ $decisionTitle }}
|
||||
</span>
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $integritySummary }}
|
||||
</div>
|
||||
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ $startabilitySummary }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid min-w-0 grid-cols-3 gap-2 sm:min-w-[24rem]">
|
||||
<div data-testid="restore-run-validation-stat-card" class="rounded-lg border border-gray-200 bg-white px-3 py-2 dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Blockers</div>
|
||||
<div class="mt-0.5 text-sm font-semibold text-gray-950 dark:text-white">{{ $blocking }}</div>
|
||||
</div>
|
||||
<div data-testid="restore-run-validation-stat-card" class="rounded-lg border border-gray-200 bg-white px-3 py-2 dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Warnings</div>
|
||||
<div class="mt-0.5 text-sm font-semibold text-gray-950 dark:text-white">{{ $warning }}</div>
|
||||
</div>
|
||||
<div data-testid="restore-run-validation-stat-card" class="rounded-lg border border-gray-200 bg-white px-3 py-2 dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Safe</div>
|
||||
<div class="mt-0.5 text-sm font-semibold text-gray-950 dark:text-white">{{ $safe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($providerConnectionsUrl)
|
||||
<div>
|
||||
<x-filament::button
|
||||
tag="a"
|
||||
size="sm"
|
||||
color="primary"
|
||||
:href="$providerConnectionsUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Review provider connection
|
||||
</x-filament::button>
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-950/40">
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
|
||||
Execution prerequisites
|
||||
</div>
|
||||
<div class="mt-1 flex flex-wrap gap-2">
|
||||
<x-filament::badge :color="$startabilityTone" size="sm">
|
||||
{{ $startabilityTone === 'success' ? 'Prerequisites available' : 'Execution unavailable' }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-950/40">
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
|
||||
Primary next step
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $nextActionLabel }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($invalidationReasons !== [])
|
||||
<div class="text-xs text-amber-800 dark:text-amber-200">
|
||||
Invalidated by: {{ implode(', ', array_map(static fn (string $reason): string => \Illuminate\Support\Str::replace('_', ' ', $reason), $invalidationReasons)) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
<x-filament::section
|
||||
heading="Safety checks"
|
||||
:description="$ranAtLabel ? ('Last run: ' . $ranAtLabel) : 'Checks tell you whether the current scope can be defended, not just whether it can start.'"
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-filament::badge :color="$integritySpec->color" :icon="$integritySpec->icon" size="sm">
|
||||
{{ $integritySpec->label }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge :color="$startabilityTone" size="sm">
|
||||
{{ $startabilityTone === 'success' ? 'Prerequisites healthy' : 'Execution blocked' }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3 text-sm text-slate-900 dark:border-white/10 dark:bg-white/5 dark:text-slate-100">
|
||||
<div class="font-medium">What validation proves</div>
|
||||
<div class="mt-1">{{ $integritySummary }}</div>
|
||||
<div class="mt-2 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
||||
Execution prerequisites
|
||||
@if (! $resultsPresent)
|
||||
<x-filament::section>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
No checks have been recorded for this scope yet.
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-slate-600 dark:text-slate-300">
|
||||
{{ $startabilitySummary }}
|
||||
</div>
|
||||
<div class="mt-2 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
||||
Primary next step
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-slate-600 dark:text-slate-300">
|
||||
{{ $nextActionLabel }}
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@else
|
||||
<div class="space-y-4">
|
||||
@foreach ([
|
||||
'blocking' => ['label' => 'Blockers', 'tone' => 'danger'],
|
||||
'warning' => ['label' => 'Warnings', 'tone' => 'warning'],
|
||||
] as $bucket => $bucketMeta)
|
||||
@php
|
||||
$bucketResults = is_array($groupedResults[$bucket] ?? null) ? $groupedResults[$bucket] : [];
|
||||
@endphp
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-filament::badge :color="$blocking > 0 ? 'danger' : 'gray'">
|
||||
{{ $blocking }} blockers
|
||||
</x-filament::badge>
|
||||
<x-filament::badge :color="$warning > 0 ? 'warning' : 'gray'">
|
||||
{{ $warning }} warnings
|
||||
</x-filament::badge>
|
||||
<x-filament::badge :color="$safe > 0 ? 'success' : 'gray'">
|
||||
{{ $safe }} safe
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
@if ($invalidationReasons !== [])
|
||||
<div class="text-xs text-amber-800 dark:text-amber-200">
|
||||
Invalidated by: {{ implode(', ', array_map(static fn (string $reason): string => \Illuminate\Support\Str::replace('_', ' ', $reason), $invalidationReasons)) }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
@if (! $resultsPresent)
|
||||
<x-filament::section>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
No checks have been recorded for this scope yet.
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@else
|
||||
<div class="space-y-5">
|
||||
@foreach ([
|
||||
'blocking' => ['label' => 'Blockers', 'tone' => 'danger'],
|
||||
'warning' => ['label' => 'Warnings', 'tone' => 'warning'],
|
||||
'safe' => ['label' => 'Safe checks', 'tone' => 'success'],
|
||||
] as $bucket => $bucketMeta)
|
||||
@php
|
||||
$bucketResults = is_array($groupedResults[$bucket] ?? null) ? $groupedResults[$bucket] : [];
|
||||
@endphp
|
||||
|
||||
@if ($bucketResults !== [])
|
||||
<x-filament::section>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $bucketMeta['label'] }}
|
||||
@if ($bucketResults !== [])
|
||||
<x-filament::section>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $bucketMeta['label'] }}
|
||||
</div>
|
||||
<x-filament::badge :color="$bucketMeta['tone']" size="sm">
|
||||
{{ count($bucketResults) }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
<x-filament::badge :color="$bucketMeta['tone']" size="sm">
|
||||
{{ count($bucketResults) }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
@foreach ($bucketResults as $result)
|
||||
@php
|
||||
$title = is_array($result) ? ($result['title'] ?? $result['code'] ?? 'Check') : 'Check';
|
||||
$message = is_array($result) ? ($result['message'] ?? null) : null;
|
||||
@endphp
|
||||
<div class="mt-4 space-y-3">
|
||||
@foreach ($bucketResults as $result)
|
||||
@php
|
||||
$title = is_array($result) ? ($result['title'] ?? $result['code'] ?? 'Check') : 'Check';
|
||||
$message = is_array($result) ? ($result['message'] ?? null) : null;
|
||||
@endphp
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-950/40">
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ is_string($title) ? $title : 'Check' }}
|
||||
@ -176,18 +204,43 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
<x-filament::badge :color="$bucketMeta['tone']" size="sm">
|
||||
{{ $bucketMeta['label'] }}
|
||||
</x-filament::badge>
|
||||
@if ($safeResults !== [])
|
||||
<details data-testid="restore-run-safe-checks-details" class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-950/40">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $safeDetailsLabel }}
|
||||
</summary>
|
||||
<div class="mt-4 space-y-3">
|
||||
@foreach ($safeResults as $result)
|
||||
@php
|
||||
$title = is_array($result) ? ($result['title'] ?? $result['code'] ?? 'Check') : 'Check';
|
||||
$message = is_array($result) ? ($result['message'] ?? null) : null;
|
||||
@endphp
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="space-y-1">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ is_string($title) ? $title : 'Check' }}
|
||||
</div>
|
||||
@if (is_string($message) && $message !== '')
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ $message }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</details>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
|
||||
@ -3,8 +3,12 @@
|
||||
|
||||
$decisionCard = is_array($decisionCard ?? null) ? $decisionCard : [];
|
||||
$processFlow = is_array($processFlow ?? null) ? $processFlow : [];
|
||||
$wizardGate = is_array($wizardGate ?? null) ? $wizardGate : [];
|
||||
|
||||
$statusTone = (string) ($decisionCard['tone'] ?? 'gray');
|
||||
$statusTone = (string) ($wizardGate['confirmation_state_tone'] ?? ($decisionCard['tone'] ?? 'gray'));
|
||||
$confirmationStateLabel = (string) ($wizardGate['confirmation_state_label'] ?? ($decisionCard['status'] ?? 'Unavailable'));
|
||||
$primaryCtaLabel = (string) ($wizardGate['primary_cta_label'] ?? 'Create preview-only run');
|
||||
$primaryNextStepLabel = (string) ($wizardGate['primary_next_step_label'] ?? ($wizardGate['required_action_label'] ?? ($decisionCard['nextAction'] ?? 'Review the current restore state.')));
|
||||
|
||||
$statusBadgeClasses = static function (string $tone): string {
|
||||
return match ($tone) {
|
||||
@ -17,24 +21,38 @@
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<x-filament::section heading="Confirmation summary" description="Execution only becomes available after high-friction confirmation. Proof remains unavailable until a run starts.">
|
||||
<x-filament::section heading="Confirmation summary" description="Preview-only and real execution states are separated before the run is created. Proof remains unavailable until a run starts.">
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $decisionCard['title'] ?? 'Restore Safety' }}
|
||||
</div>
|
||||
<span class="inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-semibold {{ $statusBadgeClasses($statusTone) }}">
|
||||
{{ $decisionCard['status'] ?? 'Unavailable' }}
|
||||
{{ $confirmationStateLabel }}
|
||||
</span>
|
||||
@if (filled($wizardGate['confirmation_mode_label'] ?? null))
|
||||
<span class="inline-flex items-center rounded-full border border-gray-200 bg-white px-2.5 py-1 text-xs font-semibold text-gray-700 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300">
|
||||
{{ $wizardGate['confirmation_mode_label'] }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">
|
||||
Primary next step
|
||||
Primary action
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $decisionCard['nextAction'] ?? 'Review the current restore state.' }}
|
||||
{{ $primaryCtaLabel }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">
|
||||
Next step
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $primaryNextStepLabel }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -43,7 +61,7 @@
|
||||
Next gate
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $processFlow['nextGate'] ?? 'Unavailable' }}
|
||||
{{ $wizardGate['next_gate_label'] ?? ($processFlow['nextGate'] ?? 'Unavailable') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -52,18 +70,18 @@
|
||||
Execution
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $processFlow['executionLabel'] ?? 'Unavailable' }}
|
||||
{{ $wizardGate['execution_label'] ?? ($processFlow['executionLabel'] ?? 'Unavailable') }}
|
||||
</div>
|
||||
@if (filled($processFlow['executionSummary'] ?? null))
|
||||
@if (filled($wizardGate['execution_summary'] ?? ($processFlow['executionSummary'] ?? null)))
|
||||
<div class="mt-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $processFlow['executionSummary'] }}
|
||||
{{ $wizardGate['execution_summary'] ?? $processFlow['executionSummary'] }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-warning-200 bg-warning-50 px-4 py-3 text-sm text-warning-900 dark:border-warning-700 dark:bg-warning-950/30 dark:text-warning-100">
|
||||
Confirmation does not claim recovery. Operation proof and post-run evidence remain unavailable until execution starts.
|
||||
Operation proof is unavailable before execution. Post-run evidence is unavailable before execution. Recovery is not verified until post-run evidence exists.
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
@ -4,9 +4,17 @@
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<div class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
<div class="rounded-lg border border-warning-200 bg-warning-50/80 px-4 py-3 text-sm leading-6 text-warning-900 dark:border-warning-700 dark:bg-warning-950/20 dark:text-warning-100">
|
||||
<div class="font-semibold">
|
||||
Assignment skipped
|
||||
</div>
|
||||
<div>
|
||||
This assignment will not be restored.
|
||||
</div>
|
||||
@if ($identityHtml)
|
||||
{!! $identityHtml !!}
|
||||
<div class="mt-2 text-warning-800 dark:text-warning-200">
|
||||
{!! $identityHtml !!}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
|
||||
@ -17,8 +17,17 @@
|
||||
$groupSyncOperationsUrl = $mappingResolver['groupSyncOperationsUrl'] ?? null;
|
||||
@endphp
|
||||
|
||||
<div data-testid="restore-run-mapping-resolver-summary" class="space-y-4 rounded-lg border border-gray-200 bg-gray-50/70 p-4 dark:border-gray-800 dark:bg-gray-900/40">
|
||||
<div data-testid="restore-run-mapping-resolver-summary" class="space-y-4 rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Target mapping requirements
|
||||
</h3>
|
||||
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
Map each source assignment to a current-environment group, use a manual fallback when the cached group is missing, or skip the assignment intentionally.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="inline-flex items-center rounded-full border border-primary-200 bg-primary-50 px-3 py-1 text-xs font-semibold text-primary-700 dark:border-primary-700 dark:bg-primary-950/30 dark:text-primary-300">
|
||||
{{ $resolvedCount }} of {{ $totalCount }} mappings resolved
|
||||
@ -56,6 +65,7 @@
|
||||
<x-filament::button
|
||||
size="sm"
|
||||
color="gray"
|
||||
icon="heroicon-o-chevron-up"
|
||||
type="button"
|
||||
data-testid="restore-run-hide-mapping-details"
|
||||
x-on:click="
|
||||
@ -69,7 +79,7 @@
|
||||
@if (filled($groupSyncUrl))
|
||||
<x-filament::button
|
||||
size="sm"
|
||||
color="warning"
|
||||
color="gray"
|
||||
tag="a"
|
||||
:href="$groupSyncUrl"
|
||||
target="_blank"
|
||||
|
||||
@ -5,6 +5,13 @@
|
||||
$previewState = $previewState !== []
|
||||
? $previewState
|
||||
: (is_array($preview_summary ?? null) ? $preview_summary : []);
|
||||
$validationState = is_array($validationSummary ?? null) ? $validationSummary : [];
|
||||
$processFlowState = is_array($processFlow ?? null) ? $processFlow : [];
|
||||
$wizardGateState = is_array($wizardGate ?? null) ? $wizardGate : [];
|
||||
$processFlowSteps = is_array($processFlowState['steps'] ?? null) ? $processFlowState['steps'] : [];
|
||||
$proofState = is_array($proofAside ?? null) ? $proofAside : [];
|
||||
$proofItems = is_array($proofState['items'] ?? null) ? $proofState['items'] : [];
|
||||
$diagnosticsState = is_array($diagnosticsDisclosure ?? null) ? $diagnosticsDisclosure : [];
|
||||
|
||||
$integritySpec = $previewState['integritySpec'] ?? null;
|
||||
|
||||
@ -25,11 +32,16 @@
|
||||
|
||||
$integritySummary = (string) ($previewState['integritySummary'] ?? 'Generate a preview before real execution.');
|
||||
$scopeLabel = (string) ($previewState['scopeLabel'] ?? 'Restore scope');
|
||||
$primaryNextActionLabel = (string) ($previewState['primaryNextActionLabel'] ?? 'Review the current scope and safety evidence.');
|
||||
$primaryNextActionLabel = (string) ($wizardGateState['required_action_label'] ?? ($previewState['primaryNextActionLabel'] ?? 'Review the current scope and safety evidence.'));
|
||||
|
||||
$summary = is_array($previewState['previewSummary'] ?? null) ? $previewState['previewSummary'] : [];
|
||||
$needsAttentionDiffs = is_array($previewState['needsAttentionDiffs'] ?? null) ? $previewState['needsAttentionDiffs'] : [];
|
||||
$unchangedDiffs = is_array($previewState['unchangedDiffs'] ?? null) ? $previewState['unchangedDiffs'] : [];
|
||||
$changedDiffs = $needsAttentionDiffs;
|
||||
$allReviewedDiffs = collect([...$changedDiffs, ...$unchangedDiffs])
|
||||
->filter(fn ($entry): bool => is_array($entry))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$policiesTotal = (int) ($summary['policies_total'] ?? 0);
|
||||
$policiesChanged = (int) ($summary['policies_changed'] ?? 0);
|
||||
@ -37,7 +49,41 @@
|
||||
$scopeTagsChanged = (int) ($summary['scope_tags_changed'] ?? 0);
|
||||
$diffsOmitted = (int) ($summary['diffs_omitted'] ?? 0);
|
||||
|
||||
$reviewedCount = count($needsAttentionDiffs) + count($unchangedDiffs);
|
||||
$reviewedCount = max($policiesTotal, count($allReviewedDiffs));
|
||||
$changedCount = count($changedDiffs);
|
||||
$reportedChangedCount = max($policiesChanged, $changedCount);
|
||||
$unchangedCount = count($unchangedDiffs);
|
||||
$requiresReviewCount = $changedCount + $diffsOmitted;
|
||||
$blockingCount = (int) ($validationState['blockingCount'] ?? 0);
|
||||
$warningCount = (int) ($validationState['warningCount'] ?? 0);
|
||||
$nextGate = (string) ($wizardGateState['next_gate_label'] ?? ($previewState['nextGateLabel'] ?? ($processFlowState['nextGate'] ?? 'Unavailable')));
|
||||
|
||||
$gateBadgeTone = static function (string $status): string {
|
||||
return match ($status) {
|
||||
'complete' => 'success',
|
||||
'required' => 'warning',
|
||||
'blocked' => 'danger',
|
||||
default => 'gray',
|
||||
};
|
||||
};
|
||||
|
||||
$gateBadgeLabel = static function (string $status): string {
|
||||
return match ($status) {
|
||||
'complete' => 'Complete',
|
||||
'required' => 'Required',
|
||||
'blocked' => 'Blocked',
|
||||
default => 'Unavailable',
|
||||
};
|
||||
};
|
||||
|
||||
$proofBadgeClasses = static function (string $tone): string {
|
||||
return match ($tone) {
|
||||
'success' => 'border-success-200 bg-success-50 text-success-700 dark:border-success-700 dark:bg-success-950/30 dark:text-success-300',
|
||||
'warning' => 'border-warning-200 bg-warning-50 text-warning-700 dark:border-warning-700 dark:bg-gray-950/70 dark:text-warning-300',
|
||||
'danger' => 'border-danger-200 bg-danger-50 text-danger-700 dark:border-danger-700 dark:bg-danger-950/30 dark:text-danger-300',
|
||||
default => 'border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300',
|
||||
};
|
||||
};
|
||||
|
||||
$policyLabel = static function (array $entry): string {
|
||||
$displayName = $entry['display_name'] ?? $entry['displayName'] ?? null;
|
||||
@ -62,17 +108,117 @@
|
||||
$action = $entry['action'] ?? null;
|
||||
|
||||
return match ((string) $action) {
|
||||
'create' => 'Create',
|
||||
'delete' => 'Delete',
|
||||
'update' => 'Update',
|
||||
default => 'Review',
|
||||
'create' => 'Create new',
|
||||
'delete' => 'Delete existing',
|
||||
'update' => 'Update existing',
|
||||
'noop', 'no_op', 'no-op' => 'No restore change',
|
||||
'blocked' => 'Blocked',
|
||||
default => 'Review required',
|
||||
};
|
||||
};
|
||||
|
||||
$policyDiffSummary = static function (array $entry): array {
|
||||
$diff = is_array($entry['diff'] ?? null) ? $entry['diff'] : [];
|
||||
$summary = is_array($diff['summary'] ?? null) ? $diff['summary'] : [];
|
||||
$added = (int) ($summary['added'] ?? 0);
|
||||
$removed = (int) ($summary['removed'] ?? 0);
|
||||
$changed = (int) ($summary['changed'] ?? 0);
|
||||
if ((bool) ($entry['diff_omitted'] ?? false)) {
|
||||
return [
|
||||
'label' => 'Detailed diff omitted',
|
||||
'key' => 'omitted',
|
||||
'tone' => 'warning',
|
||||
'parts' => [],
|
||||
];
|
||||
}
|
||||
|
||||
if ($added === 0 && $removed === 0 && $changed === 0) {
|
||||
return [
|
||||
'label' => 'No policy changes',
|
||||
'key' => 'unchanged',
|
||||
'tone' => 'success',
|
||||
'parts' => [],
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'label' => "{$added} added {$removed} removed {$changed} changed",
|
||||
'key' => 'summary',
|
||||
'tone' => 'gray',
|
||||
'parts' => [
|
||||
['key' => 'added', 'label' => "{$added} added", 'tone' => 'success'],
|
||||
['key' => 'removed', 'label' => "{$removed} removed", 'tone' => 'danger'],
|
||||
['key' => 'changed', 'label' => "{$changed} changed", 'tone' => 'warning'],
|
||||
],
|
||||
];
|
||||
};
|
||||
|
||||
$previewResultLabel = static function (array $entry, bool $isUnchanged) use ($policyActionLabel): string {
|
||||
if ($isUnchanged) {
|
||||
return 'No restore change';
|
||||
}
|
||||
|
||||
return $policyActionLabel($entry);
|
||||
};
|
||||
|
||||
$reviewReasonLabel = static function (array $entry): string {
|
||||
$reason = $entry['review_reason'] ?? null;
|
||||
|
||||
return is_string($reason) && trim($reason) !== ''
|
||||
? $reason
|
||||
: 'Review preview evidence before confirmation.';
|
||||
};
|
||||
|
||||
$reviewActionLabel = static function (array $entry): string {
|
||||
$action = $entry['review_action_label'] ?? null;
|
||||
|
||||
return is_string($action) && trim($action) !== ''
|
||||
? $action
|
||||
: 'Review item';
|
||||
};
|
||||
|
||||
$signalLabel = static fn (bool $changed): string => $changed ? 'Changed' : 'No change';
|
||||
|
||||
$signalPillClasses = static function (bool $changed): string {
|
||||
return $changed
|
||||
? 'border-warning-200 bg-warning-50 text-warning-700 dark:border-warning-700 dark:bg-gray-950/70 dark:text-warning-300'
|
||||
: 'border-success-200 bg-success-50 text-success-700 dark:border-success-700 dark:bg-success-950/30 dark:text-success-300';
|
||||
};
|
||||
|
||||
$diffPillClasses = static function (string $tone): string {
|
||||
return match ($tone) {
|
||||
'success' => 'border-success-200 bg-success-50 text-success-700 dark:border-success-700 dark:bg-success-950/30 dark:text-success-300',
|
||||
'danger' => 'border-danger-200 bg-danger-50 text-danger-700 dark:border-danger-700 dark:bg-danger-950/30 dark:text-danger-300',
|
||||
'warning' => 'border-warning-200 bg-warning-50 text-warning-700 dark:border-warning-700 dark:bg-gray-950/70 dark:text-warning-300',
|
||||
default => 'border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300',
|
||||
};
|
||||
};
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-12">
|
||||
<div class="space-y-4 lg:col-span-8">
|
||||
<style>
|
||||
@media (min-width: 768px) {
|
||||
[data-testid="restore-run-preview-review-cards"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
[data-testid="restore-run-preview-review-table"] {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
[data-testid="restore-run-preview-review-cards"] {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
[data-testid="restore-run-preview-review-table"] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="space-y-4">
|
||||
<x-filament::section
|
||||
heading="Preview"
|
||||
:description="$generatedAtLabel ? ('Generated: ' . $generatedAtLabel) : 'Preview answers what would change for the current scope.'"
|
||||
@ -87,18 +233,18 @@
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3 text-sm text-slate-900 dark:border-white/10 dark:bg-white/5 dark:text-slate-100">
|
||||
<div class="font-medium">What the preview proves</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 text-sm text-gray-900 dark:border-gray-800 dark:bg-gray-950/40 dark:text-gray-100">
|
||||
<div class="font-medium">Preview evidence</div>
|
||||
<div class="mt-1">{{ $integritySummary }}</div>
|
||||
<div class="mt-2 text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
||||
<div class="mt-2 text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
|
||||
Primary next step
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-slate-600 dark:text-slate-300">
|
||||
<div class="mt-1 text-xs text-gray-600 dark:text-gray-300">
|
||||
{{ $primaryNextActionLabel }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Policies reviewed</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
@ -108,7 +254,19 @@
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Policies changed</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $policiesChanged }}
|
||||
{{ $reportedChangedCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Policies unchanged</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $unchangedCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Requires review</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $requiresReviewCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
@ -123,85 +281,366 @@
|
||||
{{ $scopeTagsChanged }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Validation blockers</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $blockingCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Validation warnings</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $warningCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Next gate</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $nextGate }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::section heading="Policy change preview">
|
||||
<x-filament::section heading="Preview details">
|
||||
<div class="space-y-4">
|
||||
@if ($policiesTotal === 0)
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
No policies are included in this preview yet.
|
||||
</div>
|
||||
@elseif ($needsAttentionDiffs === [] && $unchangedDiffs !== [])
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-800 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-200">
|
||||
No policy changes
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($diffsOmitted > 0)
|
||||
<div class="rounded-lg border border-warning-200 bg-warning-50/80 px-4 py-3 text-sm leading-6 text-warning-800 dark:border-warning-700 dark:bg-warning-950/20 dark:text-warning-200">
|
||||
{{ $diffsOmitted }} {{ \Illuminate\Support\Str::plural('policy diff', $diffsOmitted) }} omitted due to preview limits. Narrow scope to review more.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($needsAttentionDiffs !== [])
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Needs attention
|
||||
</div>
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<table class="min-w-full divide-y divide-gray-200 text-sm dark:divide-gray-800">
|
||||
<x-filament::badge :color="$requiresReviewCount > 0 ? 'warning' : 'gray'" size="sm">
|
||||
{{ $requiresReviewCount }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
@if ($diffsOmitted > 0)
|
||||
<div class="rounded-lg border border-warning-200 bg-warning-50/80 px-4 py-3 text-sm leading-6 text-warning-800 dark:border-warning-700 dark:bg-gray-950/70 dark:text-warning-200">
|
||||
{{ $diffsOmitted }} {{ \Illuminate\Support\Str::plural('policy diff', $diffsOmitted) }} omitted due to preview limits. Narrow scope to review more.
|
||||
</div>
|
||||
@elseif ($requiresReviewCount === 0)
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300">
|
||||
No preview items currently require extra review.
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-lg border border-warning-200 bg-warning-50/80 px-4 py-3 text-sm leading-6 text-warning-800 dark:border-warning-700 dark:bg-gray-950/70 dark:text-warning-200">
|
||||
{{ $requiresReviewCount }} {{ \Illuminate\Support\Str::plural('policy', $requiresReviewCount) }} {{ $requiresReviewCount === 1 ? 'requires' : 'require' }} review. Use the Review reason column to inspect policy diff, assignments, and scope-tag evidence before confirmation.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($changedDiffs !== [])
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Changes detected
|
||||
</div>
|
||||
<x-filament::badge color="warning" size="sm">
|
||||
{{ $changedCount }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
<div data-testid="restore-run-preview-review-cards" data-preview-group="changed" class="space-y-3 md:hidden">
|
||||
@foreach ($changedDiffs as $entry)
|
||||
@php
|
||||
$entry = is_array($entry) ? $entry : [];
|
||||
$diffSummary = $policyDiffSummary($entry);
|
||||
$assignmentsHaveChanges = (bool) ($entry['assignments_changed'] ?? false);
|
||||
$scopeTagsHaveChanges = (bool) ($entry['scope_tags_changed'] ?? false);
|
||||
$reviewReason = $reviewReasonLabel($entry);
|
||||
$reviewAction = $reviewActionLabel($entry);
|
||||
@endphp
|
||||
<article data-testid="restore-run-preview-review-card" class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Policy</div>
|
||||
<div class="mt-1 text-sm font-semibold leading-5 text-gray-950 dark:text-white">
|
||||
{{ $policyLabel($entry) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="shrink-0 sm:text-right">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Restore action</div>
|
||||
<div class="mt-1 inline-flex items-center rounded-md border border-gray-200 bg-gray-50 px-2 py-1 text-sm font-medium text-gray-700 dark:border-gray-700 dark:bg-gray-950/50 dark:text-gray-200">
|
||||
{{ $policyActionLabel($entry) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Policy diff</div>
|
||||
<div class="mt-2 flex flex-wrap items-center gap-1.5">
|
||||
@if (($diffSummary['parts'] ?? []) !== [])
|
||||
@foreach ($diffSummary['parts'] as $part)
|
||||
<span data-diff-part="{{ $part['key'] ?? 'part' }}" class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $diffPillClasses((string) ($part['tone'] ?? 'gray')) }}">
|
||||
{{ $part['label'] ?? '' }}
|
||||
</span>
|
||||
@endforeach
|
||||
@else
|
||||
<span data-diff-part="{{ $diffSummary['key'] ?? 'summary' }}" class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $diffPillClasses((string) $diffSummary['tone']) }}">
|
||||
{{ $diffSummary['label'] }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="mt-3 grid gap-2 sm:grid-cols-2">
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Assignments</dt>
|
||||
<dd class="mt-1">
|
||||
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $signalPillClasses($assignmentsHaveChanges) }}">
|
||||
{{ $signalLabel($assignmentsHaveChanges) }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Scope tags</dt>
|
||||
<dd class="mt-1">
|
||||
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $signalPillClasses($scopeTagsHaveChanges) }}">
|
||||
{{ $signalLabel($scopeTagsHaveChanges) }}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Review reason</dt>
|
||||
<dd class="mt-1 text-sm leading-5 text-gray-700 dark:text-gray-200">{{ $reviewReason }}</dd>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Action</dt>
|
||||
<dd class="mt-1 text-sm font-semibold leading-5 text-gray-900 dark:text-gray-100">{{ $reviewAction }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div data-testid="restore-run-preview-review-table" data-preview-group="changed" class="hidden overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900/80 md:block">
|
||||
<table class="min-w-[72rem] divide-y divide-gray-200 text-sm dark:divide-gray-800">
|
||||
<thead class="bg-gray-50 dark:bg-gray-950/40">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Policy</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Restore action</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Policy diff</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Assignments</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Scope tags</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Review reason</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
@foreach ($needsAttentionDiffs as $entry)
|
||||
@foreach ($changedDiffs as $entry)
|
||||
@php
|
||||
$entry = is_array($entry) ? $entry : [];
|
||||
$diffSummary = $policyDiffSummary($entry);
|
||||
$assignmentsHaveChanges = (bool) ($entry['assignments_changed'] ?? false);
|
||||
$scopeTagsHaveChanges = (bool) ($entry['scope_tags_changed'] ?? false);
|
||||
$reviewReason = $reviewReasonLabel($entry);
|
||||
$reviewAction = $reviewActionLabel($entry);
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium text-gray-900 dark:text-gray-100">
|
||||
<td class="px-4 py-3 align-top font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $policyLabel($entry) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-600 dark:text-gray-300">
|
||||
<td class="px-4 py-3 align-top text-gray-600 dark:text-gray-300">
|
||||
{{ $policyActionLabel($entry) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
@if (($diffSummary['parts'] ?? []) !== [])
|
||||
@foreach ($diffSummary['parts'] as $part)
|
||||
<span data-diff-part="{{ $part['key'] ?? 'part' }}" class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $diffPillClasses((string) ($part['tone'] ?? 'gray')) }}">
|
||||
{{ $part['label'] ?? '' }}
|
||||
</span>
|
||||
@endforeach
|
||||
@else
|
||||
<span data-diff-part="{{ $diffSummary['key'] ?? 'summary' }}" class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $diffPillClasses((string) $diffSummary['tone']) }}">
|
||||
{{ $diffSummary['label'] }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top">
|
||||
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $signalPillClasses($assignmentsHaveChanges) }}">
|
||||
{{ $signalLabel($assignmentsHaveChanges) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top">
|
||||
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $signalPillClasses($scopeTagsHaveChanges) }}">
|
||||
{{ $signalLabel($scopeTagsHaveChanges) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top text-gray-600 dark:text-gray-300">
|
||||
{{ $reviewReason }}
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $reviewAction }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@elseif ($policiesTotal > 0)
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Changes detected
|
||||
</div>
|
||||
<x-filament::badge :color="$reportedChangedCount > 0 ? 'warning' : 'gray'" size="sm">
|
||||
{{ $reportedChangedCount }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300">
|
||||
{{ $reportedChangedCount > 0 ? 'Changed policy rows were omitted by preview limits. Narrow scope to inspect individual policies.' : 'No policy changes' }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($unchangedDiffs !== [])
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Unchanged
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
No changes detected
|
||||
</div>
|
||||
<x-filament::badge color="success" size="sm">
|
||||
{{ $unchangedCount }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<table class="min-w-full divide-y divide-gray-200 text-sm dark:divide-gray-800">
|
||||
<div data-testid="restore-run-preview-review-cards" data-preview-group="unchanged" class="space-y-3 md:hidden">
|
||||
@foreach ($unchangedDiffs as $entry)
|
||||
@php
|
||||
$entry = is_array($entry) ? $entry : [];
|
||||
$diffSummary = $policyDiffSummary($entry);
|
||||
$reviewReason = $reviewReasonLabel($entry);
|
||||
$reviewAction = $reviewActionLabel($entry);
|
||||
@endphp
|
||||
<article data-testid="restore-run-preview-review-card" class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Policy</div>
|
||||
<div class="mt-1 text-sm font-semibold leading-5 text-gray-950 dark:text-white">
|
||||
{{ $policyLabel($entry) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="shrink-0 sm:text-right">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Preview result</div>
|
||||
<div class="mt-1 inline-flex items-center rounded-md border border-gray-200 bg-gray-50 px-2 py-1 text-sm font-medium text-gray-700 dark:border-gray-700 dark:bg-gray-950/50 dark:text-gray-200">
|
||||
No restore change
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Policy diff</div>
|
||||
<div class="mt-2 flex flex-wrap items-center gap-1.5">
|
||||
@if (($diffSummary['parts'] ?? []) !== [])
|
||||
@foreach ($diffSummary['parts'] as $part)
|
||||
<span data-diff-part="{{ $part['key'] ?? 'part' }}" class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $diffPillClasses((string) ($part['tone'] ?? 'gray')) }}">
|
||||
{{ $part['label'] ?? '' }}
|
||||
</span>
|
||||
@endforeach
|
||||
@else
|
||||
<span data-diff-part="{{ $diffSummary['key'] ?? 'summary' }}" class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $diffPillClasses((string) $diffSummary['tone']) }}">
|
||||
{{ $diffSummary['label'] }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dl class="mt-3 grid gap-2 sm:grid-cols-2">
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Assignments</dt>
|
||||
<dd class="mt-1">
|
||||
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $signalPillClasses(false) }}">
|
||||
No change
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Scope tags</dt>
|
||||
<dd class="mt-1">
|
||||
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $signalPillClasses(false) }}">
|
||||
No change
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Review reason</dt>
|
||||
<dd class="mt-1 text-sm leading-5 text-gray-700 dark:text-gray-200">{{ $reviewReason }}</dd>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Action</dt>
|
||||
<dd class="mt-1 text-sm font-semibold leading-5 text-gray-900 dark:text-gray-100">{{ $reviewAction }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div data-testid="restore-run-preview-review-table" data-preview-group="unchanged" class="hidden overflow-x-auto rounded-lg border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900/80 md:block">
|
||||
<table class="min-w-[72rem] divide-y divide-gray-200 text-sm dark:divide-gray-800">
|
||||
<thead class="bg-gray-50 dark:bg-gray-950/40">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Policy</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Result</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Preview result</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Policy diff</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Assignments</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Scope tags</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Review reason</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
@foreach ($unchangedDiffs as $entry)
|
||||
@php
|
||||
$entry = is_array($entry) ? $entry : [];
|
||||
$diffSummary = $policyDiffSummary($entry);
|
||||
$reviewReason = $reviewReasonLabel($entry);
|
||||
$reviewAction = $reviewActionLabel($entry);
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium text-gray-900 dark:text-gray-100">
|
||||
<td class="px-4 py-3 align-top font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $policyLabel($entry) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-600 dark:text-gray-300">
|
||||
No policy changes
|
||||
<td class="px-4 py-3 align-top text-gray-600 dark:text-gray-300">
|
||||
No restore change
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
@if (($diffSummary['parts'] ?? []) !== [])
|
||||
@foreach ($diffSummary['parts'] as $part)
|
||||
<span data-diff-part="{{ $part['key'] ?? 'part' }}" class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $diffPillClasses((string) ($part['tone'] ?? 'gray')) }}">
|
||||
{{ $part['label'] ?? '' }}
|
||||
</span>
|
||||
@endforeach
|
||||
@else
|
||||
<span data-diff-part="{{ $diffSummary['key'] ?? 'summary' }}" class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $diffPillClasses((string) $diffSummary['tone']) }}">
|
||||
{{ $diffSummary['label'] }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top">
|
||||
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $signalPillClasses(false) }}">
|
||||
No change
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top">
|
||||
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $signalPillClasses(false) }}">
|
||||
No change
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top text-gray-600 dark:text-gray-300">
|
||||
{{ $reviewReason }}
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $reviewAction }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@ -209,20 +648,288 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@elseif ($policiesTotal > 0)
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
No changes detected
|
||||
</div>
|
||||
<x-filament::badge color="gray" size="sm">
|
||||
0
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300">
|
||||
No unchanged policies are included in this preview.
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($allReviewedDiffs !== [])
|
||||
<details class="rounded-lg border border-gray-200 bg-white px-4 py-3 text-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer font-semibold text-gray-950 dark:text-white">
|
||||
All reviewed items
|
||||
</summary>
|
||||
<div data-testid="restore-run-preview-review-cards" data-preview-group="all" class="mt-3 space-y-3 md:hidden">
|
||||
@foreach ($allReviewedDiffs as $entry)
|
||||
@php
|
||||
$entry = is_array($entry) ? $entry : [];
|
||||
$isUnchanged = in_array($entry, $unchangedDiffs, true);
|
||||
$diffSummary = $policyDiffSummary($entry);
|
||||
$assignmentsHaveChanges = (bool) ($entry['assignments_changed'] ?? false);
|
||||
$scopeTagsHaveChanges = (bool) ($entry['scope_tags_changed'] ?? false);
|
||||
$reviewReason = $reviewReasonLabel($entry);
|
||||
$reviewAction = $reviewActionLabel($entry);
|
||||
@endphp
|
||||
<article data-testid="restore-run-preview-review-card" class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Policy</div>
|
||||
<div class="mt-1 font-semibold leading-5 text-gray-950 dark:text-white">
|
||||
{{ $policyLabel($entry) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="shrink-0 sm:text-right">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Preview result</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
{{ $previewResultLabel($entry, $isUnchanged) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap items-center gap-1.5">
|
||||
@if (($diffSummary['parts'] ?? []) !== [])
|
||||
@foreach ($diffSummary['parts'] as $part)
|
||||
<span data-diff-part="{{ $part['key'] ?? 'part' }}" class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $diffPillClasses((string) ($part['tone'] ?? 'gray')) }}">
|
||||
{{ $part['label'] ?? '' }}
|
||||
</span>
|
||||
@endforeach
|
||||
@else
|
||||
<span data-diff-part="{{ $diffSummary['key'] ?? 'summary' }}" class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $diffPillClasses((string) $diffSummary['tone']) }}">
|
||||
{{ $diffSummary['label'] }}
|
||||
</span>
|
||||
@endif
|
||||
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $signalPillClasses($assignmentsHaveChanges) }}">
|
||||
Assignments: {{ $signalLabel($assignmentsHaveChanges) }}
|
||||
</span>
|
||||
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $signalPillClasses($scopeTagsHaveChanges) }}">
|
||||
Scope tags: {{ $signalLabel($scopeTagsHaveChanges) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-3 grid gap-2 sm:grid-cols-2">
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-3 py-2 dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Review reason</div>
|
||||
<div class="mt-1 text-sm leading-5 text-gray-700 dark:text-gray-200">{{ $reviewReason }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-3 py-2 dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Action</div>
|
||||
<div class="mt-1 text-sm font-semibold leading-5 text-gray-900 dark:text-gray-100">{{ $reviewAction }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div data-testid="restore-run-preview-review-table" data-preview-group="all" class="mt-3 hidden overflow-x-auto md:block">
|
||||
<table class="min-w-[72rem] divide-y divide-gray-200 text-sm dark:divide-gray-800">
|
||||
<thead class="bg-gray-50 dark:bg-gray-950/40">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Policy</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Preview result</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Policy diff</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Assignments</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Scope tags</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Review reason</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
@foreach ($allReviewedDiffs as $entry)
|
||||
@php
|
||||
$entry = is_array($entry) ? $entry : [];
|
||||
$isUnchanged = in_array($entry, $unchangedDiffs, true);
|
||||
$diffSummary = $policyDiffSummary($entry);
|
||||
$assignmentsHaveChanges = (bool) ($entry['assignments_changed'] ?? false);
|
||||
$scopeTagsHaveChanges = (bool) ($entry['scope_tags_changed'] ?? false);
|
||||
$reviewReason = $reviewReasonLabel($entry);
|
||||
$reviewAction = $reviewActionLabel($entry);
|
||||
@endphp
|
||||
<tr>
|
||||
<td class="px-4 py-3 align-top font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $policyLabel($entry) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top text-gray-600 dark:text-gray-300">
|
||||
{{ $previewResultLabel($entry, $isUnchanged) }}
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
@if (($diffSummary['parts'] ?? []) !== [])
|
||||
@foreach ($diffSummary['parts'] as $part)
|
||||
<span data-diff-part="{{ $part['key'] ?? 'part' }}" class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $diffPillClasses((string) ($part['tone'] ?? 'gray')) }}">
|
||||
{{ $part['label'] ?? '' }}
|
||||
</span>
|
||||
@endforeach
|
||||
@else
|
||||
<span data-diff-part="{{ $diffSummary['key'] ?? 'summary' }}" class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $diffPillClasses((string) $diffSummary['tone']) }}">
|
||||
{{ $diffSummary['label'] }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top">
|
||||
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $signalPillClasses($assignmentsHaveChanges) }}">
|
||||
{{ $signalLabel($assignmentsHaveChanges) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top">
|
||||
<span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $signalPillClasses($scopeTagsHaveChanges) }}">
|
||||
{{ $signalLabel($scopeTagsHaveChanges) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top text-gray-600 dark:text-gray-300">
|
||||
{{ $reviewReason }}
|
||||
</td>
|
||||
<td class="px-4 py-3 align-top font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $reviewAction }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
@elseif ($policiesTotal > 0)
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
All reviewed items
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300">
|
||||
The preview summary has no item rows to show.
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 lg:col-span-4">
|
||||
@include('filament.forms.components.partials.restore-run-process-flow-panel', [
|
||||
'processFlow' => $processFlow ?? [],
|
||||
])
|
||||
<x-filament::section>
|
||||
<div data-testid="restore-run-preview-evidence" class="space-y-4">
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-base font-semibold text-gray-950 dark:text-white">
|
||||
Preview evidence
|
||||
</h2>
|
||||
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
Gate status and restore proof remain available after preview without competing with preview review.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@include('filament.forms.components.partials.restore-run-proof-panel', [
|
||||
'proofAside' => $proofAside ?? [],
|
||||
'diagnosticsDisclosure' => $diagnosticsDisclosure ?? [],
|
||||
])
|
||||
</div>
|
||||
<dl class="grid gap-2 sm:grid-cols-3 lg:min-w-[34rem]">
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">Gates</dt>
|
||||
<dd class="mt-0.5 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $processFlowState['gatesComplete'] ?? 0 }}/{{ $processFlowState['gatesTotal'] ?? count($processFlowSteps) }} complete
|
||||
</dd>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">Next gate</dt>
|
||||
<dd class="mt-0.5 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $nextGate }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">Execution</dt>
|
||||
<dd class="mt-0.5 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $wizardGateState['execution_label'] ?? ($processFlowState['executionLabel'] ?? 'Unavailable') }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<details data-testid="restore-run-preview-evidence-details" class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
View safety gates and restore proof
|
||||
</summary>
|
||||
|
||||
<div class="mt-4 grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(18rem,0.8fr)]">
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Restore safety gates
|
||||
</div>
|
||||
|
||||
<ol class="space-y-2" aria-label="Restore safety gates">
|
||||
@foreach ($processFlowSteps as $step)
|
||||
@php
|
||||
$status = (string) ($step['status'] ?? 'unavailable');
|
||||
@endphp
|
||||
|
||||
<li
|
||||
data-testid="restore-run-process-flow-step"
|
||||
data-step-label="{{ $step['label'] ?? 'Gate' }}"
|
||||
data-step-status="{{ $status }}"
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-800 dark:bg-gray-950/50"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-gray-200 bg-white text-xs font-semibold text-gray-600 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300">
|
||||
{{ $step['step'] ?? '•' }}
|
||||
</span>
|
||||
<span class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $step['label'] ?? 'Gate' }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs leading-5 text-gray-600 dark:text-gray-300">
|
||||
{{ $step['summary'] ?? '' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<x-filament::badge :color="$gateBadgeTone($status)" size="sm">
|
||||
{{ $gateBadgeLabel($status) }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<aside class="space-y-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $proofState['title'] ?? 'Restore Proof' }}
|
||||
</div>
|
||||
|
||||
@foreach ($proofItems as $item)
|
||||
<div
|
||||
data-testid="restore-run-proof-item"
|
||||
data-proof-label="{{ $item['label'] ?? 'Proof item' }}"
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-800 dark:bg-gray-950/50"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $item['label'] ?? 'Proof item' }}
|
||||
</div>
|
||||
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $item['description'] ?? '' }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="inline-flex shrink-0 items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $proofBadgeClasses((string) ($item['tone'] ?? 'gray')) }}">
|
||||
{{ $item['value'] ?? 'Unavailable' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<details data-testid="restore-run-diagnostics-disclosure" class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $diagnosticsState['label'] ?? 'Diagnostics - Collapsed' }}
|
||||
</summary>
|
||||
<p class="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ $diagnosticsState['summary'] ?? 'Diagnostics remain closed by default.' }}
|
||||
</p>
|
||||
</details>
|
||||
</aside>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
|
||||
@ -12,6 +12,95 @@
|
||||
};
|
||||
@endphp
|
||||
|
||||
@once
|
||||
<style>
|
||||
.restore-run-create-wizard .fi-sc-wizard-header {
|
||||
background: rgb(249 250 251) !important;
|
||||
border: 1px solid rgb(229 231 235);
|
||||
border-radius: 0.75rem;
|
||||
display: flex !important;
|
||||
flex-wrap: nowrap !important;
|
||||
gap: 0.5rem;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 0.375rem;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.dark .restore-run-create-wizard .fi-sc-wizard-header {
|
||||
background: rgb(17 24 39) !important;
|
||||
border-color: rgb(55 65 81);
|
||||
}
|
||||
|
||||
.restore-run-create-wizard .fi-sc-wizard-header-step {
|
||||
flex: 0 0 14rem;
|
||||
min-width: 14rem;
|
||||
max-width: 16rem;
|
||||
}
|
||||
|
||||
.restore-run-create-wizard .fi-sc-wizard-header-step-separator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.restore-run-create-wizard .fi-sc-wizard-header-step-btn {
|
||||
height: auto;
|
||||
min-height: 5rem;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
background: rgb(255 255 255) !important;
|
||||
border: 1px solid rgb(229 231 235);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 1px 2px rgb(15 23 42 / 0.04);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.dark .restore-run-create-wizard .fi-sc-wizard-header-step-btn {
|
||||
background: rgb(31 41 55) !important;
|
||||
border-color: rgb(55 65 81);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.restore-run-create-wizard .fi-sc-wizard-header-step.fi-active .fi-sc-wizard-header-step-btn {
|
||||
border-color: var(--primary-500);
|
||||
box-shadow: 0 0 0 1px color-mix(in oklab, var(--primary-500) 16%, transparent), 0 1px 2px rgb(15 23 42 / 0.04);
|
||||
}
|
||||
|
||||
.dark .restore-run-create-wizard .fi-sc-wizard-header-step.fi-active .fi-sc-wizard-header-step-btn {
|
||||
border-color: var(--primary-400);
|
||||
box-shadow: 0 0 0 1px color-mix(in oklab, var(--primary-400) 20%, transparent);
|
||||
}
|
||||
|
||||
.restore-run-create-wizard .fi-sc-wizard-header-step-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.restore-run-create-wizard .fi-sc-wizard-header-step-label,
|
||||
.restore-run-create-wizard .fi-sc-wizard-header-step-description {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.restore-run-create-wizard .fi-sc-wizard-header-step {
|
||||
flex-basis: 15rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.restore-run-create-wizard .fi-sc-wizard-header-step {
|
||||
flex: 0 0 calc((100% - 1rem) / 3);
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.restore-run-create-wizard .fi-sc-wizard-header-step {
|
||||
flex-basis: calc((100% - 2rem) / 5);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endonce
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<x-filament::section>
|
||||
<div data-testid="restore-run-decision-card" class="grid gap-5 xl:grid-cols-[minmax(0,1fr)_20rem]">
|
||||
|
||||
@ -0,0 +1,167 @@
|
||||
@php
|
||||
$fieldWrapperView = $getFieldWrapperView();
|
||||
$processFlow = is_array($processFlow ?? null) ? $processFlow : [];
|
||||
$wizardGate = is_array($wizardGate ?? null) ? $wizardGate : [];
|
||||
$steps = is_array($processFlow['steps'] ?? null) ? $processFlow['steps'] : [];
|
||||
$proofAside = is_array($proofAside ?? null) ? $proofAside : [];
|
||||
$proofItems = is_array($proofAside['items'] ?? null) ? $proofAside['items'] : [];
|
||||
$diagnosticsDisclosure = is_array($diagnosticsDisclosure ?? null) ? $diagnosticsDisclosure : [];
|
||||
$evidenceTitle = is_string($evidenceTitle ?? null) && $evidenceTitle !== '' ? $evidenceTitle : 'Safety evidence';
|
||||
$evidenceDescription = is_string($evidenceDescription ?? null) && $evidenceDescription !== ''
|
||||
? $evidenceDescription
|
||||
: 'Restore gates and proof stay available, but remain collapsed until review is needed.';
|
||||
$evidenceDetailsLabel = is_string($evidenceDetailsLabel ?? null) && $evidenceDetailsLabel !== ''
|
||||
? $evidenceDetailsLabel
|
||||
: 'View safety gates and proof';
|
||||
|
||||
$badgeTone = static function (string $status): string {
|
||||
return match ($status) {
|
||||
'complete' => 'success',
|
||||
'required' => 'warning',
|
||||
'blocked' => 'danger',
|
||||
default => 'gray',
|
||||
};
|
||||
};
|
||||
|
||||
$badgeLabel = static function (string $status): string {
|
||||
return match ($status) {
|
||||
'complete' => 'Complete',
|
||||
'required' => 'Required',
|
||||
'blocked' => 'Blocked',
|
||||
default => 'Unavailable',
|
||||
};
|
||||
};
|
||||
|
||||
$proofBadgeClasses = static function (string $tone): string {
|
||||
return match ($tone) {
|
||||
'success' => 'border-success-200 bg-success-50 text-success-700 dark:border-success-700 dark:bg-success-950/30 dark:text-success-300',
|
||||
'warning' => 'border-warning-200 bg-warning-50 text-warning-700 dark:border-warning-700 dark:bg-warning-950/30 dark:text-warning-300',
|
||||
'danger' => 'border-danger-200 bg-danger-50 text-danger-700 dark:border-danger-700 dark:bg-danger-950/30 dark:text-danger-300',
|
||||
default => 'border-gray-200 bg-gray-50 text-gray-700 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300',
|
||||
};
|
||||
};
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<x-filament::section>
|
||||
<div data-testid="restore-run-safety-evidence" class="space-y-4">
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-base font-semibold text-gray-950 dark:text-white">
|
||||
{{ $evidenceTitle }}
|
||||
</h2>
|
||||
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ $evidenceDescription }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<dl class="grid gap-2 sm:grid-cols-3 lg:min-w-[34rem]">
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">Gates</dt>
|
||||
<dd class="mt-0.5 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $processFlow['gatesComplete'] ?? 0 }}/{{ $processFlow['gatesTotal'] ?? count($steps) }} complete
|
||||
</dd>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">Next gate</dt>
|
||||
<dd class="mt-0.5 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $wizardGate['next_gate_label'] ?? ($processFlow['nextGate'] ?? 'Unavailable') }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">Execution</dt>
|
||||
<dd class="mt-0.5 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $wizardGate['execution_label'] ?? ($processFlow['executionLabel'] ?? 'Unavailable') }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<details class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $evidenceDetailsLabel }}
|
||||
</summary>
|
||||
|
||||
<div class="mt-4 grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(18rem,0.8fr)]">
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Restore safety gates
|
||||
</div>
|
||||
|
||||
<ol class="space-y-2" aria-label="Restore safety gates">
|
||||
@foreach ($steps as $step)
|
||||
@php
|
||||
$status = (string) ($step['status'] ?? 'unavailable');
|
||||
@endphp
|
||||
|
||||
<li
|
||||
data-testid="restore-run-process-flow-step"
|
||||
data-step-label="{{ $step['label'] ?? 'Gate' }}"
|
||||
data-step-status="{{ $status }}"
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-800 dark:bg-gray-950/50"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-gray-200 bg-white text-xs font-semibold text-gray-600 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300">
|
||||
{{ $step['step'] ?? '•' }}
|
||||
</span>
|
||||
<span class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $step['label'] ?? 'Gate' }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs leading-5 text-gray-600 dark:text-gray-300">
|
||||
{{ $step['summary'] ?? '' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<x-filament::badge :color="$badgeTone($status)" size="sm">
|
||||
{{ $badgeLabel($status) }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<aside class="space-y-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $proofAside['title'] ?? 'Restore Proof' }}
|
||||
</div>
|
||||
|
||||
@foreach ($proofItems as $item)
|
||||
<div
|
||||
data-testid="restore-run-proof-item"
|
||||
data-proof-label="{{ $item['label'] ?? 'Proof item' }}"
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-800 dark:bg-gray-950/50"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $item['label'] ?? 'Proof item' }}
|
||||
</div>
|
||||
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $item['description'] ?? '' }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="inline-flex shrink-0 items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $proofBadgeClasses((string) ($item['tone'] ?? 'gray')) }}">
|
||||
{{ $item['value'] ?? 'Unavailable' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<details data-testid="restore-run-diagnostics-disclosure" class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $diagnosticsDisclosure['label'] ?? 'Diagnostics - Collapsed' }}
|
||||
</summary>
|
||||
<p class="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ $diagnosticsDisclosure['summary'] ?? 'Diagnostics remain closed by default.' }}
|
||||
</p>
|
||||
</details>
|
||||
</aside>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</x-dynamic-component>
|
||||
@ -13,6 +13,7 @@
|
||||
$unresolvedCount = (int) ($mappingResolver['unresolvedCount'] ?? 0);
|
||||
$skippedCount = (int) ($mappingResolver['skippedCount'] ?? 0);
|
||||
$manualFallbackCount = (int) ($mappingResolver['manualFallbackCount'] ?? 0);
|
||||
$mappingRequiredCount = max($totalCount - $resolvedCount, 0);
|
||||
|
||||
$blockedReason = $blocked_reason ?? null;
|
||||
$blockedReason = is_string($blockedReason) && $blockedReason !== '' ? $blockedReason : null;
|
||||
@ -22,24 +23,50 @@
|
||||
@endphp
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<x-filament::section heading="Scope summary" description="Define the scope, then resolve any required dependency mappings before validation.">
|
||||
<x-filament::section heading="Scope summary" description="Scope changes invalidate checks and preview evidence.">
|
||||
<div data-testid="restore-run-scope-summary" class="space-y-4">
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">
|
||||
Selected scope
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
|
||||
Step 2 decision
|
||||
</div>
|
||||
<div class="text-base font-semibold text-gray-950 dark:text-white">
|
||||
{{ $scopeMode === 'selected' ? "{$selectedCount} selected item" . ($selectedCount === 1 ? '' : 's') : 'All items (default)' }}
|
||||
</div>
|
||||
@if ($blockedReason)
|
||||
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ $blockedReason }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $scopeMode === 'selected' ? "{$selectedCount} selected item" . ($selectedCount === 1 ? '' : 's') : 'All items (default)' }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
Scope changes invalidate checks and preview evidence.
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="inline-flex items-center rounded-full border border-gray-200 bg-gray-50 px-3 py-1 text-xs font-semibold text-gray-700 dark:border-gray-700 dark:bg-gray-950/60 dark:text-gray-300">
|
||||
{{ $scopeMode === 'selected' ? "{$selectedCount} item" . ($selectedCount === 1 ? '' : 's') : 'All items' }}
|
||||
</span>
|
||||
|
||||
@if ($totalCount > 0)
|
||||
<span class="inline-flex items-center rounded-full border border-warning-200 bg-warning-50 px-3 py-1 text-xs font-semibold text-warning-700 dark:border-warning-700 dark:bg-warning-950/30 dark:text-warning-300">
|
||||
{{ $mappingRequiredCount }} {{ $mappingRequiredCount === 1 ? 'mapping' : 'mappings' }} required
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex items-center rounded-full border border-success-200 bg-success-50 px-3 py-1 text-xs font-semibold text-success-700 dark:border-success-700 dark:bg-success-950/30 dark:text-success-300">
|
||||
No mappings required
|
||||
</span>
|
||||
@endif
|
||||
|
||||
<span class="inline-flex items-center rounded-full border px-3 py-1 text-xs font-semibold {{ $canContinue === true ? 'border-success-200 bg-success-50 text-success-700 dark:border-success-700 dark:bg-success-950/30 dark:text-success-300' : 'border-danger-200 bg-danger-50 text-danger-700 dark:border-danger-700 dark:bg-danger-950/30 dark:text-danger-300' }}">
|
||||
{{ $canContinue === true ? 'Validation ready' : 'Validation blocked' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">
|
||||
Dependency mappings
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
|
||||
Mapping state
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-950 dark:text-white">
|
||||
@if ($totalCount === 0)
|
||||
@ -51,10 +78,8 @@
|
||||
@if ($totalCount > 0)
|
||||
<div class="mt-2 flex flex-wrap gap-2 text-xs text-gray-600 dark:text-gray-300">
|
||||
<span>{{ $unresolvedCount }} unresolved</span>
|
||||
<span>·</span>
|
||||
<span>{{ $skippedCount }} skipped</span>
|
||||
@if ($manualFallbackCount > 0)
|
||||
<span>·</span>
|
||||
<span>{{ $manualFallbackCount }} manual fallback</span>
|
||||
@endif
|
||||
</div>
|
||||
@ -62,37 +87,17 @@
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">
|
||||
Next
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
|
||||
Next gate
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $canContinue === true ? 'Ready to continue' : 'Blocked' }}
|
||||
{{ $canContinue === true ? 'Validation can run' : 'Resolve target mappings' }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
Required mapping decisions must be resolved or intentionally skipped before validation.
|
||||
</div>
|
||||
@if ($blockedReason)
|
||||
<div class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $blockedReason }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($totalCount > 0)
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="restore-run-open-mapping-resolver"
|
||||
class="fi-color fi-color-primary fi-bg-color-400 hover:fi-bg-color-300 dark:fi-bg-color-600 dark:hover:fi-bg-color-500 fi-text-color-900 hover:fi-text-color-800 dark:fi-text-color-950 dark:hover:fi-text-color-950 fi-btn fi-size-sm"
|
||||
wire:loading.attr="disabled"
|
||||
x-on:click="
|
||||
const section = document.querySelector('[data-testid=restore-run-mapping-resolver-section]');
|
||||
section?.scrollIntoView();
|
||||
section?.querySelector('.fi-section-header')?.click();
|
||||
"
|
||||
>
|
||||
Resolve mappings
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</x-dynamic-component>
|
||||
|
||||
@ -274,7 +274,7 @@ function spec332ScreenshotsMetadataOnlyFixture(ManagedEnvironment $tenant): Back
|
||||
|
||||
spec332ScreenshotsSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('The selected backup does not contain a usable captured item yet.');
|
||||
$page->waitForText('Source not usable');
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->screenshot(true, spec332RestoreWizardScreenshot('step-1-backup-selected'));
|
||||
spec332CopyBrowserScreenshot('step-1-backup-selected', 'step-1-backup-selected.png');
|
||||
@ -297,7 +297,7 @@ function spec332ScreenshotsMetadataOnlyFixture(ManagedEnvironment $tenant): Back
|
||||
|
||||
spec332ScreenshotsSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Resolve 1 remaining group mapping before validation can prove the current draft.');
|
||||
$page->waitForText('Continue to scope and resolve required mappings.');
|
||||
spec332ScreenshotsWizardNext($page);
|
||||
|
||||
$page->waitForText('Resolve target mappings');
|
||||
@ -305,8 +305,13 @@ function spec332ScreenshotsMetadataOnlyFixture(ManagedEnvironment $tenant): Back
|
||||
$page->screenshot(true, spec332RestoreWizardScreenshot('step-2-scope-default'));
|
||||
spec332CopyBrowserScreenshot('step-2-scope-default', 'step-2-scope-default.png');
|
||||
|
||||
$page->click('[data-testid="restore-run-open-mapping-resolver"]')
|
||||
->waitForText('Resolve mappings');
|
||||
$page->script(<<<'JS'
|
||||
(() => {
|
||||
const section = document.querySelector('[data-testid="restore-run-mapping-resolver-section"]');
|
||||
section?.querySelector('.fi-section-header')?.click();
|
||||
})()
|
||||
JS);
|
||||
$page->waitForText('Hide mapping details');
|
||||
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->screenshot(true, spec332RestoreWizardScreenshot('step-2-resolver-expanded'));
|
||||
@ -324,7 +329,7 @@ function spec332ScreenshotsMetadataOnlyFixture(ManagedEnvironment $tenant): Back
|
||||
|
||||
spec332ScreenshotsSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('The selected backup does not contain a usable captured item yet.');
|
||||
$page->waitForText('Source not usable');
|
||||
spec332ScreenshotsWizardNext($page);
|
||||
$page->waitForText('Define Restore Scope');
|
||||
spec332ScreenshotsWizardNext($page);
|
||||
@ -353,7 +358,7 @@ function spec332ScreenshotsMetadataOnlyFixture(ManagedEnvironment $tenant): Back
|
||||
|
||||
spec332ScreenshotsSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('A usable source backup is selected for this restore draft.');
|
||||
$page->waitForText('Source selected');
|
||||
spec332ScreenshotsWizardNext($page);
|
||||
$page->waitForText('Define Restore Scope');
|
||||
spec332ScreenshotsWizardNext($page);
|
||||
@ -361,13 +366,13 @@ function spec332ScreenshotsMetadataOnlyFixture(ManagedEnvironment $tenant): Back
|
||||
|
||||
$page->waitForText('Run checks')
|
||||
->click('Run checks')
|
||||
->waitForText('No group-based assignments detected.');
|
||||
->waitForText('Validation passed');
|
||||
|
||||
spec332ScreenshotsWizardNext($page);
|
||||
|
||||
$page->waitForText('Generate preview')
|
||||
->click('Generate preview')
|
||||
->waitForText('Policy change preview');
|
||||
->waitForText('Preview details');
|
||||
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->screenshot(true, spec332RestoreWizardScreenshot('step-4-preview-generated'));
|
||||
|
||||
@ -250,7 +250,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
return [$backupSet, $backupItem];
|
||||
}
|
||||
|
||||
it('shows the full product process flow on step 1', function (): void {
|
||||
it('shows compact safety evidence on step 1', function (): void {
|
||||
[$user, $tenant] = spec332BrowserTenant();
|
||||
[$backupSet] = spec332BrowserUsableBackupFixture($tenant);
|
||||
|
||||
@ -261,28 +261,40 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
spec332BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('A usable source backup is selected for this restore draft.')
|
||||
$page->waitForText('Source selected')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->assertSee('Restore Safety')
|
||||
->assertSee('Backup quality summary')
|
||||
->assertSee('Restore safety gates')
|
||||
->assertSee('Restore Proof')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertSee('Continue to scope and resolve required mappings.')
|
||||
->assertSee('Validate impact before execution.')
|
||||
->assertSee('Safety evidence')
|
||||
->assertSee('View safety gates and proof')
|
||||
->assertSee('View quality caveat and detail')
|
||||
->assertSee('Continue to scope.')
|
||||
->assertSee('Validate impact.')
|
||||
->assertDontSee('Technical startability')
|
||||
->assertDontSee('write-gate')
|
||||
->assertDontSee('hard-blocker')
|
||||
->assertDontSee('Is this dangerous?')
|
||||
->assertDontSee('tenant-wide recoverability')
|
||||
->assertScript(spec332BrowserElementIsVisibleScript('restore-run-process-flow-full'), true)
|
||||
->assertScript(<<<'JS'
|
||||
Array.from(document.querySelectorAll('[data-testid="restore-run-safety-evidence"]')).some((element) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
|
||||
return style.display !== 'none'
|
||||
&& style.visibility !== 'hidden'
|
||||
&& ! element.hidden
|
||||
&& Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
||||
})
|
||||
JS, true)
|
||||
->assertScript(spec332BrowserElementIsVisibleScript('restore-run-process-flow-full'), false)
|
||||
->assertScript(spec332BrowserElementIsVisibleScript('restore-run-process-flow-compact'), false)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-safety-evidence\"] details")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-backup-quality-summary\"] details")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-diagnostics-disclosure\"]")?.open === false', true)
|
||||
->assertSee('A usable source backup is selected for this restore draft.');
|
||||
->assertSee('A usable source backup is selected.');
|
||||
});
|
||||
|
||||
it('shows compact restore safety status by default on step 2', function (): void {
|
||||
it('shows compact restore evidence by default on step 2', function (): void {
|
||||
[$user, $tenant] = spec332BrowserTenant();
|
||||
[$backupSet] = spec332BrowserUsableBackupFixture($tenant);
|
||||
|
||||
@ -293,22 +305,28 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
spec332BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('A usable source backup is selected for this restore draft.');
|
||||
$page->waitForText('Source selected');
|
||||
spec332BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('2/7 gates complete')
|
||||
->assertSee('Restore safety status')
|
||||
->assertSee('2/7 gates complete')
|
||||
->assertSee('View safety gates')
|
||||
->assertDontSee('Hide safety gates')
|
||||
->assertSee('Restore Proof')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
$page->waitForText('3/7 complete')
|
||||
->assertSee('Restore evidence')
|
||||
->assertSee('3/7 complete')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertDontSee('Technical startability')
|
||||
->assertDontSee('write-gate')
|
||||
->assertDontSee('hard-blocker')
|
||||
->assertScript(spec332BrowserElementIsVisibleScript('restore-run-process-flow-compact'), true)
|
||||
->assertScript(<<<'JS'
|
||||
Array.from(document.querySelectorAll('[data-testid="restore-run-safety-evidence"]')).some((element) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
|
||||
return style.display !== 'none'
|
||||
&& style.visibility !== 'hidden'
|
||||
&& ! element.hidden
|
||||
&& Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
||||
})
|
||||
JS, true)
|
||||
->assertScript(spec332BrowserElementIsVisibleScript('restore-run-process-flow-full'), false)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-process-flow-compact-expanded\"]") === null', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-safety-evidence\"] details")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-diagnostics-disclosure\"]")?.open === false', true);
|
||||
});
|
||||
|
||||
@ -323,31 +341,34 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
spec332BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Resolve 1 remaining group mapping before validation can prove the current draft.');
|
||||
$page->waitForText('Continue to scope and resolve required mappings.');
|
||||
spec332BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Resolve target mappings')
|
||||
->assertSee('Scope summary')
|
||||
->assertSee('Resolve mappings')
|
||||
->assertSee('1 mapping required')
|
||||
->assertSee('Validation blocked')
|
||||
->assertSee('0 of 1 mappings resolved')
|
||||
->assertSee('Resolve target mappings')
|
||||
->assertSee('Restore safety status')
|
||||
->assertSee('Restore Proof')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertSee('Restore evidence')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertSee('Resolve required mappings before validation can run.')
|
||||
->assertDontSee('Paste the target Entra ID group Object ID (GUID).')
|
||||
->assertScript(<<<'JS'
|
||||
(() => {
|
||||
const section = Array.from(document.querySelectorAll('.fi-section')).find((element) =>
|
||||
element.textContent?.includes('Resolve target mappings')
|
||||
);
|
||||
const section = document.querySelector('[data-testid="restore-run-mapping-resolver-section"]');
|
||||
|
||||
return section?.querySelector('.fi-section-content-ctn')?.getAttribute('aria-expanded') === 'false';
|
||||
})()
|
||||
JS, true)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-process-flow-compact-expanded\"]") === null', true);
|
||||
|
||||
$page->click('[data-testid="restore-run-open-mapping-resolver"]');
|
||||
$page->script(<<<'JS'
|
||||
(() => {
|
||||
const section = document.querySelector('[data-testid="restore-run-mapping-resolver-section"]');
|
||||
section?.querySelector('.fi-section-header')?.click();
|
||||
})()
|
||||
JS);
|
||||
|
||||
$page->waitForText('0 of 1 mappings resolved')
|
||||
->assertSee('0 of 1 mappings resolved')
|
||||
@ -360,9 +381,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
->assertDontSee('Paste the target Entra ID group Object ID (GUID).')
|
||||
->assertScript(<<<'JS'
|
||||
(() => {
|
||||
const section = Array.from(document.querySelectorAll('.fi-section')).find((element) =>
|
||||
element.textContent?.includes('Resolve target mappings')
|
||||
);
|
||||
const section = document.querySelector('[data-testid="restore-run-mapping-resolver-section"]');
|
||||
|
||||
return section?.querySelector('.fi-section-content-ctn')?.getAttribute('aria-expanded') === 'true';
|
||||
})()
|
||||
@ -380,7 +399,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
spec332BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Resolve 1 remaining group mapping before validation can prove the current draft.');
|
||||
$page->waitForText('Continue to scope and resolve required mappings.');
|
||||
spec332BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Resolve target mappings');
|
||||
@ -450,11 +469,16 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
spec332BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Resolve 1 remaining group mapping before validation can prove the current draft.');
|
||||
$page->waitForText('Continue to scope and resolve required mappings.');
|
||||
spec332BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Resolve target mappings')
|
||||
->click('[data-testid="restore-run-open-mapping-resolver"]');
|
||||
$page->waitForText('Resolve target mappings');
|
||||
$page->script(<<<'JS'
|
||||
(() => {
|
||||
const section = document.querySelector('[data-testid="restore-run-mapping-resolver-section"]');
|
||||
section?.querySelector('.fi-section-header')?.click();
|
||||
})()
|
||||
JS);
|
||||
|
||||
$pickerOpened = $page->script(<<<'JS'
|
||||
(() => {
|
||||
@ -484,7 +508,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
spec332BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Resolve 1 remaining group mapping before validation can prove the current draft.');
|
||||
$page->waitForText('Continue to scope and resolve required mappings.');
|
||||
spec332BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Resolve target mappings');
|
||||
@ -507,7 +531,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
spec332BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('The selected backup does not contain a usable captured item yet.');
|
||||
$page->waitForText('Source not usable');
|
||||
spec332BrowserWizardNext($page);
|
||||
$page->waitForText('Define Restore Scope');
|
||||
spec332BrowserWizardNext($page);
|
||||
@ -521,7 +545,10 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
$page
|
||||
->waitForText('Validation blocked')
|
||||
->assertSee('Resolve the blocking validation issues before moving to preview.')
|
||||
->assertSee('Validation decision')
|
||||
->assertSee('Select another backup set.')
|
||||
->assertSee('Snapshot completeness')
|
||||
->assertSee('Validation evidence')
|
||||
->assertSee('Safety & Conflict Checks');
|
||||
});
|
||||
|
||||
@ -536,7 +563,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
spec332BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('A usable source backup is selected for this restore draft.');
|
||||
$page->waitForText('Source selected');
|
||||
spec332BrowserWizardNext($page);
|
||||
$page->waitForText('Define Restore Scope');
|
||||
spec332BrowserWizardNext($page);
|
||||
@ -544,23 +571,35 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
$page->waitForText('Run checks')
|
||||
->click('Run checks')
|
||||
->waitForText('No group-based assignments detected.');
|
||||
->waitForText('Validation passed')
|
||||
->assertSee('Validation decision')
|
||||
->assertSee('Validation evidence')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertSee('Checks are current for this scope. Rerun only after scope or mapping changes.')
|
||||
->assertDontSee('Run checks after defining scope and mapping missing groups.')
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-safe-checks-details\"]")?.open === false', true);
|
||||
|
||||
spec332BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Generate preview')
|
||||
->click('Generate preview')
|
||||
->waitForText('Policy change preview')
|
||||
->assertSee('Review the preview and complete confirmation before execution can be queued.')
|
||||
->assertSee('Restore safety status')
|
||||
->assertSee('View safety gates')
|
||||
->waitForText('Preview details')
|
||||
->assertSee('Review the preview and continue to confirmation.')
|
||||
->assertSee('Regenerate preview')
|
||||
->assertSee('Preview is current for this scope. Regenerate only after scope, mapping, or source changes.')
|
||||
->assertSee('Preview evidence')
|
||||
->assertSee('View safety gates and restore proof')
|
||||
->assertSee('Restore action')
|
||||
->assertSee('Update existing')
|
||||
->assertSee('Policy diff')
|
||||
->assertSee('Assignments')
|
||||
->assertSee('Scope tags')
|
||||
->assertDontSee('Restore safety status')
|
||||
->assertDontSee('Hide safety gates')
|
||||
->assertSee('Restore Proof')
|
||||
->assertSee('Operation proof')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertDontSee('tenant-wide recovery is proven')
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-process-flow-compact\"]") !== null', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-process-flow-full\"]") === null', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-preview-evidence-details\"]")?.open === false', true)
|
||||
->assertScript(spec332BrowserElementIsVisibleScript('restore-run-process-flow-compact'), false)
|
||||
->assertScript(spec332BrowserElementIsVisibleScript('restore-run-process-flow-full'), false)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-diagnostics-disclosure\"]")?.open === false', true);
|
||||
});
|
||||
|
||||
@ -575,7 +614,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
spec332BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('A usable source backup is selected for this restore draft.');
|
||||
$page->waitForText('Source selected');
|
||||
spec332BrowserWizardNext($page);
|
||||
$page->waitForText('Define Restore Scope');
|
||||
spec332BrowserWizardNext($page);
|
||||
@ -583,21 +622,23 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
$page->waitForText('Run checks')
|
||||
->click('Run checks')
|
||||
->waitForText('No group-based assignments detected.');
|
||||
->waitForText('Validation passed')
|
||||
->assertSee('Validation evidence')
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-safe-checks-details\"]")?.open === false', true);
|
||||
|
||||
spec332BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Generate preview')
|
||||
->click('Generate preview')
|
||||
->waitForText('Policy change preview');
|
||||
->waitForText('Preview details');
|
||||
|
||||
spec332BrowserWizardNext($page);
|
||||
|
||||
$page
|
||||
->waitForText('Confirm & Execute')
|
||||
->assertSee('Confirmation summary')
|
||||
->assertSee('Available after confirmation')
|
||||
->assertSee('Confirmation does not claim recovery.')
|
||||
->assertSee('Execution unavailable until confirmation')
|
||||
->assertSee('Recovery is not verified until post-run evidence exists.')
|
||||
->assertSee('Restore Proof')
|
||||
->assertSee('Operation proof')
|
||||
->assertSee('Post-run evidence')
|
||||
|
||||
@ -0,0 +1,735 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Resources\RestoreRunResource;
|
||||
use App\Models\BackupItem;
|
||||
use App\Models\BackupSet;
|
||||
use App\Models\EntraGroup;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\Policy;
|
||||
use App\Models\PolicyVersion;
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
pest()->browser()->timeout(60_000);
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function spec333BrowserScreenshotName(string $name): string
|
||||
{
|
||||
return 'spec333-restore-create-'.$name;
|
||||
}
|
||||
|
||||
function spec333CopyBrowserScreenshot(string $name): void
|
||||
{
|
||||
$filename = spec333BrowserScreenshotName($name).'.png';
|
||||
$source = base_path('tests/Browser/Screenshots/'.$filename);
|
||||
$targetDirectory = repo_path('specs/333-restore-create-ux-final-productization/artifacts/screenshots');
|
||||
|
||||
if (! is_dir($targetDirectory)) {
|
||||
@mkdir($targetDirectory, 0755, true);
|
||||
}
|
||||
|
||||
if (! is_dir($targetDirectory) || ! is_writable($targetDirectory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! is_file($source)) {
|
||||
$source = \Pest\Browser\Support\Screenshot::path($filename);
|
||||
}
|
||||
|
||||
for ($attempt = 0; $attempt < 10 && ! is_file($source); $attempt++) {
|
||||
usleep(100_000);
|
||||
clearstatcache(true, $source);
|
||||
}
|
||||
|
||||
if (is_file($source)) {
|
||||
@copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$name.'.png');
|
||||
}
|
||||
}
|
||||
|
||||
function spec333BrowserLoginUrl(User $user, ManagedEnvironment $tenant, string $redirect = ''): string
|
||||
{
|
||||
return route('admin.local.smoke-login', array_filter([
|
||||
'email' => $user->email,
|
||||
'tenant' => $tenant->external_id,
|
||||
'workspace' => $tenant->workspace->slug,
|
||||
'redirect' => $redirect,
|
||||
], static fn (?string $value): bool => filled($value)));
|
||||
}
|
||||
|
||||
function spec333BrowserTenant(bool $credentialAvailable = true): array
|
||||
{
|
||||
$tenant = ManagedEnvironment::factory()->create([
|
||||
'rbac_status' => 'ok',
|
||||
'rbac_last_checked_at' => now(),
|
||||
]);
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
ensureDefaultProviderConnection($tenant, 'microsoft', ensureCredential: $credentialAvailable);
|
||||
bindFailHardGraphClient();
|
||||
|
||||
return [$user, $tenant];
|
||||
}
|
||||
|
||||
function spec333BrowserRedirect(ManagedEnvironment $tenant): string
|
||||
{
|
||||
$redirectBase = RestoreRunResource::getUrl('create', panel: 'admin', tenant: $tenant);
|
||||
|
||||
return parse_url($redirectBase, PHP_URL_PATH) ?: '/admin';
|
||||
}
|
||||
|
||||
function spec333BrowserSelectBackupSet($page, BackupSet $backupSet): void
|
||||
{
|
||||
$selected = $page->script(<<<JS
|
||||
(() => {
|
||||
const select = document.getElementById('form.backup_set_id');
|
||||
|
||||
if (! select) {
|
||||
return false;
|
||||
}
|
||||
|
||||
select.value = '{$backupSet->getKey()}';
|
||||
select.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
return true;
|
||||
})()
|
||||
JS);
|
||||
|
||||
expect($selected)->toBeTrue();
|
||||
}
|
||||
|
||||
function spec333BrowserWizardNext($page): void
|
||||
{
|
||||
$clicked = $page->script(<<<'JS'
|
||||
(() => {
|
||||
const footer = document.querySelector('.fi-sc-wizard-footer');
|
||||
|
||||
if (! footer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nextTrigger = footer.querySelector('div[x-on\\:click*="requestNextStep"]');
|
||||
|
||||
if (! nextTrigger || nextTrigger.classList.contains('fi-hidden')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nextTrigger.scrollIntoView({ block: 'center' });
|
||||
nextTrigger.click();
|
||||
|
||||
return true;
|
||||
})()
|
||||
JS);
|
||||
|
||||
expect($clicked)->toBeTrue();
|
||||
}
|
||||
|
||||
function spec333BrowserUsableBackupFixture(ManagedEnvironment $tenant): BackupSet
|
||||
{
|
||||
$policy = Policy::create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'external_id' => 'spec333-browser-policy-usable',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'display_name' => 'Spec333 Browser Policy',
|
||||
'platform' => 'windows',
|
||||
]);
|
||||
|
||||
PolicyVersion::create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'policy_id' => (int) $policy->getKey(),
|
||||
'version_number' => 1,
|
||||
'policy_type' => $policy->policy_type,
|
||||
'platform' => $policy->platform,
|
||||
'captured_at' => now(),
|
||||
'snapshot' => [
|
||||
'foo' => 'current',
|
||||
],
|
||||
'metadata' => [],
|
||||
'assignments' => [],
|
||||
'scope_tags' => [],
|
||||
]);
|
||||
|
||||
$backupSet = BackupSet::factory()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'name' => 'Spec333 Browser Usable Backup',
|
||||
'status' => 'completed',
|
||||
'item_count' => 1,
|
||||
]);
|
||||
|
||||
BackupItem::factory()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'policy_id' => (int) $policy->getKey(),
|
||||
'policy_identifier' => $policy->external_id,
|
||||
'policy_type' => $policy->policy_type,
|
||||
'platform' => $policy->platform,
|
||||
'captured_at' => now(),
|
||||
'payload' => [
|
||||
'foo' => 'backup',
|
||||
'displayName' => 'Spec333 Browser Policy',
|
||||
],
|
||||
'assignments' => [],
|
||||
'metadata' => [
|
||||
'displayName' => 'Spec333 Browser Policy',
|
||||
],
|
||||
]);
|
||||
|
||||
return $backupSet;
|
||||
}
|
||||
|
||||
function spec333BrowserGroupBackupFixture(ManagedEnvironment $tenant): BackupSet
|
||||
{
|
||||
$policy = Policy::create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'external_id' => 'spec333-browser-policy-group',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'display_name' => 'Spec333 Group Mapping Policy',
|
||||
'platform' => 'windows',
|
||||
]);
|
||||
|
||||
$backupSet = BackupSet::factory()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'name' => 'Spec333 Browser Group Backup',
|
||||
'status' => 'completed',
|
||||
'item_count' => 1,
|
||||
]);
|
||||
|
||||
BackupItem::factory()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'policy_id' => (int) $policy->getKey(),
|
||||
'policy_identifier' => $policy->external_id,
|
||||
'policy_type' => $policy->policy_type,
|
||||
'platform' => $policy->platform,
|
||||
'captured_at' => now(),
|
||||
'payload' => [
|
||||
'foo' => 'backup',
|
||||
'displayName' => 'Spec333 Group Mapping Policy',
|
||||
],
|
||||
'assignments' => [[
|
||||
'target' => [
|
||||
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
|
||||
'groupId' => '11111111-1111-1111-1111-111111111111',
|
||||
'group_display_name' => 'Spec333 Missing Group',
|
||||
],
|
||||
]],
|
||||
'metadata' => [
|
||||
'displayName' => 'Spec333 Group Mapping Policy',
|
||||
],
|
||||
]);
|
||||
|
||||
return $backupSet;
|
||||
}
|
||||
|
||||
function spec333BrowserOpenGroupPicker($page): void
|
||||
{
|
||||
$opened = $page->script(<<<'JS'
|
||||
(() => {
|
||||
const header = Array.from(document.querySelectorAll('.fi-section-header')).find((element) =>
|
||||
element.textContent?.includes('Resolve target mappings')
|
||||
);
|
||||
|
||||
header?.click();
|
||||
|
||||
const pickerButton = document.querySelector('button[wire\\:click*="select_from_directory_cache_11111111_1111_1111_1111_111111111111"]');
|
||||
|
||||
pickerButton?.click();
|
||||
|
||||
return Boolean(pickerButton);
|
||||
})()
|
||||
JS);
|
||||
|
||||
expect($opened)->toBeTrue();
|
||||
}
|
||||
|
||||
function spec333BrowserOpenMappingResolver($page): void
|
||||
{
|
||||
$opened = $page->script(<<<'JS'
|
||||
(() => {
|
||||
const section = document.querySelector('[data-testid="restore-run-mapping-resolver-section"]');
|
||||
const content = section?.querySelector('.fi-section-content-ctn');
|
||||
|
||||
if (content?.getAttribute('aria-expanded') !== 'true') {
|
||||
section?.querySelector('.fi-section-header')?.click();
|
||||
}
|
||||
|
||||
return Boolean(section);
|
||||
})()
|
||||
JS);
|
||||
|
||||
expect($opened)->toBeTrue();
|
||||
}
|
||||
|
||||
function spec333BrowserWizardHeaderMetrics($page): array
|
||||
{
|
||||
return $page->script(<<<'JS'
|
||||
(() => {
|
||||
const header = document.querySelector('.restore-run-create-wizard .fi-sc-wizard-header');
|
||||
const steps = Array.from(document.querySelectorAll('.restore-run-create-wizard .fi-sc-wizard-header-step'));
|
||||
const buttons = Array.from(document.querySelectorAll('.restore-run-create-wizard .fi-sc-wizard-header-step-btn'));
|
||||
const activeButton = document.querySelector('.restore-run-create-wizard .fi-sc-wizard-header-step.fi-active .fi-sc-wizard-header-step-btn');
|
||||
const inactiveButton = document.querySelector('.restore-run-create-wizard .fi-sc-wizard-header-step:not(.fi-active) .fi-sc-wizard-header-step-btn');
|
||||
const headerStyles = header ? window.getComputedStyle(header) : null;
|
||||
const firstButtonStyles = buttons[0] ? window.getComputedStyle(buttons[0]) : null;
|
||||
const activeButtonStyles = activeButton ? window.getComputedStyle(activeButton) : null;
|
||||
const inactiveButtonStyles = inactiveButton ? window.getComputedStyle(inactiveButton) : null;
|
||||
const stepWidths = steps.map((step) => Math.round(step.getBoundingClientRect().width));
|
||||
const buttonHeights = buttons.map((button) => Math.round(button.getBoundingClientRect().height));
|
||||
const documentStyles = window.getComputedStyle(document.documentElement);
|
||||
|
||||
return {
|
||||
display: headerStyles?.display ?? null,
|
||||
overflowX: headerStyles?.overflowX ?? null,
|
||||
primary400: documentStyles.getPropertyValue('--primary-400').trim(),
|
||||
primary500: documentStyles.getPropertyValue('--primary-500').trim(),
|
||||
headerBackground: headerStyles?.backgroundColor ?? null,
|
||||
firstButtonBackground: firstButtonStyles?.backgroundColor ?? null,
|
||||
firstButtonBorderColor: firstButtonStyles?.borderColor ?? null,
|
||||
inactiveButtonBorderColor: inactiveButtonStyles?.borderColor ?? null,
|
||||
activeButtonBorderColor: activeButtonStyles?.borderColor ?? null,
|
||||
activeButtonShadow: activeButtonStyles?.boxShadow ?? null,
|
||||
stepCount: steps.length,
|
||||
pageOverflows: document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
||||
headerScrollsInternally: Boolean(header && header.scrollWidth > header.clientWidth),
|
||||
smallestStepWidth: Math.min(...stepWidths),
|
||||
tallestButtonHeight: Math.max(...buttonHeights),
|
||||
};
|
||||
})()
|
||||
JS);
|
||||
}
|
||||
|
||||
it('keeps the restore create wizard header compact at enterprise browser widths', function (): void {
|
||||
[$user, $tenant] = spec333BrowserTenant();
|
||||
$backupSet = spec333BrowserUsableBackupFixture($tenant);
|
||||
|
||||
$page = visit(spec333BrowserLoginUrl($user, $tenant, spec333BrowserRedirect($tenant)));
|
||||
|
||||
$page->resize(900, 1100)
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('A usable source backup is selected.')
|
||||
->assertSee('Backup quality summary')
|
||||
->assertSee('Available')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
$metrics = spec333BrowserWizardHeaderMetrics($page);
|
||||
|
||||
expect($metrics)
|
||||
->display->toBe('flex')
|
||||
->overflowX->toBe('auto')
|
||||
->stepCount->toBe(5)
|
||||
->pageOverflows->toBeFalse()
|
||||
->headerScrollsInternally->toBeTrue()
|
||||
->smallestStepWidth->toBeGreaterThanOrEqual(220)
|
||||
->tallestButtonHeight->toBeLessThanOrEqual(120);
|
||||
|
||||
expect($metrics['headerBackground'])->not->toBe($metrics['firstButtonBackground']);
|
||||
expect($metrics['primary500'])->not->toBeEmpty();
|
||||
expect($metrics['activeButtonBorderColor'])->toBe($metrics['primary500']);
|
||||
expect($metrics['activeButtonBorderColor'])->not->toBe($metrics['inactiveButtonBorderColor']);
|
||||
expect($metrics['activeButtonShadow'])->not->toBe('none');
|
||||
|
||||
$page->script("document.documentElement.classList.add('dark');");
|
||||
|
||||
$darkMetrics = spec333BrowserWizardHeaderMetrics($page);
|
||||
|
||||
expect($darkMetrics['primary400'])->not->toBeEmpty();
|
||||
|
||||
expect($darkMetrics)
|
||||
->headerBackground->toBe('rgb(17, 24, 39)')
|
||||
->firstButtonBackground->toBe('rgb(31, 41, 55)')
|
||||
->activeButtonBorderColor->toBe($darkMetrics['primary400'])
|
||||
->pageOverflows->toBeFalse()
|
||||
->smallestStepWidth->toBeGreaterThanOrEqual(220)
|
||||
->tallestButtonHeight->toBeLessThanOrEqual(120);
|
||||
});
|
||||
|
||||
it('captures step 1 and step 2 restore create states', function (): void {
|
||||
[$user, $tenant] = spec333BrowserTenant();
|
||||
$backupSet = spec333BrowserGroupBackupFixture($tenant);
|
||||
|
||||
$page = visit(spec333BrowserLoginUrl($user, $tenant, spec333BrowserRedirect($tenant)));
|
||||
|
||||
$page->resize(1920, 1200)
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Source selected')
|
||||
->assertSee('Continue to scope and resolve required mappings.')
|
||||
->assertSee('Safety evidence')
|
||||
->assertSee('View safety gates and proof')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
$stepOneDensity = $page->script(<<<'JS'
|
||||
(() => {
|
||||
const evidence = document.querySelector('[data-testid="restore-run-safety-evidence"]');
|
||||
const evidenceDetails = evidence?.querySelector('details');
|
||||
const qualityDetails = document.querySelector('[data-testid="restore-run-backup-quality-summary"] details');
|
||||
return {
|
||||
evidenceDetailsOpen: Boolean(evidenceDetails?.open),
|
||||
qualityDetailsOpen: Boolean(qualityDetails?.open),
|
||||
evidenceHeight: Math.round(evidence?.getBoundingClientRect().height ?? 0),
|
||||
qualityHeight: Math.round(document.querySelector('[data-testid="restore-run-backup-quality-summary"]')?.getBoundingClientRect().height ?? 0),
|
||||
};
|
||||
})()
|
||||
JS);
|
||||
|
||||
expect($stepOneDensity)
|
||||
->evidenceDetailsOpen->toBeFalse()
|
||||
->qualityDetailsOpen->toBeFalse()
|
||||
->evidenceHeight->toBeLessThanOrEqual(180)
|
||||
->qualityHeight->toBeLessThanOrEqual(260);
|
||||
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->screenshot(true, spec333BrowserScreenshotName('01-step-1-backup-selected'));
|
||||
spec333CopyBrowserScreenshot('01-step-1-backup-selected');
|
||||
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Scope summary')
|
||||
->assertSee('1 mapping required')
|
||||
->assertSee('Validation blocked')
|
||||
->assertSee('Resolve target mappings')
|
||||
->assertSee('Restore evidence')
|
||||
->assertDontSee('Resolve mappings');
|
||||
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->screenshot(true, spec333BrowserScreenshotName('02-step-2-scope-default'));
|
||||
spec333CopyBrowserScreenshot('02-step-2-scope-default');
|
||||
|
||||
spec333BrowserOpenMappingResolver($page);
|
||||
$page->waitForText('Hide mapping details');
|
||||
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->screenshot(true, spec333BrowserScreenshotName('03-step-2-resolver-expanded'));
|
||||
spec333CopyBrowserScreenshot('03-step-2-resolver-expanded');
|
||||
});
|
||||
|
||||
it('captures group picker results and empty states', function (): void {
|
||||
[$userWithCache, $tenantWithCache] = spec333BrowserTenant();
|
||||
$backupSetWithCache = spec333BrowserGroupBackupFixture($tenantWithCache);
|
||||
|
||||
EntraGroup::factory()->for($tenantWithCache)->create([
|
||||
'entra_id' => 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
'display_name' => 'Spec333 Cached Target Group',
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
|
||||
$page = visit(spec333BrowserLoginUrl($userWithCache, $tenantWithCache, spec333BrowserRedirect($tenantWithCache)));
|
||||
|
||||
$page->resize(1920, 1200)
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $backupSetWithCache);
|
||||
$page->waitForText('Continue to scope and resolve required mappings.');
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Resolve target mappings');
|
||||
spec333BrowserOpenGroupPicker($page);
|
||||
|
||||
$page->waitForText('Resolve target group mapping')
|
||||
->assertSee('Spec333 Cached Target Group')
|
||||
->assertDontSee('No directory group cache available');
|
||||
|
||||
$page->screenshot(true, spec333BrowserScreenshotName('04-step-2-group-picker-results'));
|
||||
spec333CopyBrowserScreenshot('04-step-2-group-picker-results');
|
||||
|
||||
[$userWithoutCache, $tenantWithoutCache] = spec333BrowserTenant();
|
||||
$backupSetWithoutCache = spec333BrowserGroupBackupFixture($tenantWithoutCache);
|
||||
|
||||
$page = visit(spec333BrowserLoginUrl($userWithoutCache, $tenantWithoutCache, spec333BrowserRedirect($tenantWithoutCache)));
|
||||
|
||||
$page->resize(1920, 1200)
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $backupSetWithoutCache);
|
||||
$page->waitForText('Continue to scope and resolve required mappings.');
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Resolve target mappings');
|
||||
spec333BrowserOpenGroupPicker($page);
|
||||
|
||||
$page->waitForText('No directory group cache available')
|
||||
->assertSee('Open group sync')
|
||||
->assertSee('View group sync operations');
|
||||
|
||||
$page->screenshot(true, spec333BrowserScreenshotName('05-step-2-group-picker-empty'));
|
||||
spec333CopyBrowserScreenshot('05-step-2-group-picker-empty');
|
||||
});
|
||||
|
||||
it('captures blocked and passed validation states', function (): void {
|
||||
[$blockedUser, $blockedTenant] = spec333BrowserTenant(credentialAvailable: false);
|
||||
$blockedBackupSet = spec333BrowserUsableBackupFixture($blockedTenant);
|
||||
|
||||
$page = visit(spec333BrowserLoginUrl($blockedUser, $blockedTenant, spec333BrowserRedirect($blockedTenant)));
|
||||
|
||||
$page->resize(1920, 1200)
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $blockedBackupSet);
|
||||
$page->waitForText('Source selected');
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Scope summary');
|
||||
spec333BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Validation blocked')
|
||||
->assertSee('Validation decision')
|
||||
->assertSee('Provider access must be repaired before restore checks can run.')
|
||||
->assertSee('Provider credentials are not available for this environment.')
|
||||
->assertSee('Review provider connection')
|
||||
->assertSee('Repair the provider connection before validation can run.')
|
||||
->assertDontSee('Run checks');
|
||||
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->screenshot(true, spec333BrowserScreenshotName('06-step-3-validation-blocked'));
|
||||
spec333CopyBrowserScreenshot('06-step-3-validation-blocked');
|
||||
|
||||
[$passedUser, $passedTenant] = spec333BrowserTenant();
|
||||
$passedBackupSet = spec333BrowserUsableBackupFixture($passedTenant);
|
||||
|
||||
$page = visit(spec333BrowserLoginUrl($passedUser, $passedTenant, spec333BrowserRedirect($passedTenant)));
|
||||
|
||||
$page->resize(1920, 1200)
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $passedBackupSet);
|
||||
$page->waitForText('Source selected');
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Scope summary');
|
||||
spec333BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Run checks')
|
||||
->click('Run checks')
|
||||
->waitForText('Validation passed')
|
||||
->assertSee('View 7 safe check details')
|
||||
->assertSee('Prerequisites available')
|
||||
->assertSee('Generate a preview for the current scope before confirmation.')
|
||||
->assertSee('Checks are current for this scope. Rerun only after scope or mapping changes.')
|
||||
->assertSee('Validation evidence')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertDontSee('Run checks after defining scope and mapping missing groups.')
|
||||
->assertDontSee('Safety checks completed');
|
||||
|
||||
$page->assertScript('document.querySelector("[data-testid=\"restore-run-safe-checks-details\"]")?.open === false', true);
|
||||
|
||||
$validationSurfaceMetrics = $page->script(<<<'JS'
|
||||
(() => {
|
||||
const decisionCard = document.querySelector('[data-testid="restore-run-validation-decision-card"]');
|
||||
const statCard = document.querySelector('[data-testid="restore-run-validation-stat-card"]');
|
||||
|
||||
if (! decisionCard || ! statCard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const decisionStyle = window.getComputedStyle(decisionCard);
|
||||
const statStyle = window.getComputedStyle(statCard);
|
||||
|
||||
return {
|
||||
decisionBackground: decisionStyle.backgroundColor,
|
||||
decisionBorder: decisionStyle.borderColor,
|
||||
statBackground: statStyle.backgroundColor,
|
||||
};
|
||||
})()
|
||||
JS);
|
||||
|
||||
expect($validationSurfaceMetrics)->not->toBeNull();
|
||||
expect($validationSurfaceMetrics['decisionBackground'])->not->toBe($validationSurfaceMetrics['statBackground']);
|
||||
|
||||
$page->script("document.documentElement.classList.add('dark');");
|
||||
|
||||
$darkValidationSurfaceMetrics = $page->script(<<<'JS'
|
||||
(() => {
|
||||
const decisionCard = document.querySelector('[data-testid="restore-run-validation-decision-card"]');
|
||||
const statCard = document.querySelector('[data-testid="restore-run-validation-stat-card"]');
|
||||
|
||||
if (! decisionCard || ! statCard) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const decisionStyle = window.getComputedStyle(decisionCard);
|
||||
const statStyle = window.getComputedStyle(statCard);
|
||||
|
||||
return {
|
||||
decisionBackground: decisionStyle.backgroundColor,
|
||||
decisionBorder: decisionStyle.borderColor,
|
||||
statBackground: statStyle.backgroundColor,
|
||||
};
|
||||
})()
|
||||
JS);
|
||||
|
||||
expect($darkValidationSurfaceMetrics)->not->toBeNull();
|
||||
expect($darkValidationSurfaceMetrics['decisionBackground'])->not->toBe($validationSurfaceMetrics['decisionBackground']);
|
||||
expect($darkValidationSurfaceMetrics['decisionBackground'])->not->toBe($darkValidationSurfaceMetrics['statBackground']);
|
||||
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->screenshot(true, spec333BrowserScreenshotName('07-step-3-validation-passed-dark'));
|
||||
spec333CopyBrowserScreenshot('07-step-3-validation-passed-dark');
|
||||
|
||||
$page->script("document.documentElement.classList.remove('dark');");
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->screenshot(true, spec333BrowserScreenshotName('07-step-3-validation-passed'));
|
||||
spec333CopyBrowserScreenshot('07-step-3-validation-passed');
|
||||
});
|
||||
|
||||
it('captures preview and confirmation states after current evidence exists', function (): void {
|
||||
[$user, $tenant] = spec333BrowserTenant();
|
||||
$backupSet = spec333BrowserUsableBackupFixture($tenant);
|
||||
|
||||
$page = visit(spec333BrowserLoginUrl($user, $tenant, spec333BrowserRedirect($tenant)));
|
||||
|
||||
$page->resize(1920, 1200)
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $backupSet);
|
||||
$page->waitForText('Source selected');
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Scope summary');
|
||||
spec333BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Run checks')
|
||||
->click('Run checks')
|
||||
->waitForText('Validation passed');
|
||||
|
||||
spec333BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Generate preview')
|
||||
->click('Generate preview')
|
||||
->waitForText('Preview details')
|
||||
->assertSee('Review the preview and continue to confirmation.')
|
||||
->assertSee('Regenerate preview')
|
||||
->assertSee('Preview is current for this scope. Regenerate only after scope, mapping, or source changes.')
|
||||
->assertSee('Preview evidence')
|
||||
->assertSee('View safety gates and restore proof')
|
||||
->assertSee('Needs attention')
|
||||
->assertSee('Changes detected')
|
||||
->assertSee('Restore action')
|
||||
->assertSee('Update existing')
|
||||
->assertSee('Policy diff')
|
||||
->assertSee('Assignments')
|
||||
->assertSee('Scope tags')
|
||||
->assertSee('No changes detected')
|
||||
->assertSee('All reviewed items')
|
||||
->assertDontSee('Restore safety status')
|
||||
->assertDontSee('What the preview proves')
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-preview-evidence-details\"]")?.open === false', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
$page->resize(680, 1100);
|
||||
$page->script('document.querySelector("[data-testid=\"restore-run-preview-review-cards\"][data-preview-group=\"changed\"]")?.scrollIntoView({ block: "center" });');
|
||||
$page
|
||||
->assertScript('(() => {
|
||||
const cards = document.querySelector("[data-testid=\"restore-run-preview-review-cards\"][data-preview-group=\"changed\"]");
|
||||
const table = document.querySelector("[data-testid=\"restore-run-preview-review-table\"][data-preview-group=\"changed\"]");
|
||||
|
||||
return Boolean(
|
||||
cards
|
||||
&& table
|
||||
&& getComputedStyle(cards).display !== "none"
|
||||
&& cards.getClientRects().length > 0
|
||||
&& getComputedStyle(table).display === "none"
|
||||
);
|
||||
})()', true)
|
||||
->assertSee('Policy diff')
|
||||
->assertSee('0 added')
|
||||
->assertSee('0 removed')
|
||||
->assertSee('1 changed');
|
||||
|
||||
$page->resize(1920, 1200);
|
||||
$page->script('document.querySelector("[data-testid=\"restore-run-preview-review-table\"][data-preview-group=\"changed\"]")?.scrollIntoView({ block: "center" });');
|
||||
$page
|
||||
->assertScript('(() => {
|
||||
const cards = document.querySelector("[data-testid=\"restore-run-preview-review-cards\"][data-preview-group=\"changed\"]");
|
||||
const table = document.querySelector("[data-testid=\"restore-run-preview-review-table\"][data-preview-group=\"changed\"]");
|
||||
|
||||
return Boolean(
|
||||
cards
|
||||
&& table
|
||||
&& getComputedStyle(cards).display === "none"
|
||||
&& getComputedStyle(table).display !== "none"
|
||||
&& table.getClientRects().length > 0
|
||||
);
|
||||
})()', true);
|
||||
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->screenshot(true, spec333BrowserScreenshotName('08-step-4-preview-generated'));
|
||||
spec333CopyBrowserScreenshot('08-step-4-preview-generated');
|
||||
|
||||
spec333BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Confirm & Execute')
|
||||
->assertSee('Preview-only run ready')
|
||||
->assertSee('Create preview-only run')
|
||||
->assertSee('Create a preview-only restore run.')
|
||||
->assertSee('Operation proof is unavailable before execution.')
|
||||
->assertSee('Post-run evidence is unavailable before execution.')
|
||||
->assertSee('Recovery is not verified until post-run evidence exists.');
|
||||
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->screenshot(true, spec333BrowserScreenshotName('09-step-5-confirm-ready'));
|
||||
spec333CopyBrowserScreenshot('09-step-5-confirm-ready');
|
||||
});
|
||||
|
||||
it('captures confirmation review when execution prerequisites become unavailable after preview', function (): void {
|
||||
[$user, $tenant] = spec333BrowserTenant();
|
||||
$backupSet = spec333BrowserUsableBackupFixture($tenant);
|
||||
|
||||
$page = visit(spec333BrowserLoginUrl($user, $tenant, spec333BrowserRedirect($tenant)));
|
||||
|
||||
$page->resize(1920, 1200)
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $backupSet);
|
||||
$page->waitForText('Source selected');
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Scope summary');
|
||||
spec333BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Run checks')
|
||||
->click('Run checks')
|
||||
->waitForText('Validation passed');
|
||||
|
||||
spec333BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Generate preview')
|
||||
->click('Generate preview')
|
||||
->waitForText('Preview details');
|
||||
|
||||
$connection = ProviderConnection::query()
|
||||
->where('managed_environment_id', (int) $tenant->getKey())
|
||||
->where('provider', 'microsoft')
|
||||
->first();
|
||||
|
||||
$connection?->credential()->delete();
|
||||
|
||||
$page->waitForText('Regenerate preview')
|
||||
->click('Regenerate preview')
|
||||
->waitForText('Execution unavailable until prerequisites are resolved')
|
||||
->assertSee('Confirmation required')
|
||||
->assertSee('Review preview and continue to confirmation; resolve execution prerequisites before executing.')
|
||||
->assertDontSee('Execution blocked');
|
||||
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->screenshot(true, spec333BrowserScreenshotName('10-step-4-preview-execution-prerequisites-unavailable'));
|
||||
spec333CopyBrowserScreenshot('10-step-4-preview-execution-prerequisites-unavailable');
|
||||
|
||||
spec333BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Confirm & Execute')
|
||||
->assertSee('Confirmation summary')
|
||||
->assertSee('Execution prerequisites blocked')
|
||||
->assertSee('Create preview-only run')
|
||||
->assertSee('Create a preview-only run, or resolve execution prerequisites before queueing real execution.')
|
||||
->assertSee('Execution unavailable until prerequisites are resolved')
|
||||
->assertSee('Operation proof is unavailable before execution.')
|
||||
->assertSee('Preview only (dry-run)');
|
||||
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->screenshot(true, spec333BrowserScreenshotName('11-step-5-execution-prerequisites-locked'));
|
||||
spec333CopyBrowserScreenshot('11-step-5-execution-prerequisites-locked');
|
||||
});
|
||||
@ -3,7 +3,10 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Resources\RestoreRunResource\Pages\CreateRestoreRun;
|
||||
use App\Models\BackupItem;
|
||||
use App\Models\BackupSet;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\Policy;
|
||||
use App\Support\RestoreSafety\RestoreSafetyResolver;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
@ -18,13 +21,44 @@
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
ensureDefaultProviderConnection($tenant, 'microsoft');
|
||||
|
||||
$policy = Policy::create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'external_id' => 'policy-1',
|
||||
'policy_type' => 'deviceCompliancePolicy',
|
||||
'display_name' => 'Bitlocker Require',
|
||||
'platform' => 'all',
|
||||
]);
|
||||
$backupSet = BackupSet::factory()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'name' => 'Preview productization backup',
|
||||
'status' => 'completed',
|
||||
'item_count' => 1,
|
||||
]);
|
||||
$backupItem = BackupItem::factory()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'policy_id' => (int) $policy->getKey(),
|
||||
'policy_identifier' => $policy->external_id,
|
||||
'policy_type' => $policy->policy_type,
|
||||
'platform' => $policy->platform,
|
||||
'payload' => [
|
||||
'id' => $policy->external_id,
|
||||
'displayName' => $policy->display_name,
|
||||
'settings' => [],
|
||||
],
|
||||
'metadata' => [
|
||||
'displayName' => $policy->display_name,
|
||||
],
|
||||
'assignments' => [],
|
||||
]);
|
||||
|
||||
/** @var RestoreSafetyResolver $resolver */
|
||||
$resolver = app(RestoreSafetyResolver::class);
|
||||
|
||||
$data = [
|
||||
'backup_set_id' => 10,
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'scope_mode' => 'selected',
|
||||
'backup_item_ids' => [1],
|
||||
'backup_item_ids' => [(int) $backupItem->getKey()],
|
||||
'group_mapping' => [],
|
||||
'check_summary' => ['blocking' => 0, 'warning' => 0, 'safe' => 1],
|
||||
'check_results' => [['code' => 'safe', 'severity' => 'safe']],
|
||||
@ -64,12 +98,27 @@
|
||||
->test(CreateRestoreRun::class)
|
||||
->set('data', $data)
|
||||
->goToWizardStep(4)
|
||||
->assertSeeText('Review the preview and complete confirmation before execution can be queued.')
|
||||
->assertSeeText('Review the preview and continue to confirmation.')
|
||||
->assertDontSeeText('Review prerequisites before execution.')
|
||||
->assertSeeText('Policy change preview')
|
||||
->assertSeeText('Preview evidence')
|
||||
->assertSeeText('View safety gates and restore proof')
|
||||
->assertSeeText('Preview details')
|
||||
->assertSeeText('No changes detected')
|
||||
->assertSeeText('All reviewed items')
|
||||
->assertSeeText('BitLocker Require')
|
||||
->assertSeeText('No policy changes')
|
||||
->assertSeeText('Policy diff')
|
||||
->assertSeeText('Assignments')
|
||||
->assertSeeText('Scope tags')
|
||||
->assertSeeText('Review reason')
|
||||
->assertSeeText('No changes detected.')
|
||||
->assertSeeText('No action needed')
|
||||
->assertSeeHtml('data-testid="restore-run-preview-review-cards"')
|
||||
->assertSeeHtml('data-testid="restore-run-preview-review-table"')
|
||||
->assertSeeHtml('data-diff-part="unchanged"')
|
||||
->assertSeeText('1 policy reviewed')
|
||||
->assertDontSeeText('Policy change preview')
|
||||
->assertDontSeeText('What the preview proves')
|
||||
->assertDontSeeText('deviceCompliancePolicy')
|
||||
->assertDontSeeText('deviceCompliancePolicy • all');
|
||||
});
|
||||
|
||||
@ -35,7 +35,6 @@ function makeAssignment(string $odataType, string $groupId, ?string $displayName
|
||||
])
|
||||
->get(RestoreRunResource::getUrl('create', panel: 'admin', tenant: $tenant))
|
||||
->assertOk()
|
||||
->assertSee('Create restore run')
|
||||
->assertSee('Select Backup Set');
|
||||
});
|
||||
|
||||
@ -72,7 +71,8 @@ function makeAssignment(string $odataType, string $groupId, ?string $displayName
|
||||
->get($url)
|
||||
->assertOk()
|
||||
->assertSee('Example Group')
|
||||
->assertSee('Source ID: '.$groupId)
|
||||
->assertSee('Source ID:')
|
||||
->assertSee($groupId)
|
||||
->assertSee('Resolve target mappings')
|
||||
->assertSee('Select a target group from the directory cache or enter a target group object ID as a fallback.')
|
||||
->assertDontSee('Paste the target Entra ID group Object ID')
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
use App\Filament\Resources\RestoreRunResource;
|
||||
use App\Filament\Resources\RestoreRunResource\Pages\CreateRestoreRun;
|
||||
use App\Filament\Resources\RestoreRunResource\Presenters\RestoreRunCreatePresenter;
|
||||
use App\Models\BackupItem;
|
||||
use App\Models\BackupSet;
|
||||
use App\Models\EntraGroup;
|
||||
@ -168,6 +169,7 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'scope_mode' => 'selected',
|
||||
'backup_item_ids' => [(int) $backupItem->getKey()],
|
||||
'is_dry_run' => true,
|
||||
'group_mapping' => [],
|
||||
'check_summary' => ['blocking' => 0, 'warning' => 0, 'safe' => 1],
|
||||
'check_results' => [['code' => 'safe', 'severity' => 'safe']],
|
||||
@ -221,7 +223,7 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
->toContain('Spec 332 was reconciled from the narrower `specs/332-restore-run-preview-productization` path');
|
||||
});
|
||||
|
||||
it('renders the full product process flow on step 1 for a usable backup source', function (): void {
|
||||
it('renders compact safety evidence on step 1 for a usable backup source', function (): void {
|
||||
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
||||
[$backupSet] = spec332UsableBackupFixture($tenant);
|
||||
|
||||
@ -231,13 +233,16 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
])
|
||||
->assertSee('Restore Safety')
|
||||
->assertSee('Backup quality summary')
|
||||
->assertSee('Safety evidence')
|
||||
->assertSee('View safety gates and proof')
|
||||
->assertSee('View quality caveat and detail')
|
||||
->assertSee('Restore safety gates')
|
||||
->assertSee('Restore Proof')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertSee('Input quality signals do not prove that execution is safe or that recovery is verified.')
|
||||
->assertSee('A usable source backup is selected for this restore draft.')
|
||||
->assertSee('Continue to scope and resolve required mappings.')
|
||||
->assertSee('Validate impact before execution.')
|
||||
->assertSee('Continue to scope.')
|
||||
->assertSee('Validate impact.')
|
||||
->assertSee('This create flow does not prove recoverability before execution and post-run evidence exist.')
|
||||
->assertDontSee('Technical startability')
|
||||
->assertDontSee('write-gate')
|
||||
@ -247,7 +252,9 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
->assertSeeHtml('data-step-label="Usable source selected"')
|
||||
->assertSeeHtml('data-proof-label="Operation proof"');
|
||||
|
||||
expect($component->html())->toContain('data-testid="restore-run-process-flow-full"');
|
||||
expect($component->html())
|
||||
->toContain('data-testid="restore-run-safety-evidence"')
|
||||
->not->toContain('data-testid="restore-run-process-flow-full"');
|
||||
});
|
||||
|
||||
it('does not mark usable source as complete when the backup has no captured items', function (): void {
|
||||
@ -263,7 +270,7 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
->assertSeeHtml('data-step-status="required"');
|
||||
});
|
||||
|
||||
it('renders compact restore safety status on step 2 while keeping restore proof visible', function (): void {
|
||||
it('renders compact restore evidence on step 2 without a separate proof rail', function (): void {
|
||||
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
||||
[$backupSet] = spec332UsableBackupFixture($tenant);
|
||||
|
||||
@ -273,15 +280,14 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
])
|
||||
->goToNextWizardStep()
|
||||
->assertWizardCurrentStep(2)
|
||||
->assertSee('Restore safety status')
|
||||
->assertSee('2/7 gates complete')
|
||||
->assertSee('View safety gates')
|
||||
->assertSee('Restore Proof')
|
||||
->assertSee('Restore evidence')
|
||||
->assertSee('3/7 gates complete')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertSee('Requested by')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertSeeHtml('data-testid="restore-run-process-flow-compact"');
|
||||
->assertSeeHtml('data-testid="restore-run-safety-evidence"');
|
||||
|
||||
expect($component->html())->toContain('data-testid="restore-run-process-flow-compact"');
|
||||
expect($component->html())->toContain('data-testid="restore-run-safety-evidence"');
|
||||
});
|
||||
|
||||
it('keeps group mapping details collapsed by default on step 2 until the resolver is opened explicitly', function (): void {
|
||||
@ -295,14 +301,16 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
->goToNextWizardStep()
|
||||
->assertWizardCurrentStep(2)
|
||||
->assertSee('Scope summary')
|
||||
->assertSee('Resolve mappings')
|
||||
->assertSee('1 mapping required')
|
||||
->assertSee('Validation blocked')
|
||||
->assertSee('Resolve target mappings')
|
||||
->assertSee('Restore safety status')
|
||||
->assertSee('Restore Proof')
|
||||
->assertSee('Restore evidence')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertSee('0 of 1 mappings resolved')
|
||||
->assertSee('1 unresolved')
|
||||
->assertSee('0 skipped')
|
||||
->assertSee('Resolve required mappings before validation can run.')
|
||||
->assertSee('Target mapping requirements')
|
||||
->assertSee('Select a target group from the directory cache or enter a target group object ID as a fallback. Required mappings must be resolved before validation can run.')
|
||||
->assertDontSee('Paste the target Entra ID group Object ID (GUID).');
|
||||
|
||||
@ -335,7 +343,8 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
],
|
||||
])
|
||||
->assertSee('Target group: Spec332 Cached Target Group')
|
||||
->assertSee('Target ID: '.$targetGroupId);
|
||||
->assertSee('Target ID:')
|
||||
->assertSee($targetGroupId);
|
||||
});
|
||||
|
||||
it('labels manual GUID mapping as a manual fallback and counts it in the resolver summary', function (): void {
|
||||
@ -360,7 +369,7 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
->assertSee('0 unresolved')
|
||||
->assertSee('0 skipped')
|
||||
->assertSee('1 manual fallback')
|
||||
->assertSee('Manual target object ID')
|
||||
->assertSee('Manual fallback target object ID')
|
||||
->assertSee('Badge: Manual fallback');
|
||||
});
|
||||
|
||||
@ -477,10 +486,14 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
->goToNextWizardStep()
|
||||
->goToNextWizardStep()
|
||||
->assertWizardCurrentStep(3)
|
||||
->assertSee('Validation decision')
|
||||
->assertSee('Validation blocked')
|
||||
->assertSee('Provider access must be repaired before restore checks can run.')
|
||||
->assertSee('Provider credentials are not available for this environment.')
|
||||
->assertSee('Restore checks cannot run until the provider connection is repaired.')
|
||||
->assertSee('Repair the provider connection before validation can run.')
|
||||
->assertSee('Review provider connection')
|
||||
->assertFormComponentActionDoesNotExist('check_results', 'run_restore_checks')
|
||||
->assertDontSee('Provider credentials are missing');
|
||||
|
||||
$component
|
||||
@ -491,6 +504,34 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
expect($component->html())->not->toContain('Exception');
|
||||
});
|
||||
|
||||
it('renders compact validation evidence on step 3 after checks pass', function (): void {
|
||||
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
||||
[$backupSet, $backupItem] = spec332UsableBackupFixture($tenant);
|
||||
$data = spec332CurrentPreviewData($backupSet, $backupItem);
|
||||
|
||||
$component = spec332WizardComponent($user, $tenant)
|
||||
->set('data', $data)
|
||||
->goToWizardStep(3)
|
||||
->assertWizardCurrentStep(3)
|
||||
->assertSee('Validation decision')
|
||||
->assertSee('Validation passed')
|
||||
->assertSee('Blockers')
|
||||
->assertSee('Warnings')
|
||||
->assertSee('Safe')
|
||||
->assertSee('View 1 safe check detail')
|
||||
->assertSee('Validation evidence')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertSee('Checks are current for this scope. Rerun only after scope or mapping changes.')
|
||||
->assertDontSee('No checks have been recorded for this scope yet.')
|
||||
->assertDontSee('Run checks after defining scope and mapping missing groups.');
|
||||
|
||||
$html = $component->html();
|
||||
|
||||
expect($html)
|
||||
->toContain('data-testid="restore-run-safe-checks-details"')
|
||||
->toContain('data-testid="restore-run-safety-evidence"');
|
||||
});
|
||||
|
||||
it('keeps preview decision-first while showing compact safety status and restore proof', function (): void {
|
||||
[$user, $tenant] = spec332ProductProcessFlowTenant();
|
||||
[$backupSet, $backupItem] = spec332UsableBackupFixture($tenant);
|
||||
@ -500,10 +541,10 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
->set('data', $data)
|
||||
->goToWizardStep(4)
|
||||
->assertWizardCurrentStep(4)
|
||||
->assertSee('Review the preview and complete confirmation before execution can be queued.')
|
||||
->assertSee('Review the preview and continue to confirmation.')
|
||||
->assertDontSee('Review prerequisites before execution.')
|
||||
->assertSee('Restore safety status')
|
||||
->assertSee('Next gate:')
|
||||
->assertSee('Preview evidence')
|
||||
->assertSee('View safety gates and restore proof')
|
||||
->assertSee('Confirmation')
|
||||
->assertSee('Restore Proof')
|
||||
->assertSee('Operation proof')
|
||||
@ -511,6 +552,14 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertDontSee('spec332 raw payload should stay hidden')
|
||||
->assertDontSee('tenant-wide recovery is proven');
|
||||
|
||||
$html = $component->html();
|
||||
|
||||
expect($html)
|
||||
->toContain('data-testid="restore-run-preview-evidence"');
|
||||
|
||||
expect(preg_match('/<details[^>]*data-testid="restore-run-preview-evidence-details"[^>]*>/', $html, $matches))->toBe(1);
|
||||
expect($matches[0])->not->toContain(' open');
|
||||
});
|
||||
|
||||
it('keeps confirm step locked when execution prerequisites are unavailable', function (): void {
|
||||
@ -524,16 +573,26 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
[$backupSet, $backupItem] = spec332UsableBackupFixture($tenant);
|
||||
$data = spec332CurrentPreviewData($backupSet, $backupItem);
|
||||
|
||||
$contract = RestoreRunCreatePresenter::contract($data, 4, true, $tenant, $user);
|
||||
|
||||
expect(data_get($contract, 'previewSummary.primaryNextActionLabel'))
|
||||
->toBe('Review preview and continue to confirmation; resolve execution prerequisites before executing.')
|
||||
->and(data_get($contract, 'wizard_gate.next_gate_label'))->toBe('Confirmation required')
|
||||
->and(data_get($contract, 'wizard_gate.can_continue'))->toBeTrue()
|
||||
->and(data_get($contract, 'wizard_gate.can_execute'))->toBeFalse();
|
||||
|
||||
$component = spec332WizardComponent($user, $tenant)
|
||||
->set('data', $data)
|
||||
->goToWizardStep(5)
|
||||
->assertWizardCurrentStep(5)
|
||||
->assertSee('Confirmation summary')
|
||||
->assertSee('Execution prerequisites blocked')
|
||||
->assertSee('Create preview-only run')
|
||||
->assertSee('Create a preview-only run, or resolve execution prerequisites before queueing real execution.')
|
||||
->assertSee('Execution')
|
||||
->assertSee('Unavailable')
|
||||
->assertSee('Review prerequisites before execution.')
|
||||
->assertSee('Restore execution is blocked until required prerequisites are healthy again. Evidence does not exist yet.')
|
||||
->assertSee('Confirmation does not claim recovery.')
|
||||
->assertSee('Execution unavailable until prerequisites are resolved')
|
||||
->assertSee('Restore execution is blocked until required prerequisites are available. Evidence does not exist yet.')
|
||||
->assertSee('Recovery is not verified until post-run evidence exists.')
|
||||
->assertFormFieldDisabled('is_dry_run');
|
||||
|
||||
expect($component->html())->not->toContain('Operation proof is complete');
|
||||
@ -549,8 +608,11 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
->goToWizardStep(5)
|
||||
->assertWizardCurrentStep(5)
|
||||
->assertSee('Confirmation summary')
|
||||
->assertSee('Preview-only run ready')
|
||||
->assertSee('Create preview-only run')
|
||||
->assertSee('Create a preview-only restore run.')
|
||||
->assertSee('Execution')
|
||||
->assertSee('Available after confirmation')
|
||||
->assertSee('Confirmation does not claim recovery.')
|
||||
->assertSee('Execution unavailable until confirmation')
|
||||
->assertSee('Recovery is not verified until post-run evidence exists.')
|
||||
->assertFormFieldEnabled('is_dry_run');
|
||||
});
|
||||
|
||||
@ -0,0 +1,521 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Resources\RestoreRunResource;
|
||||
use App\Filament\Resources\RestoreRunResource\Pages\CreateRestoreRun;
|
||||
use App\Filament\Resources\RestoreRunResource\Presenters\RestoreRunCreatePresenter;
|
||||
use App\Models\BackupItem;
|
||||
use App\Models\BackupSet;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\Policy;
|
||||
use App\Support\RestoreSafety\RestoreSafetyResolver;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function spec333RestoreCreateTenant(bool $credentialAvailable = true): array
|
||||
{
|
||||
$tenant = ManagedEnvironment::factory()->create([
|
||||
'rbac_status' => 'ok',
|
||||
'rbac_last_checked_at' => now(),
|
||||
]);
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
ensureDefaultProviderConnection($tenant, 'microsoft', ensureCredential: $credentialAvailable);
|
||||
|
||||
return [$user, $tenant];
|
||||
}
|
||||
|
||||
function spec333RestoreCreateComponent($user, ManagedEnvironment $tenant): \Livewire\Features\SupportTesting\Testable
|
||||
{
|
||||
setAdminPanelContext($tenant);
|
||||
|
||||
return Livewire::actingAs($user)->test(CreateRestoreRun::class);
|
||||
}
|
||||
|
||||
function spec333RestoreCreateBackupFixture(ManagedEnvironment $tenant, array $overrides = []): array
|
||||
{
|
||||
$policy = Policy::create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'external_id' => $overrides['external_id'] ?? 'spec333-policy',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'display_name' => $overrides['display_name'] ?? 'Spec333 Restore Policy',
|
||||
'platform' => 'windows',
|
||||
]);
|
||||
|
||||
$backupSet = BackupSet::factory()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'name' => $overrides['backup_name'] ?? 'Spec333 Restore Backup',
|
||||
'status' => 'completed',
|
||||
'item_count' => 1,
|
||||
]);
|
||||
|
||||
$backupItem = BackupItem::factory()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'policy_id' => (int) $policy->getKey(),
|
||||
'policy_identifier' => $policy->external_id,
|
||||
'policy_type' => $policy->policy_type,
|
||||
'platform' => $policy->platform,
|
||||
'payload' => $overrides['payload'] ?? [
|
||||
'id' => $policy->external_id,
|
||||
'displayName' => $policy->display_name,
|
||||
'settings' => ['encryption' => 'required'],
|
||||
],
|
||||
'metadata' => $overrides['metadata'] ?? [
|
||||
'displayName' => $policy->display_name,
|
||||
],
|
||||
'assignments' => $overrides['assignments'] ?? [],
|
||||
]);
|
||||
|
||||
return [$backupSet, $backupItem, $policy];
|
||||
}
|
||||
|
||||
function spec333RestoreCreateEmptyBackup(ManagedEnvironment $tenant): BackupSet
|
||||
{
|
||||
return BackupSet::factory()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'name' => 'Spec333 Empty Backup',
|
||||
'status' => 'completed',
|
||||
'item_count' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
function spec333RestoreCreateCurrentData(BackupSet $backupSet, BackupItem $backupItem): array
|
||||
{
|
||||
/** @var RestoreSafetyResolver $resolver */
|
||||
$resolver = app(RestoreSafetyResolver::class);
|
||||
|
||||
$data = [
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'scope_mode' => 'selected',
|
||||
'backup_item_ids' => [(int) $backupItem->getKey()],
|
||||
'is_dry_run' => true,
|
||||
'group_mapping' => [],
|
||||
'check_summary' => ['blocking' => 0, 'warning' => 1, 'safe' => 2],
|
||||
'check_results' => [
|
||||
['code' => 'warning', 'severity' => 'warning', 'message' => 'Review assignment changes before execution.'],
|
||||
['code' => 'safe', 'severity' => 'safe', 'message' => 'Scope fingerprint is current.'],
|
||||
],
|
||||
'checks_ran_at' => now('UTC')->toIso8601String(),
|
||||
'preview_summary' => [
|
||||
'generated_at' => now('UTC')->toIso8601String(),
|
||||
'policies_total' => 2,
|
||||
'policies_changed' => 1,
|
||||
'assignments_changed' => 1,
|
||||
'scope_tags_changed' => 0,
|
||||
'raw_payload_marker' => 'spec333 raw payload should stay hidden',
|
||||
],
|
||||
'preview_diffs' => [
|
||||
[
|
||||
'policy_identifier' => 'spec333-policy',
|
||||
'display_name' => 'Spec333 Restore Policy',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'platform' => 'windows',
|
||||
'action' => 'update',
|
||||
'assignments_changed' => true,
|
||||
'scope_tags_changed' => false,
|
||||
'diff' => [
|
||||
'summary' => ['added' => 0, 'removed' => 0, 'changed' => 1],
|
||||
'changed' => ['setting' => ['before' => 'off', 'after' => 'on']],
|
||||
'added' => [],
|
||||
'removed' => [],
|
||||
],
|
||||
],
|
||||
[
|
||||
'policy_identifier' => 'spec333-policy-unchanged',
|
||||
'display_name' => 'Spec333 Unchanged Policy',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'platform' => 'windows',
|
||||
'action' => 'update',
|
||||
'assignments_changed' => false,
|
||||
'scope_tags_changed' => false,
|
||||
'diff' => [
|
||||
'summary' => ['added' => 0, 'removed' => 0, 'changed' => 0],
|
||||
'changed' => [],
|
||||
'added' => [],
|
||||
'removed' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
'preview_ran_at' => now('UTC')->toIso8601String(),
|
||||
];
|
||||
|
||||
$data['check_basis'] = $resolver->checksBasisFromData($data);
|
||||
$data['preview_basis'] = $resolver->previewBasisFromData($data);
|
||||
|
||||
return RestoreRunResource::synchronizeRestoreSafetyDraft($data);
|
||||
}
|
||||
|
||||
it('renders source states with product-safe next actions', function (): void {
|
||||
[$user, $tenant] = spec333RestoreCreateTenant();
|
||||
[$usableBackup] = spec333RestoreCreateBackupFixture($tenant);
|
||||
$emptyBackup = spec333RestoreCreateEmptyBackup($tenant);
|
||||
|
||||
spec333RestoreCreateComponent($user, $tenant)
|
||||
->assertSee('Source required')
|
||||
->assertSee('Select backup set.')
|
||||
->assertSee('Restore safety cannot be judged until a source backup is selected.');
|
||||
|
||||
spec333RestoreCreateComponent($user, $tenant)
|
||||
->fillForm(['backup_set_id' => (int) $emptyBackup->getKey()])
|
||||
->assertSee('Source not usable')
|
||||
->assertSee('Select another backup set.')
|
||||
->assertDontSee('Source unavailable');
|
||||
|
||||
spec333RestoreCreateComponent($user, $tenant)
|
||||
->fillForm(['backup_set_id' => (int) $usableBackup->getKey()])
|
||||
->assertSee('Source selected')
|
||||
->assertSee('Continue to scope.')
|
||||
->assertSee('A usable source backup is selected for this restore draft.')
|
||||
->assertSee('A usable source backup is selected.')
|
||||
->assertSee('Available')
|
||||
->assertSee('No degradations were detected across 1 captured item.')
|
||||
->assertSee('Items captured')
|
||||
->assertSee('View quality caveat and detail')
|
||||
->assertSee('Safety evidence')
|
||||
->assertSee('View safety gates and proof')
|
||||
->assertDontSee('Backup quality summary Unavailable')
|
||||
->assertDontSee('Operation proof is complete')
|
||||
->assertDontSee('recovery verified');
|
||||
});
|
||||
|
||||
it('keeps scope summary first and mapping resolution gated before validation', function (): void {
|
||||
[$user, $tenant] = spec333RestoreCreateTenant();
|
||||
[$backupSet] = spec333RestoreCreateBackupFixture($tenant, [
|
||||
'assignments' => [[
|
||||
'target' => [
|
||||
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
|
||||
'groupId' => '11111111-1111-1111-1111-111111111111',
|
||||
'group_display_name' => 'Spec333 Missing Group',
|
||||
],
|
||||
]],
|
||||
]);
|
||||
|
||||
spec333RestoreCreateComponent($user, $tenant)
|
||||
->fillForm(['backup_set_id' => (int) $backupSet->getKey()])
|
||||
->goToNextWizardStep()
|
||||
->assertWizardCurrentStep(2)
|
||||
->assertSee('Scope summary')
|
||||
->assertSee('All items (default)')
|
||||
->assertSee('1 mapping required')
|
||||
->assertSee('Validation blocked')
|
||||
->assertSee('Resolve target mappings')
|
||||
->assertSee('Resolve required mappings before validation can run.')
|
||||
->assertSee('Restore evidence')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertSee('Scope/dependency mapping')
|
||||
->assertDontSee('Safety checks completed');
|
||||
});
|
||||
|
||||
it('shows provider credential blockers as validation blocked without raw failures', function (): void {
|
||||
[$user, $tenant] = spec333RestoreCreateTenant(credentialAvailable: false);
|
||||
[$backupSet] = spec333RestoreCreateBackupFixture($tenant);
|
||||
|
||||
spec333RestoreCreateComponent($user, $tenant)
|
||||
->fillForm(['backup_set_id' => (int) $backupSet->getKey()])
|
||||
->goToNextWizardStep()
|
||||
->goToNextWizardStep()
|
||||
->assertWizardCurrentStep(3)
|
||||
->assertSee('Validation decision')
|
||||
->assertSee('Validation blocked')
|
||||
->assertSee('Provider access must be repaired before restore checks can run.')
|
||||
->assertSee('Provider credentials are not available for this environment.')
|
||||
->assertSee('Restore checks cannot run until the provider connection is repaired.')
|
||||
->assertSee('Repair the provider connection before validation can run.')
|
||||
->assertSee('Review provider connection')
|
||||
->assertFormComponentActionDoesNotExist('check_results', 'run_restore_checks')
|
||||
->assertDontSee('Graph works again')
|
||||
->assertDontSee('write-gate')
|
||||
->assertDontSee('raw provider credential exception');
|
||||
});
|
||||
|
||||
it('renders passed validation as a compact decision with safe details collapsed', function (): void {
|
||||
[$user, $tenant] = spec333RestoreCreateTenant();
|
||||
[$backupSet, $backupItem] = spec333RestoreCreateBackupFixture($tenant);
|
||||
|
||||
/** @var RestoreSafetyResolver $resolver */
|
||||
$resolver = app(RestoreSafetyResolver::class);
|
||||
|
||||
$data = [
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'scope_mode' => 'selected',
|
||||
'backup_item_ids' => [(int) $backupItem->getKey()],
|
||||
'group_mapping' => [],
|
||||
'check_summary' => ['blocking' => 0, 'warning' => 0, 'safe' => 7],
|
||||
'check_results' => collect(range(1, 7))
|
||||
->map(fn (int $index): array => [
|
||||
'code' => 'spec333-safe-'.$index,
|
||||
'severity' => 'safe',
|
||||
'title' => 'Spec333 safe check '.$index,
|
||||
'message' => 'Spec333 safe validation detail '.$index.'.',
|
||||
])
|
||||
->all(),
|
||||
'checks_ran_at' => now('UTC')->toIso8601String(),
|
||||
];
|
||||
$data['check_basis'] = $resolver->checksBasisFromData($data);
|
||||
$data = RestoreRunResource::synchronizeRestoreSafetyDraft($data);
|
||||
|
||||
$component = spec333RestoreCreateComponent($user, $tenant)
|
||||
->set('data', $data)
|
||||
->goToWizardStep(3)
|
||||
->assertWizardCurrentStep(3)
|
||||
->assertSee('Validation decision')
|
||||
->assertSee('Validation passed')
|
||||
->assertSee('Blockers')
|
||||
->assertSee('Warnings')
|
||||
->assertSee('Safe')
|
||||
->assertSee('View 7 safe check details')
|
||||
->assertSee('Prerequisites available')
|
||||
->assertSee('Generate a preview for the current scope before confirmation.')
|
||||
->assertSee('Checks are current for this scope. Rerun only after scope or mapping changes.')
|
||||
->assertSee('Validation evidence')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertDontSee('No checks have been recorded for this scope yet.')
|
||||
->assertDontSee('Run checks after defining scope and mapping missing groups.');
|
||||
|
||||
$html = $component->html();
|
||||
|
||||
expect(preg_match('/<details[^>]*data-testid="restore-run-safe-checks-details"[^>]*>/', $html, $matches))->toBe(1);
|
||||
expect($matches[0])->not->toContain(' open');
|
||||
});
|
||||
|
||||
it('renders preview groups and proof-safe confirmation copy', function (): void {
|
||||
[$user, $tenant] = spec333RestoreCreateTenant();
|
||||
[$backupSet, $backupItem] = spec333RestoreCreateBackupFixture($tenant);
|
||||
$data = spec333RestoreCreateCurrentData($backupSet, $backupItem);
|
||||
|
||||
spec333RestoreCreateComponent($user, $tenant)
|
||||
->set('data', $data)
|
||||
->goToWizardStep(4)
|
||||
->assertWizardCurrentStep(4)
|
||||
->assertSee('Preview evidence')
|
||||
->assertSee('Review the preview and continue to confirmation.')
|
||||
->assertSee('View safety gates and restore proof')
|
||||
->assertSee('Policies reviewed')
|
||||
->assertSee('Requires review')
|
||||
->assertSee('Validation warnings')
|
||||
->assertSee('Needs attention')
|
||||
->assertSee('Changes detected')
|
||||
->assertSee('No changes detected')
|
||||
->assertSee('All reviewed items')
|
||||
->assertSee('Spec333 Restore Policy')
|
||||
->assertSee('Spec333 Unchanged Policy')
|
||||
->assertSee('Restore action')
|
||||
->assertSee('Update existing')
|
||||
->assertSee('Policy diff')
|
||||
->assertSee('0 added')
|
||||
->assertSee('0 removed')
|
||||
->assertSee('1 changed')
|
||||
->assertSee('No policy changes')
|
||||
->assertSee('Assignments')
|
||||
->assertSee('Scope tags')
|
||||
->assertSee('Review reason')
|
||||
->assertSee('Policy settings and assignments differ from current target state.')
|
||||
->assertSee('No changes detected.')
|
||||
->assertSee('Action')
|
||||
->assertSee('Review changes')
|
||||
->assertSee('No action needed')
|
||||
->assertSeeHtml('data-testid="restore-run-preview-review-cards"')
|
||||
->assertSeeHtml('data-testid="restore-run-preview-review-table"')
|
||||
->assertSeeHtml('data-diff-part="added"')
|
||||
->assertSeeHtml('data-diff-part="removed"')
|
||||
->assertSeeHtml('data-diff-part="changed"')
|
||||
->assertSee('1 policy requires review. Use the Review reason column to inspect policy diff, assignments, and scope-tag evidence before confirmation.')
|
||||
->assertDontSee('Policy change preview')
|
||||
->assertDontSee('What the preview proves')
|
||||
->assertDontSee('Review prerequisites before execution.')
|
||||
->assertDontSee('deviceConfiguration • windows')
|
||||
->assertDontSee('spec333 raw payload should stay hidden')
|
||||
->goToWizardStep(5)
|
||||
->assertWizardCurrentStep(5)
|
||||
->assertSee('Preview-only run ready')
|
||||
->assertSee('Create preview-only run')
|
||||
->assertSee('Create a preview-only restore run.')
|
||||
->assertSee('Operation proof is unavailable before execution.')
|
||||
->assertSee('Post-run evidence is unavailable before execution.')
|
||||
->assertSee('Recovery is not verified until post-run evidence exists.')
|
||||
->assertDontSee('Confirmation does not claim recovery.')
|
||||
->assertDontSee('recovery verified');
|
||||
});
|
||||
|
||||
it('derives mapping blockers from the central wizard gate contract', function (): void {
|
||||
[$user, $tenant] = spec333RestoreCreateTenant();
|
||||
[$backupSet] = spec333RestoreCreateBackupFixture($tenant, [
|
||||
'assignments' => [[
|
||||
'target' => [
|
||||
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
|
||||
'groupId' => '22222222-2222-2222-2222-222222222222',
|
||||
'group_display_name' => 'Spec333 Missing Group',
|
||||
],
|
||||
]],
|
||||
]);
|
||||
|
||||
$contract = RestoreRunCreatePresenter::contract(
|
||||
data: [
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'scope_mode' => 'all',
|
||||
'backup_item_ids' => [],
|
||||
'group_mapping' => [],
|
||||
],
|
||||
currentStep: 2,
|
||||
compactFlow: true,
|
||||
tenant: $tenant,
|
||||
user: $user,
|
||||
);
|
||||
|
||||
expect(data_get($contract, 'wizard_gate.next_gate'))->toBe('scope_dependency_mapping')
|
||||
->and(data_get($contract, 'wizard_gate.next_gate_label'))->toBe('Scope/dependency mapping')
|
||||
->and(data_get($contract, 'wizard_gate.can_continue'))->toBeFalse()
|
||||
->and(data_get($contract, 'wizard_gate.required_action_label'))->toBe('Resolve required mappings before validation can run.')
|
||||
->and(data_get($contract, 'processFlow.nextGate'))->toBe(data_get($contract, 'wizard_gate.next_gate_label'));
|
||||
});
|
||||
|
||||
it('derives validation blockers from the central wizard gate contract', function (): void {
|
||||
[$user, $tenant] = spec333RestoreCreateTenant();
|
||||
[$backupSet, $backupItem] = spec333RestoreCreateBackupFixture($tenant);
|
||||
|
||||
/** @var RestoreSafetyResolver $resolver */
|
||||
$resolver = app(RestoreSafetyResolver::class);
|
||||
|
||||
$data = [
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'scope_mode' => 'selected',
|
||||
'backup_item_ids' => [(int) $backupItem->getKey()],
|
||||
'group_mapping' => [],
|
||||
'check_summary' => ['blocking' => 1, 'warning' => 0, 'safe' => 0],
|
||||
'check_results' => [
|
||||
['code' => 'blocking', 'severity' => 'blocking', 'message' => 'Spec333 validation blocker.'],
|
||||
],
|
||||
'checks_ran_at' => now('UTC')->toIso8601String(),
|
||||
];
|
||||
$data['check_basis'] = $resolver->checksBasisFromData($data);
|
||||
$data = RestoreRunResource::synchronizeRestoreSafetyDraft($data);
|
||||
|
||||
$contract = RestoreRunCreatePresenter::contract(
|
||||
data: $data,
|
||||
currentStep: 3,
|
||||
compactFlow: true,
|
||||
tenant: $tenant,
|
||||
user: $user,
|
||||
);
|
||||
|
||||
expect(data_get($contract, 'wizard_gate.next_gate'))->toBe('validation_blocked')
|
||||
->and(data_get($contract, 'wizard_gate.next_gate_label'))->toBe('Validation blocked')
|
||||
->and(data_get($contract, 'wizard_gate.can_continue'))->toBeFalse()
|
||||
->and(data_get($contract, 'wizard_gate.continue_disabled_reason'))->toBe('Resolve blocking validation checks before moving to preview.')
|
||||
->and(data_get($contract, 'validationSummary.nextActionLabel'))->toBe(data_get($contract, 'wizard_gate.required_action_label'));
|
||||
});
|
||||
|
||||
it('derives preview-required and confirmation-required states from the central wizard gate contract', function (): void {
|
||||
[$user, $tenant] = spec333RestoreCreateTenant();
|
||||
[$backupSet, $backupItem] = spec333RestoreCreateBackupFixture($tenant);
|
||||
|
||||
/** @var RestoreSafetyResolver $resolver */
|
||||
$resolver = app(RestoreSafetyResolver::class);
|
||||
|
||||
$validatedData = [
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'scope_mode' => 'selected',
|
||||
'backup_item_ids' => [(int) $backupItem->getKey()],
|
||||
'group_mapping' => [],
|
||||
'check_summary' => ['blocking' => 0, 'warning' => 0, 'safe' => 1],
|
||||
'check_results' => [['code' => 'safe', 'severity' => 'safe']],
|
||||
'checks_ran_at' => now('UTC')->toIso8601String(),
|
||||
];
|
||||
$validatedData['check_basis'] = $resolver->checksBasisFromData($validatedData);
|
||||
$validatedData = RestoreRunResource::synchronizeRestoreSafetyDraft($validatedData);
|
||||
|
||||
$previewRequired = RestoreRunCreatePresenter::contract(
|
||||
data: $validatedData,
|
||||
currentStep: 4,
|
||||
compactFlow: true,
|
||||
tenant: $tenant,
|
||||
user: $user,
|
||||
);
|
||||
|
||||
$previewCurrent = RestoreRunCreatePresenter::contract(
|
||||
data: spec333RestoreCreateCurrentData($backupSet, $backupItem),
|
||||
currentStep: 4,
|
||||
compactFlow: true,
|
||||
tenant: $tenant,
|
||||
user: $user,
|
||||
);
|
||||
|
||||
expect(data_get($previewRequired, 'wizard_gate.next_gate'))->toBe('preview_required')
|
||||
->and(data_get($previewRequired, 'wizard_gate.can_continue'))->toBeFalse()
|
||||
->and(data_get($previewRequired, 'previewSummary.nextGateLabel'))->toBe('Preview required')
|
||||
->and(data_get($previewCurrent, 'wizard_gate.next_gate'))->toBe('confirmation_required')
|
||||
->and(data_get($previewCurrent, 'wizard_gate.next_gate_label'))->toBe('Confirmation required')
|
||||
->and(data_get($previewCurrent, 'wizard_gate.can_continue'))->toBeTrue()
|
||||
->and(data_get($previewCurrent, 'wizard_gate.execution_state'))->toBe('unavailable_until_confirmation')
|
||||
->and(data_get($previewCurrent, 'wizard_gate.confirmation_state_label'))->toBe('Preview-only run ready')
|
||||
->and(data_get($previewCurrent, 'wizard_gate.primary_cta_label'))->toBe('Create preview-only run')
|
||||
->and(data_get($previewCurrent, 'wizard_gate.primary_next_step_label'))->toBe('Create a preview-only restore run.')
|
||||
->and(data_get($previewCurrent, 'previewSummary.nextGateLabel'))->toBe('Confirmation required');
|
||||
});
|
||||
|
||||
it('derives real-execution confirmation CTA state from the central wizard gate contract', function (): void {
|
||||
[$user, $tenant] = spec333RestoreCreateTenant();
|
||||
[$backupSet, $backupItem] = spec333RestoreCreateBackupFixture($tenant);
|
||||
$data = [
|
||||
...spec333RestoreCreateCurrentData($backupSet, $backupItem),
|
||||
'is_dry_run' => false,
|
||||
'acknowledged_impact' => true,
|
||||
'tenant_confirm' => (string) ($tenant->name ?? $tenant->managed_environment_id ?? $tenant->external_id ?? $tenant->getKey()),
|
||||
];
|
||||
|
||||
$contract = RestoreRunCreatePresenter::contract(
|
||||
data: $data,
|
||||
currentStep: 5,
|
||||
compactFlow: true,
|
||||
tenant: $tenant,
|
||||
user: $user,
|
||||
);
|
||||
|
||||
expect(data_get($contract, 'wizard_gate.confirmation_state_label'))->toBe('Execution ready to queue')
|
||||
->and(data_get($contract, 'wizard_gate.primary_cta_label'))->toBe('Queue restore execution')
|
||||
->and(data_get($contract, 'wizard_gate.primary_next_step_label'))->toBe('Queue restore execution.')
|
||||
->and(data_get($contract, 'wizard_gate.real_execution_confirmation_satisfied'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows confirmation review when preview is current but execution prerequisites are unavailable', function (): void {
|
||||
[$user, $tenant] = spec333RestoreCreateTenant(credentialAvailable: false);
|
||||
[$backupSet, $backupItem] = spec333RestoreCreateBackupFixture($tenant);
|
||||
$data = spec333RestoreCreateCurrentData($backupSet, $backupItem);
|
||||
|
||||
$contract = RestoreRunCreatePresenter::contract(
|
||||
data: $data,
|
||||
currentStep: 4,
|
||||
compactFlow: true,
|
||||
tenant: $tenant,
|
||||
user: $user,
|
||||
);
|
||||
|
||||
expect(data_get($contract, 'wizard_gate.next_gate_label'))->toBe('Confirmation required')
|
||||
->and(data_get($contract, 'wizard_gate.can_continue'))->toBeTrue()
|
||||
->and(data_get($contract, 'wizard_gate.can_execute'))->toBeFalse()
|
||||
->and(data_get($contract, 'wizard_gate.execution_state'))->toBe('unavailable_until_prerequisites')
|
||||
->and(data_get($contract, 'wizard_gate.confirmation_state_label'))->toBe('Execution prerequisites blocked')
|
||||
->and(data_get($contract, 'wizard_gate.primary_cta_label'))->toBe('Create preview-only run')
|
||||
->and(data_get($contract, 'wizard_gate.primary_next_step_label'))->toBe('Create a preview-only run, or resolve execution prerequisites before queueing real execution.')
|
||||
->and(data_get($contract, 'previewSummary.canProceedToConfirm'))->toBeTrue();
|
||||
|
||||
spec333RestoreCreateComponent($user, $tenant)
|
||||
->set('data', $data)
|
||||
->goToWizardStep(4)
|
||||
->assertWizardCurrentStep(4)
|
||||
->assertSee('Confirmation required')
|
||||
->assertSee('Execution unavailable until prerequisites are resolved')
|
||||
->assertSee('Review preview and continue to confirmation; resolve execution prerequisites before executing.')
|
||||
->assertDontSee('Execution blocked')
|
||||
->goToNextWizardStep()
|
||||
->assertWizardCurrentStep(5)
|
||||
->assertSee('Confirmation summary')
|
||||
->assertSee('Execution prerequisites blocked')
|
||||
->assertSee('Create preview-only run')
|
||||
->assertSee('Execution unavailable until prerequisites are resolved')
|
||||
->assertSee('Create a preview-only run, or resolve execution prerequisites before queueing real execution.')
|
||||
->assertFormFieldDisabled('is_dry_run');
|
||||
});
|
||||
@ -66,7 +66,9 @@
|
||||
|
||||
expect($firstSummary)
|
||||
->toBeString()
|
||||
->toContain('does not contain a usable captured item yet');
|
||||
->toContain('does not contain a usable captured item yet')
|
||||
->and(data_get($first, 'decisionCard.status'))->toBe('Source not usable')
|
||||
->and(data_get($first, 'decisionCard.nextAction'))->toBe('Select another backup set.');
|
||||
|
||||
$backupItem->update([
|
||||
'payload' => [
|
||||
@ -92,7 +94,15 @@
|
||||
expect($secondSummary)
|
||||
->toBeString()
|
||||
->toContain('A usable source backup is selected for this restore draft.')
|
||||
->not->toContain('does not contain a usable captured item yet');
|
||||
->not->toContain('does not contain a usable captured item yet')
|
||||
->and(data_get($second, 'decisionCard.status'))->toBe('Source selected')
|
||||
->and(data_get($second, 'decisionCard.reason'))->toBe('A usable source backup is selected.')
|
||||
->and(data_get($second, 'decisionCard.impact'))->toBe('Scope, validation, and preview must still prove this draft before confirmation or real execution.')
|
||||
->and(data_get($second, 'decisionCard.tone'))->toBe('success')
|
||||
->and(data_get($second, 'decisionCard.nextAction'))->toBe('Continue to scope.')
|
||||
->and(data_get($second, 'backupQualityCard.status'))->toBe('Available')
|
||||
->and(data_get($second, 'backupQualityCard.tone'))->toBe('success')
|
||||
->and(data_get($second, 'backupQualityCard.summary'))->toBe('No degradations were detected across 1 captured item.');
|
||||
});
|
||||
|
||||
it('does not leak presenter state between independent restore draft contracts', function (): void {
|
||||
@ -146,7 +156,8 @@
|
||||
|
||||
expect(data_get($first, 'processFlow.steps.0.summary'))
|
||||
->toBeString()
|
||||
->toContain('does not contain a usable captured item yet');
|
||||
->toContain('does not contain a usable captured item yet')
|
||||
->and(data_get($first, 'decisionCard.status'))->toBe('Source not usable');
|
||||
|
||||
$usableBackup = BackupSet::factory()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
@ -189,5 +200,6 @@
|
||||
expect(data_get($second, 'processFlow.steps.0.summary'))
|
||||
->toBeString()
|
||||
->toContain('A usable source backup is selected for this restore draft.')
|
||||
->not->toContain('does not contain a usable captured item yet');
|
||||
->not->toContain('does not contain a usable captured item yet')
|
||||
->and(data_get($second, 'decisionCard.status'))->toBe('Source selected');
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 299 KiB After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 261 KiB |
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 276 KiB |
@ -0,0 +1,300 @@
|
||||
# Restore Preview Step Audit - Spec 333
|
||||
|
||||
## Executive Summary
|
||||
- readiness: not ready
|
||||
- top findings:
|
||||
- P0: Step 04 can show `Next gate: Confirmation` while execution readiness is unavailable and `canProceedToConfirm` is false. Browser verification confirmed `Next` stays on Step 04 and raises `Execution blocked`, so progression is protected, but the visible gate is contradictory.
|
||||
- P1: Preview rows do not carry an explicit review reason. Operators must infer why review is needed from generic row chips.
|
||||
- P1: Assignment and scope-tag preview data is repo-backed only as booleans and aggregate counts, not as detailed diffs.
|
||||
- P2: `Next` remains visually enabled before preview and after blocked execution readiness; enforcement happens through the wizard validation hook.
|
||||
- P2: Preview copy still exposes implementation terms such as `normalized diff` and uses preview notifications that say items "will be updated during execution."
|
||||
- recommended implementation strategy: keep changes narrow in the presenter and preview Blade. Fix gate wording/action state first, then add a derived `review_reason` row contract from already-available preview fields. Do not add new persisted entities for assignment or scope-tag details until the diff generator can produce them.
|
||||
|
||||
## Repo Safety
|
||||
- command snapshot:
|
||||
- `git status --short --branch`
|
||||
- `git diff --name-only`
|
||||
- branch: `333-restore-create-ux-final-productization`
|
||||
- base commit recorded before audit: `3bbea1bd feat: productize restore wizard preview safety gates and process flow (#399)`
|
||||
- dirty files existed before this audit and were treated as protected user/agent work.
|
||||
- runtime files that looked related to Spec 333 and were not edited by this audit:
|
||||
- `apps/platform/app/Filament/Resources/RestoreRunResource.php`
|
||||
- `apps/platform/app/Filament/Resources/RestoreRunResource/Pages/CreateRestoreRun.php`
|
||||
- `apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php`
|
||||
- `apps/platform/app/Support/RestoreSafety/RestoreSafetyResolver.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-preview.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-confirm-panel.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-safety-*.blade.php`
|
||||
- `apps/platform/tests/Browser/Spec333RestoreCreateUxFinalProductizationSmokeTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/Spec333RestoreCreateUxFinalProductizationTest.php`
|
||||
- dirty files not to touch during this audit:
|
||||
- existing Spec 332 screenshots and tests
|
||||
- existing runtime PHP, Blade, and test changes
|
||||
- any staged or untracked runtime files
|
||||
- files created by this audit:
|
||||
- `specs/333-restore-create-ux-final-productization/artifacts/restore-preview-step-audit.md`
|
||||
- screenshots under `specs/333-restore-create-ux-final-productization/artifacts/screenshots/preview-audit/`
|
||||
|
||||
## Repo Truth
|
||||
### Data sources
|
||||
- Backup item and restore source: `backup_items.payload`, `backup_items.assignments`, `backup_items.metadata.scope_tag_ids`.
|
||||
- Current target state: latest `policy_versions.snapshot`, `policy_versions.assignments`, `policy_versions.scope_tags`.
|
||||
- Preview generation: `App\Services\Intune\RestoreDiffGenerator::generate()`.
|
||||
- Preview state in wizard: `preview_summary`, `preview_diffs`, `preview_ran_at`, `preview_basis`, `preview_invalidation_reasons`.
|
||||
- Presenter contract: `RestoreRunCreatePresenter::make()` and `previewSummary()`.
|
||||
- Visible UI: `restore-run-preview.blade.php`.
|
||||
- Gate/proof data: `RestoreSafetyResolver`, `PreviewIntegrityState`, `ChecksIntegrityState`, `ExecutionReadinessState`, process-flow and proof partials.
|
||||
|
||||
### Diff data availability
|
||||
- repo-verified: per-policy `diff.summary.added`, `removed`, `changed`.
|
||||
- repo-verified: bounded raw diff arrays exist in `diff.added`, `diff.removed`, `diff.changed`, but current Step 04 does not render raw diff details by default.
|
||||
- repo-verified: `diff_omitted` and `diff_truncated` exist for limit handling.
|
||||
- derived from existing model: `needsAttentionDiffs` is derived from diff counts, `assignments_changed`, and `scope_tags_changed`.
|
||||
- not available: row-level human review reason as a first-class field.
|
||||
|
||||
### Assignment and scope-tag availability
|
||||
- repo-verified: `assignments_changed` boolean per preview entry.
|
||||
- repo-verified: aggregate `assignments_changed` policy count in preview summary.
|
||||
- repo-verified: `scope_tags_changed` boolean per preview entry.
|
||||
- repo-verified: aggregate `scope_tags_changed` policy count in preview summary.
|
||||
- not available: assignment added/removed/changed counts.
|
||||
- not available: scope-tag added/removed/changed counts.
|
||||
- deferred: mapping-specific assignment/scope-tag explanation in preview rows.
|
||||
|
||||
### Preview action availability
|
||||
- repo-verified: `RestoreDiffGenerator` emits `action = update` when a current version exists and `action = create` otherwise.
|
||||
- foundation-real but not generated here: Blade contains a `delete` label path, but the generator path audited here does not produce deletes.
|
||||
- derived from existing model: UI displays unchanged rows as `No policy changes` even though generator action can still be `update`.
|
||||
- not available: a persisted no-op action type.
|
||||
|
||||
### Review, warning, and blocker categories
|
||||
- repo-verified: validation blockers/warnings come from check summary and check results.
|
||||
- repo-verified: execution readiness can block confirmation even when validation and preview are current.
|
||||
- repo-verified: preview/check integrity states are `not_generated`, `invalidated`, `stale`, and `current`.
|
||||
- derived from existing model: `canProceedToConfirm = checksAreCurrent && previewIsCurrent && executionAllowed`.
|
||||
- current gap: `nextGateLabel` currently returns `Confirmation` when checks and preview are current, even if `executionAllowed` is false.
|
||||
|
||||
## Browser Fixture / URLs
|
||||
- local target: `http://localhost/admin`
|
||||
- fixture used for reachable diff state:
|
||||
- workspace: `spec332-audit-dbdw18`
|
||||
- environment: `spec332-audit-env-tazinm`
|
||||
- user: `spec332-audit-i5optd@example.test`
|
||||
- backup set: `18` (`Spec332 Audit Usable Backup`)
|
||||
- route pattern used:
|
||||
- `/admin/local/smoke-login?...&redirect=/admin/workspaces/spec332-audit-dbdw18/environments/spec332-audit-env-tazinm/restore-runs/create?backup_set_id=18`
|
||||
- capture method:
|
||||
- Integrated Browser was used to navigate and verify interactions.
|
||||
- In-app screenshot capture timed out on `Page.captureScreenshot`, so image files were captured with the repo-installed Playwright package in `apps/platform`.
|
||||
- no restore run was created.
|
||||
|
||||
### Reachable states
|
||||
- Preview generated with differences: reachable.
|
||||
- Needs attention: reachable.
|
||||
- Safety/proof disclosure: reachable.
|
||||
- Dark mode: reachable.
|
||||
- Next before preview: reachable; visually enabled, halted by hook.
|
||||
- Next after generated preview with execution unavailable: reachable; visually enabled, halted by hook.
|
||||
|
||||
### Not reachable states
|
||||
- Preview generated with no changes: not reachable with current workspace-scoped fixtures. No-change local backups exist in legacy non-workspace-scoped data (`backup_set_id` 4/14), but not in the Spec 332/333 scoped route.
|
||||
- Preview with assignment changes: data exists in `backup_set_id` 19, but browser flow stops at Step 03 with a validation blocker: `1 group assignment targets are missing in the tenant and require mapping (or skip)`.
|
||||
- Preview with scope-tag changes: repo-supported by generator fields, but no current scoped browser fixture was found.
|
||||
- Preview blocked/unavailable on Step 04: not directly reachable because Step 03 validation blockers halt progression before Step 04. The presenter can render unavailable states from state, but the wizard path did not expose them without adding fixtures.
|
||||
|
||||
## Screenshot Index
|
||||
| State | Screenshot | Notes |
|
||||
| --- | --- | --- |
|
||||
| Preview generated summary | `screenshots/preview-audit/01-preview-generated-summary.png` | Diff state with current preview, execution unavailable |
|
||||
| Needs attention | `screenshots/preview-audit/02-preview-needs-attention.png` | Row shows policy diff, assignments, scope tags |
|
||||
| Details expanded | `screenshots/preview-audit/03-preview-details-expanded.png` | All reviewed items opened |
|
||||
| All reviewed collapsed | `screenshots/preview-audit/04-preview-all-reviewed-collapsed.png` | Default collapsed disclosure |
|
||||
| No changes | not captured | No workspace-scoped no-change fixture found |
|
||||
| Assignment change | not captured | Existing fixture blocked by missing group mapping at validation |
|
||||
| Scope-tag change | not captured | No scoped browser fixture found |
|
||||
| Blocked preview | not captured | Wizard halts before Step 04 for blocker fixture |
|
||||
| Safety/proof disclosure | `screenshots/preview-audit/09-preview-safety-proof-disclosure.png` | Safety gates and proof opened |
|
||||
| Dark mode | `screenshots/preview-audit/10-preview-dark-mode.png` | Step 04 summary in dark theme |
|
||||
|
||||
## Current UI Assessment
|
||||
### Decision Summary
|
||||
- Strong: summary-first layout is clear, quiet, and scannable.
|
||||
- Strong: counts for policies reviewed, changed, unchanged, review, assignments, scope tags, blockers, warnings, and next gate are visible above details.
|
||||
- Gap: `Next gate` can say `Confirmation` while `Execution` says `Unavailable` and `Next` is blocked.
|
||||
- Gap: the primary next action text says to resolve Microsoft permission readiness, but the metric card still says `Confirmation`.
|
||||
- Gap: `Next` is visually enabled before preview and when execution readiness blocks confirmation.
|
||||
|
||||
### Preview Details
|
||||
- Strong: details are not a card flood on desktop; changed items render in a table-like hierarchy with clear labels.
|
||||
- Strong: raw JSON/provider payloads are hidden by default.
|
||||
- Gap: row does not include a `Review reason`.
|
||||
- Gap: `Update existing` is clearer than `Update`, but it still does not explain what changed by itself.
|
||||
- Gap: unchanged rows still have an underlying generated action of `update`; the UI derives `No policy changes`, but the contract is not explicit in data.
|
||||
|
||||
### Needs Attention
|
||||
- Strong: changed rows are separated from unchanged rows.
|
||||
- Gap: the warning text is generic: `Review policy diff, assignments, and scope tags before confirmation.`
|
||||
- Gap: if only one dimension changed, the row still references all dimensions instead of the precise reason.
|
||||
|
||||
### All Reviewed Items
|
||||
- Strong: collapsed by default and not dominant.
|
||||
- Strong: useful as a complete audit list.
|
||||
- Gap: because changed rows are already shown above, the disclosure duplicates data. That is acceptable if the later implementation keeps it compact.
|
||||
|
||||
### Safety / Proof Disclosure
|
||||
- Strong: visible but below the preview decision and details.
|
||||
- Strong: full safety/proof content is progressively disclosed.
|
||||
- Gap: `Preview evidence` appears both in the preview summary and in the lower evidence/proof section, which can blur whether it means diff evidence or gate evidence.
|
||||
- Gap: `Execution unavailable` is visible, but its relationship to `Next gate: Confirmation` is contradictory.
|
||||
|
||||
### Buttons / Interactions
|
||||
| Action | Visible state | Expected behavior | Actual behavior | Preserves state? | Gate effect | Label clear? | Severity | Recommended fix |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| Next before preview | Visible and enabled | Should be disabled or clearly blocked until preview exists | Click stays on Step 04 through validation hook | yes | no gate advance | weak | P2 | Disable or relabel with preview requirement |
|
||||
| Next after preview, execution unavailable | Visible and enabled | Should not advertise Confirmation if execution readiness blocks confirmation | Click stays on Step 04 and shows `Execution blocked` | yes | no gate advance | contradictory | P0 | Set next gate/action to prerequisites blocked, or disable Next |
|
||||
| Back | Visible | Return to Step 03 preserving state | Works in normal wizard flow | yes | previous step | clear | P3 | No change |
|
||||
| Cancel | Visible earlier in wizard | Navigate away/cancel create flow | Standard Filament behavior | n/a | exits flow | clear | P3 | No change |
|
||||
| Generate preview | Visible before preview | Generate current preview | Works | yes | preview current | clear enough | P2 | Avoid `normalized diff` helper copy |
|
||||
| Regenerate preview | Visible after preview | Refresh preview for current basis | Works | yes | preview refreshed | clear | P3 | Keep |
|
||||
| Clear | Visible after preview | Clear preview state | Visible; not tested to avoid losing audit state | likely yes | preview not generated | acceptable | P3 | Label could be `Clear preview` for specificity |
|
||||
| All reviewed items disclosure | Visible after preview | Expand complete list | Works | yes | none | clear | P3 | Keep collapsed |
|
||||
| View safety gates and restore proof | Visible after preview | Expand safety/proof details | Works | yes | none | mostly clear | P2 | Avoid duplicate `proof` terminology if proof is incomplete |
|
||||
| Diagnostics | Present in partials/tests, not prominent in captured state | Show technical details only on demand | Not a dominant control in Step 04 | yes | none | acceptable | P3 | Keep behind disclosure |
|
||||
|
||||
### Copy / Terminology
|
||||
Problematic or mixed terms found:
|
||||
- `Generate a normalized diff preview before confirmation.`
|
||||
- classification: P2 copy issue
|
||||
- reason: `normalized diff` is implementation language.
|
||||
- `1 policy will be updated during execution.`
|
||||
- classification: P2/P1 depending context
|
||||
- reason: generated from preview stage; not create/no-op aware and implies execution certainty.
|
||||
- `Next gate: Confirmation`
|
||||
- classification: P0 in the verified fixture
|
||||
- reason: contradicted by `Execution unavailable` and blocked `Next`.
|
||||
- `Execution unavailable`
|
||||
- classification: repo-safe, but relationship to confirmation is unclear.
|
||||
- `Update existing`
|
||||
- classification: acceptable label, but insufficient without review reason.
|
||||
- Not observed in Step 04 capture:
|
||||
- `Graph works again`
|
||||
- `technical startability`
|
||||
- `write-gate`
|
||||
- `hard-blocker`
|
||||
|
||||
## Preview Row Contract Recommendation
|
||||
| Column | Data source | Available? | Classification | Fallback if missing | Recommended label |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Policy | `preview_diffs[].display_name`, `policy_identifier`, `policy_type`, `platform` | yes | repo-verified | policy identifier plus type | `Policy` |
|
||||
| Restore action | `preview_diffs[].action` plus derived unchanged state | partial | repo-verified for create/update, derived for no-change | `Review required` | `Restore action` |
|
||||
| Policy diff | `preview_diffs[].diff.summary` | yes | repo-verified | `Diff unavailable` | `Policy diff` |
|
||||
| Assignments | `preview_diffs[].assignments_changed` | yes, boolean only | repo-verified | `Unavailable` | `Assignments` |
|
||||
| Scope tags | `preview_diffs[].scope_tags_changed` | yes, boolean only | repo-verified | `Unavailable` | `Scope tags` |
|
||||
| Review reason | derived from diff counts, assignment boolean, scope-tag boolean, action, degraded/source states | not stored | derived from existing model | `Review preview evidence before confirmation` | `Review reason` |
|
||||
| Row action | no row-level action currently exists | no | not available | use disclosure only | `Action` only if an actual action is added |
|
||||
|
||||
Recommended derived review reasons:
|
||||
- `Policy settings differ from current target state.`
|
||||
- `Assignments differ from current target state.`
|
||||
- `Scope tags differ from current target state.`
|
||||
- `Target policy is missing and would be created if restore proceeds.`
|
||||
- `No restore changes detected.`
|
||||
- `Preview detail is unavailable because the diff was omitted by limits.`
|
||||
|
||||
## State Matrix
|
||||
| State | Validation state | Preview data state | Expected status | Expected next gate | Expected primary action | Can continue? | Proof claims allowed? | Current UI | Gap |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| Preview not generated | current checks | none | preview required | Preview | Generate preview | no | validation proof only | Shows `Next gate: Preview`; `Next` enabled but halted | Button state weak |
|
||||
| Preview generated, no changes | current checks | current, no changed rows | preview current | Confirmation if execution allowed, otherwise prerequisites | Continue only if execution allowed | only if execution allowed | not browser-reached | Needs fixture |
|
||||
| Preview generated, changes | current checks | current, changed rows | preview current | Confirmation if execution allowed, otherwise prerequisites | Review rows | only if execution allowed | preview proof only | Shows changes clearly | Missing review reason |
|
||||
| Preview generated, needs attention | current checks | current, needs attention | review required | Confirmation if execution allowed, otherwise prerequisites | Review reasons | only if execution allowed | preview proof only | Generic review text | Row reason missing |
|
||||
| Preview generated, blocker | blocked checks or execution blocked | current or unavailable | blocked | Resolve blockers | Resolve blockers | no | no execution proof | Shows `Confirmation` in verified execution-blocked fixture | P0 gate contradiction |
|
||||
| Preview stale after scope/mapping change | check or preview basis mismatch | stale/invalidated | regenerate required | Validation or Preview | rerun checks/preview | no | prior proof only as stale | repo-supported | Not browser-captured |
|
||||
| Validation blocked | blocked | none/current irrelevant | blocked | Validation | resolve blockers | no | no execution proof | browser reached on group mapping backup at Step 03 | Step 04 not reachable |
|
||||
| Validation passed with warnings | current with warnings | current or none | review warnings | Preview/Confirmation depending preview and execution | review warnings | maybe | warning proof only | repo-supported | Not browser-captured |
|
||||
| Validation passed clean | current clean | current | ready if execution allowed | Confirmation | continue | yes only if execution allowed | preview/check proof allowed, no execution proof | execution still unavailable in fixture | Gate wording conflict |
|
||||
|
||||
## Findings
|
||||
### P0 Blockers
|
||||
1. Step 04 shows the wrong next gate when execution readiness blocks confirmation.
|
||||
- Evidence: browser fixture shows `Next gate: Confirmation`, `Execution: Unavailable`, and primary text `resolve Microsoft permission readiness before confirmation`.
|
||||
- Actual click behavior: `Next` remains on Step 04 and shows `Execution blocked`.
|
||||
- Risk: operator sees a contradictory decision state.
|
||||
- Recommended fix: `nextGateLabel` must account for `! executionAllowed`; use `Prerequisites` or `Execution readiness` instead of `Confirmation`, and make the primary action match.
|
||||
|
||||
### P1 High
|
||||
1. Preview rows lack explicit review reason.
|
||||
- Evidence: row shows `Update existing`, `0 added`, `0 removed`, `1 changed`, `No change`, `No change`.
|
||||
- Risk: review intent remains implicit and brittle for assignment/scope-tag-only changes.
|
||||
- Recommended fix: add a derived `Review reason` field/column in presenter or view helpers.
|
||||
2. Assignment and scope-tag detail is not available beyond boolean change flags.
|
||||
- Evidence: generator emits `assignments_changed` and `scope_tags_changed`, plus aggregate counts.
|
||||
- Risk: UI can truthfully say changed/no change, but not explain which assignments or tags changed.
|
||||
- Recommended fix: do not invent detail. Label as changed/no change/mapping required/unavailable until generator supports detailed normalized diffs.
|
||||
3. Preview notification overstates execution certainty.
|
||||
- Evidence: `1 policy will be updated during execution.`
|
||||
- Risk: preview stage is not execution, and action mix can include create/no-change.
|
||||
- Recommended fix: `Preview found 1 policy with restore differences.`
|
||||
|
||||
### P2 Medium
|
||||
1. `Next` is visually enabled when the next action is blocked by missing preview or execution readiness.
|
||||
- Evidence: browser click before preview stays on Step 04; after preview and execution unavailable stays on Step 04.
|
||||
- Recommended fix: disable or change label/helper when `canProceedToConfirm` is false.
|
||||
2. `normalized diff` copy is developer-facing.
|
||||
- Evidence: helper text says `Generate a normalized diff preview before confirmation.`
|
||||
- Recommended fix: `Generate a preview of policy, assignment, and scope-tag changes before confirmation.`
|
||||
3. `Preview evidence` is used for both diff currentness and lower proof/gate area.
|
||||
- Risk: evidence language can imply more proof than exists.
|
||||
- Recommended fix: use `Preview status` for currentness and `Safety gates` for lower proof area.
|
||||
4. Browser fixture coverage is incomplete for no-change, assignment-change-after-mapping, scope-tag-change, stale, and blocked Step 04 states.
|
||||
- Recommended fix: add focused browser fixtures or component-state browser harness only after the UI contract is finalized.
|
||||
|
||||
### P3 Polish
|
||||
1. `Clear` should be `Clear preview` for specificity.
|
||||
2. `All reviewed items` is useful but duplicates changed rows; keep it collapsed and compact.
|
||||
3. The row layout is acceptable on desktop, but mobile cards should keep fixed label/value spacing to avoid repeated card bulk.
|
||||
|
||||
## Recommended Implementation Plan
|
||||
- narrow changes only:
|
||||
- presenter gate logic
|
||||
- preview row contract helpers
|
||||
- preview Blade labels/copy
|
||||
- focused tests
|
||||
- files to touch later:
|
||||
- `apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-preview.blade.php`
|
||||
- `apps/platform/app/Filament/Resources/RestoreRunResource.php` only for action label/helper/disabled-state adjustments
|
||||
- existing Spec 333 feature/browser tests
|
||||
- tests to add/update later:
|
||||
- presenter test for `executionAllowed=false` producing non-Confirmation next gate
|
||||
- feature test for derived review reasons across policy diff, assignment change, scope-tag change, create, and no-change rows
|
||||
- browser smoke for blocked `Next` state and clean preview state if fixtures are added
|
||||
- screenshots to update later:
|
||||
- no-change state
|
||||
- assignment-change state after mapping/skip fixture
|
||||
- scope-tag-change state
|
||||
- blocked/prerequisite state
|
||||
|
||||
## DoD for Later Implementation
|
||||
- `Next gate` never says `Confirmation` unless `canProceedToConfirm` is true.
|
||||
- `Next` behavior and visible label/helper match the current gate.
|
||||
- Preview rows include:
|
||||
- Policy
|
||||
- Restore action
|
||||
- Policy diff
|
||||
- Assignments
|
||||
- Scope tags
|
||||
- Review reason
|
||||
- Assignment and scope-tag columns do not imply unavailable detail.
|
||||
- Raw JSON/provider payloads remain hidden by default.
|
||||
- Copy avoids implementation terms and false execution/recovery proof claims.
|
||||
- Tests cover Filament/Livewire v5/v4 component behavior through Livewire components, not static resource classes.
|
||||
- No global assets are introduced; `filament:assets` deployment requirement remains unchanged.
|
||||
|
||||
## Filament v5 Audit Notes
|
||||
- Livewire compliance: application is on Livewire `4.1.4`, satisfying Filament v5's Livewire v4 requirement.
|
||||
- Provider registration: panel/provider registration remains in `apps/platform/bootstrap/providers.php`; this audit made no provider changes.
|
||||
- Global search: `RestoreRunResource::getPages()` includes a `view` page, satisfying the v5 hard rule if the resource is globally searchable.
|
||||
- Destructive actions: Step 04 generate/regenerate/clear preview actions are not destructive restore execution actions. The dangerous execution gate remains later in the wizard and must continue to require confirmation and authorization.
|
||||
- Asset strategy: no Filament assets were added. Deployment `php artisan filament:assets` remains unchanged.
|
||||
- Testing plan for later implementation: use Pest/Livewire component tests for wizard state and Filament action tests for Step 04 actions.
|
||||
@ -0,0 +1 @@
|
||||
|
||||
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 157 KiB |
@ -0,0 +1,42 @@
|
||||
# Specification Quality Checklist: Spec 333 - Restore Create UX Final Productization
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before implementation planning and execution.
|
||||
**Created**: 2026-05-26
|
||||
**Feature**: `specs/333-restore-create-ux-final-productization/spec.md`
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No application implementation is included in the preparation artifacts.
|
||||
- [x] Focused on operator value, safety, proof visibility, and product workflow needs.
|
||||
- [x] All mandatory repository sections are completed.
|
||||
- [x] The spec preserves TenantPilot/TenantAtlas terminology and route/context language.
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No `[NEEDS CLARIFICATION]` markers remain.
|
||||
- [x] Requirements are testable and unambiguous.
|
||||
- [x] Success criteria are measurable.
|
||||
- [x] Acceptance scenarios are defined for each primary user story.
|
||||
- [x] Edge cases are identified.
|
||||
- [x] Scope is clearly bounded to Restore Create UX productization.
|
||||
- [x] Dependencies and assumptions are identified.
|
||||
|
||||
## Constitution / Guardrail Alignment
|
||||
|
||||
- [x] Spec Candidate Check is filled and passes SPEC-GATE-001.
|
||||
- [x] UI Surface Impact and UI/Productization Coverage are filled for the changed wizard surface.
|
||||
- [x] Proportionality review explicitly rejects new architecture, persisted truth, enum/status families, and backend rewrites.
|
||||
- [x] RBAC, workspace/environment isolation, proof semantics, OperationRun UX impact, and provider-boundary impact are addressed.
|
||||
- [x] Test governance and validation lanes are explicit.
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] `spec.md`, `plan.md`, `tasks.md`, and `restore-create-state-contract.md` exist.
|
||||
- [x] Tasks are ordered, small, verifiable, and implementation-ready.
|
||||
- [x] Required screenshot artifacts path is present.
|
||||
- [x] Close alternatives and follow-up specs are kept out of scope.
|
||||
|
||||
## Notes
|
||||
|
||||
- Preparation analysis should re-check that no task requires a backend rewrite, new presenter, migration, package, or new Product Process Flow architecture.
|
||||
- Implementation must update spec/plan before runtime work if repo truth differs from skip behavior, item-level selection, proof-link availability, or group picker context assumptions.
|
||||
342
specs/333-restore-create-ux-final-productization/plan.md
Normal file
@ -0,0 +1,342 @@
|
||||
# Implementation Plan: Spec 333 - Restore Create UX Final Productization
|
||||
|
||||
- **Branch**: `333-restore-create-ux-final-productization`
|
||||
- **Date**: 2026-05-26
|
||||
- **Spec**: `specs/333-restore-create-ux-final-productization/spec.md`
|
||||
- **Input**: Productize Restore Create wizard UX over Spec 332 foundation. No new flow system, presenter, or backend rewrite.
|
||||
|
||||
## Summary
|
||||
|
||||
Finalize the visible Restore Create wizard as a coherent restore safety workflow:
|
||||
|
||||
- Step 1: source decision, backup quality, full gates, proof.
|
||||
- Step 2: calm scope summary, mapping summary by default, explicit resolver mode, group picker context.
|
||||
- Step 3: product-safe validation, grouped blockers/warnings/safe checks, provider credential blocked state.
|
||||
- Step 4: summary-first preview, needs-attention first, compact unchanged detail, gate-consistent next step.
|
||||
- Step 5: high-friction confirmation, dry-run clarity, proof-safe pre-execution copy.
|
||||
|
||||
Implementation must consume Spec 332 state truth from `RestoreRunCreatePresenter` and existing Product Process Flow components. It must not add backend behavior, persistence, Graph calls, packages, or new architecture.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: PHP 8.4.15, Laravel 12.52.0
|
||||
**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1
|
||||
**Storage**: Existing PostgreSQL-backed models only; no migration
|
||||
**Testing**: Pest Feature/Livewire, Unit, and Browser
|
||||
**Validation Lanes**: confidence + browser + formatting
|
||||
**Target Platform**: Laravel Sail local, Dokploy container runtime for staging/production
|
||||
**Project Type**: Laravel monolith with Filament admin panel
|
||||
**Performance Goals**: keep wizard rendering DB/local-state based; no Graph calls during render; long lists collapsed/compact
|
||||
**Constraints**: no new packages, no migrations, no queues/scheduler/storage/env changes, no new Product Process Flow system
|
||||
**Scale/Scope**: one existing environment-bound Restore Create wizard and its focused tests/screenshots
|
||||
|
||||
## Repo Truth Summary
|
||||
|
||||
Existing foundation:
|
||||
|
||||
- Presenter source of truth: `apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php`
|
||||
- Wizard/page entry: `apps/platform/app/Filament/Resources/RestoreRunResource.php`
|
||||
- Create page wrapper: `apps/platform/app/Filament/Resources/RestoreRunResource/Pages/CreateRestoreRun.php`
|
||||
- Restore Create components:
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-safety-decision.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-backup-quality-summary.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-safety-gates.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-proof-aside.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-scope-summary.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-mapping-resolver-summary.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-checks.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-preview.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-confirm-panel.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-group-mapping-skipped.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/partials/restore-run-process-flow-panel.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/partials/restore-run-proof-panel.blade.php`
|
||||
- Group picker:
|
||||
- `apps/platform/app/Livewire/EntraGroupCachePickerTable.php`
|
||||
- `apps/platform/resources/views/livewire/entra-group-cache-picker-table.blade.php`
|
||||
- `apps/platform/resources/views/filament/modals/entra-group-cache-picker.blade.php`
|
||||
- Existing related tests:
|
||||
- `apps/platform/tests/Feature/Filament/Spec332ProductProcessFlowSystemTest.php`
|
||||
- `apps/platform/tests/Browser/Spec332RestoreRunWizardProductProcessFlowSmokeTest.php`
|
||||
- `apps/platform/tests/Browser/Spec332RestoreRunWizardProductProcessFlowScreenshotsTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/RestoreRunPreviewProductizationTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/RestoreWizardGraphSafetyTest.php`
|
||||
- `apps/platform/tests/Feature/RestoreGroupMappingTest.php`
|
||||
- `apps/platform/tests/Unit/Filament/RestoreRunCreatePresenterDeterminismTest.php`
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: changed surfaces
|
||||
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**:
|
||||
- Restore Create wizard at `/admin/workspaces/{workspace}/environments/{environment}/restore-runs/create`
|
||||
- Step 1 source decision state
|
||||
- Step 2 scope/mapping resolver state
|
||||
- group picker modal
|
||||
- Step 3 validation state
|
||||
- Step 4 preview state
|
||||
- Step 5 confirmation/execution readiness state
|
||||
- **No-impact class, if applicable**: N/A
|
||||
- **Native vs custom classification summary**: mixed Filament wizard/forms/actions plus existing custom Blade components
|
||||
- **Shared-family relevance**: Product Process Flow, proof panels, status messaging, action links, dangerous confirmation
|
||||
- **State layers in scope**: page, wizard step, presenter contract, modal, Livewire table
|
||||
- **Audience modes in scope**: operator-MSP and support-platform; no customer-facing route added
|
||||
- **Decision/diagnostic/raw hierarchy plan**: decision first, proof second, diagnostics/raw third/collapsed
|
||||
- **Raw/support gating plan**: raw IDs secondary, raw payloads/diffs collapsed or behind disclosure
|
||||
- **One-primary-action / duplicate-truth control**: presenter contract owns status/reason/impact/next-action; Blade renders it without computing alternate truth
|
||||
- **Handling modes by drift class or surface**: review-mandatory because this is a dangerous workflow
|
||||
- **Repository-signal treatment**: review-mandatory for restore/gating/proof copy
|
||||
- **Special surface test profiles**: dangerous workflow wizard + browser smoke
|
||||
- **Required tests or manual smoke**: focused Feature/Livewire, Unit presenter determinism, Browser screenshots
|
||||
- **Exception path and spread control**: none planned
|
||||
- **Active feature PR close-out entry**: Smoke Coverage
|
||||
- **UI/Productization coverage decision**: existing route/archetype coverage remains valid; Spec 333 screenshots provide focused coverage
|
||||
- **Coverage artifacts to update**: none unless implementation discovers route/archetype/coverage drift
|
||||
- **No-impact rationale**: N/A
|
||||
- **Navigation / Filament provider-panel handling**: no panel/provider registration change
|
||||
- **Screenshot or page-report need**: screenshots required, page report not required unless drift is discovered
|
||||
|
||||
## Shared Pattern & System Fit
|
||||
|
||||
- **Cross-cutting feature marker**: yes
|
||||
- **Systems touched**: Restore Create wizard, Product Process Flow, Restore Proof, status messaging, group picker, validation/preview/confirmation panels
|
||||
- **Shared abstractions reused**: `RestoreRunCreatePresenter`, existing restore-run Blade components, `BadgeRenderer`, `OperationRunLinks`, restore safety/preview/check integrity resolvers
|
||||
- **New abstraction introduced? why?**: none
|
||||
- **Why the existing abstraction was sufficient or insufficient**: sufficient; Spec 332 intentionally centralized the product-state contract and Product Process Flow
|
||||
- **Bounded deviation / spread control**: display-only tweaks may stay in existing components but cannot calculate independent gate/proof truth
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: presentation only
|
||||
- **Central contract reused**: existing restore execution behavior and `OperationRunLinks`
|
||||
- **Delegated UX behaviors**: existing start/completion/notification behavior remains unchanged
|
||||
- **Surface-owned behavior kept local**: restore inputs, dry-run/preview-only toggle, typed confirmation, pre-execution proof copy
|
||||
- **Queued DB-notification policy**: N/A
|
||||
- **Terminal notification path**: N/A
|
||||
- **Exception path**: none
|
||||
|
||||
## Provider Boundary & Portability Fit
|
||||
|
||||
- **Shared provider/platform boundary touched?**: yes, display-only
|
||||
- **Provider-owned seams**: Entra group cache, Microsoft/provider connection readiness
|
||||
- **Platform-core seams**: environment, restore scope, proof basis, operation proof, post-run evidence
|
||||
- **Neutral platform terms / contracts preserved**: environment, provider connection, restore scope, target mapping, operation proof, post-run evidence
|
||||
- **Retained provider-specific semantics and why**: directory group and object ID wording remains where the existing Entra group mapping boundary requires it
|
||||
- **Bounded extraction or follow-up path**: broader provider readiness productization deferred to follow-up
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before implementation. Re-check after implementation.*
|
||||
|
||||
- **Inventory-first / snapshots-second**: pass; Restore Create uses existing backup snapshots and restore draft state.
|
||||
- **Read/write separation**: pass; preview/dry-run and explicit confirmation remain required before execution.
|
||||
- **Graph contract path**: pass; no new Graph calls. Existing Graph behavior remains behind existing services/contracts.
|
||||
- **Deterministic capabilities**: pass; existing capability checks remain authoritative.
|
||||
- **RBAC-UX**: pass; no UI-only authorization changes. Existing server-side authorization/capability checks remain required.
|
||||
- **Workspace/tenant isolation**: pass; existing route-bound workspace/environment context remains in force; group picker must use current environment cache only.
|
||||
- **Run observability / OperationRun UX**: pass; no new OperationRun behavior.
|
||||
- **Test governance**: pass; lane classification is explicit and bounded.
|
||||
- **Proportionality / no premature abstraction**: pass; no new presenter, framework, persistence, enum, taxonomy, or backend layer.
|
||||
- **Provider boundary**: pass; provider-specific wording remains only at existing provider-owned group/provider seams.
|
||||
- **Filament-native UI**: pass with existing mixed custom components; implementation must keep Filament semantics and no ad-hoc styling system.
|
||||
- **UI/Productization coverage**: pass; reachable UI changes are classified and screenshots are required.
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
- **Test purpose / classification by changed surface**:
|
||||
- Unit: presenter determinism and no static memoization/leakage.
|
||||
- Feature/Livewire: wizard state, copy, gating, grouped validation, preview summary, confirmation readiness.
|
||||
- Browser: visible multi-step workflow and screenshots.
|
||||
- **Affected validation lanes**: confidence + browser.
|
||||
- **Why this lane mix is the narrowest sufficient proof**: Restore Create is a Livewire wizard with browser-visible behavior and high-risk state gates; pure unit/feature tests alone cannot prove visible layout/screenshot requirements.
|
||||
- **Narrowest proving commands**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Filament/Spec333RestoreCreateUxFinalProductizationTest.php tests/Feature/RestoreGroupMappingTest.php tests/Feature/Filament/RestoreWizardGraphSafetyTest.php tests/Feature/Filament/RestoreRunPreviewProductizationTest.php tests/Unit/Filament/RestoreRunCreatePresenterDeterminismTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec333RestoreCreateUxFinalProductizationSmokeTest.php --compact`
|
||||
- **Fixture / helper / factory / seed / context cost risks**: use existing Spec 332 browser fixtures/helpers where possible; do not create broad shared defaults.
|
||||
- **Expensive defaults or shared helper growth introduced?**: no planned shared default growth.
|
||||
- **Heavy-family additions, promotions, or visibility changes**: explicit Spec 333 browser smoke only.
|
||||
- **Surface-class relief / special coverage rule**: no standard relief; dangerous workflow requires browser smoke/screenshots.
|
||||
- **Closing validation and reviewer handoff**: run targeted Feature/Unit/Browser tests, Spec 332 overlap tests, Pint dirty, and `git diff --check`; report full suite if not run.
|
||||
- **Budget / baseline / trend follow-up**: none expected unless browser fixture runtime materially grows.
|
||||
- **Review-stop questions**: lane fit, breadth, hidden fixture cost, browser screenshot reliability, no false proof copy.
|
||||
- **Escalation path**: document-in-feature.
|
||||
- **Active feature PR close-out entry**: Smoke Coverage.
|
||||
- **Why no dedicated follow-up spec is needed**: this is bounded final productization of one existing wizard; post-execution proof belongs to a separate follow-up.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/333-restore-create-ux-final-productization/
|
||||
├── spec.md
|
||||
├── plan.md
|
||||
├── tasks.md
|
||||
├── restore-create-state-contract.md
|
||||
├── checklists/
|
||||
│ └── requirements.md
|
||||
└── artifacts/
|
||||
└── screenshots/
|
||||
└── .gitkeep
|
||||
```
|
||||
|
||||
### Source Code (expected implementation surfaces)
|
||||
|
||||
```text
|
||||
apps/platform/app/Filament/Resources/
|
||||
├── RestoreRunResource.php
|
||||
└── RestoreRunResource/
|
||||
├── Pages/CreateRestoreRun.php
|
||||
└── Presenters/RestoreRunCreatePresenter.php
|
||||
|
||||
apps/platform/resources/views/filament/forms/components/
|
||||
├── restore-run-safety-decision.blade.php
|
||||
├── restore-run-backup-quality-summary.blade.php
|
||||
├── restore-run-safety-gates.blade.php
|
||||
├── restore-run-proof-aside.blade.php
|
||||
├── restore-run-scope-summary.blade.php
|
||||
├── restore-run-mapping-resolver-summary.blade.php
|
||||
├── restore-run-checks.blade.php
|
||||
├── restore-run-preview.blade.php
|
||||
├── restore-run-confirm-panel.blade.php
|
||||
└── restore-run-group-mapping-skipped.blade.php
|
||||
|
||||
apps/platform/resources/views/livewire/
|
||||
└── entra-group-cache-picker-table.blade.php
|
||||
|
||||
apps/platform/resources/views/filament/modals/
|
||||
└── entra-group-cache-picker.blade.php
|
||||
```
|
||||
|
||||
### Test Code (expected implementation surfaces)
|
||||
|
||||
```text
|
||||
apps/platform/tests/Feature/Filament/Spec333RestoreCreateUxFinalProductizationTest.php
|
||||
apps/platform/tests/Feature/RestoreGroupMappingTest.php
|
||||
apps/platform/tests/Feature/Filament/RestoreWizardGraphSafetyTest.php
|
||||
apps/platform/tests/Feature/Filament/RestoreRunPreviewProductizationTest.php
|
||||
apps/platform/tests/Unit/Filament/RestoreRunCreatePresenterDeterminismTest.php
|
||||
apps/platform/tests/Browser/Spec333RestoreCreateUxFinalProductizationSmokeTest.php
|
||||
```
|
||||
|
||||
## Domain / Model Implications
|
||||
|
||||
- No model changes.
|
||||
- No migration.
|
||||
- No new restore backend behavior.
|
||||
- No new OperationRun type.
|
||||
- No provider gateway change.
|
||||
- Existing `RestoreRun`, `BackupSet`, `BackupItem`, `EntraGroup`, `ManagedEnvironment`, provider connection, validation, and preview state remain the data sources.
|
||||
|
||||
## UI / Filament Implications
|
||||
|
||||
- Filament v5 / Livewire v4.1 compliance remains required.
|
||||
- Panel provider registration remains unchanged in `apps/platform/bootstrap/providers.php`; no new panel/provider.
|
||||
- `RestoreRunResource` keeps existing View page; global search behavior is not changed by this spec and must be verified if touched.
|
||||
- Destructive/high-impact behavior is not changed. Real restore execution remains gated by existing dry-run/preview, validation, confirmation, authorization, and audit behavior.
|
||||
- No registered Filament assets are planned, so no new `filament:assets` deployment step is introduced.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1 - State Contract / Repo Truth
|
||||
|
||||
- Re-read Spec 332 artifacts, current presenter/components, and this Spec 333 state contract.
|
||||
- Confirm `RestoreRunCreatePresenter` is the single source of truth.
|
||||
- Confirm no static/process-level memoization.
|
||||
- Update runtime only after state contract alignment is clear.
|
||||
|
||||
### Phase 2 - Step 1 Polish
|
||||
|
||||
- Align Step 1 source states to required copy.
|
||||
- Ensure backup quality summary exposes required counts and caveat.
|
||||
- Keep full gates and proof panel visible.
|
||||
- Remove internal/technical/false-proof copy.
|
||||
|
||||
### Phase 3 - Step 2 Scope / Mapping / Group Picker
|
||||
|
||||
- Keep default Step 2 calm and summary-first.
|
||||
- Hide mapping details by default.
|
||||
- Productize resolver row identity and single collapse action.
|
||||
- Preserve or explicitly state skip behavior.
|
||||
- Make group picker context and empty states current-environment safe.
|
||||
- Keep Next blocked by unresolved mappings.
|
||||
|
||||
### Phase 4 - Step 3 Validation
|
||||
|
||||
- Group blockers, warnings, and safe checks.
|
||||
- Make provider credentials missing a product-safe blocked state.
|
||||
- Fix toast copy when blockers exist.
|
||||
- Keep next state-aware and blocked when validation is blocked.
|
||||
|
||||
### Phase 5 - Step 4 Preview
|
||||
|
||||
- Make preview summary-first.
|
||||
- Show needs attention first.
|
||||
- Collapse or compact unchanged/no-change items.
|
||||
- Hide raw diff/provider payload by default.
|
||||
- Correct next-gate copy for current/blocked/not-generated states.
|
||||
|
||||
### Phase 6 - Step 5 Confirmation
|
||||
|
||||
- Make confirmation high-friction.
|
||||
- Make dry-run/preview-only state clear.
|
||||
- Show pre-execution operation proof and post-run evidence as unavailable.
|
||||
- Ensure execute availability remains controlled by existing gates and capability behavior.
|
||||
|
||||
### Phase 7 - Tests / Browser / Screenshots
|
||||
|
||||
- Add or adjust focused Feature/Unit tests.
|
||||
- Add browser smoke and screenshots.
|
||||
- Run targeted validation commands.
|
||||
- Re-run Spec 332 overlap tests if shared components changed.
|
||||
|
||||
## Validation Commands
|
||||
|
||||
```bash
|
||||
cd apps/platform
|
||||
|
||||
./vendor/bin/sail artisan test \
|
||||
tests/Feature/Filament/Spec333RestoreCreateUxFinalProductizationTest.php \
|
||||
tests/Feature/RestoreGroupMappingTest.php \
|
||||
tests/Feature/Filament/RestoreWizardGraphSafetyTest.php \
|
||||
tests/Feature/Filament/RestoreRunPreviewProductizationTest.php \
|
||||
tests/Unit/Filament/RestoreRunCreatePresenterDeterminismTest.php \
|
||||
--compact
|
||||
```
|
||||
|
||||
```bash
|
||||
cd apps/platform
|
||||
|
||||
./vendor/bin/sail php vendor/bin/pest \
|
||||
tests/Browser/Spec333RestoreCreateUxFinalProductizationSmokeTest.php \
|
||||
--compact
|
||||
```
|
||||
|
||||
```bash
|
||||
cd apps/platform
|
||||
|
||||
./vendor/bin/sail artisan test --filter=Spec332 --compact
|
||||
./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec332* --compact
|
||||
./vendor/bin/sail pint --dirty
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## Rollout / Deployment Considerations
|
||||
|
||||
- **Migrations**: none.
|
||||
- **Environment variables**: none.
|
||||
- **Queues/workers**: none.
|
||||
- **Scheduler**: none.
|
||||
- **Storage/volumes**: none.
|
||||
- **Packages/assets**: none.
|
||||
- **Dokploy/Staging/Production**: ordinary app deployment only; validate on Staging because restore is high-risk UI.
|
||||
- **Filament assets**: no new registered assets planned; if implementation unexpectedly registers assets, deployment must include `cd apps/platform && php artisan filament:assets` and this plan must be updated first.
|
||||
|
||||
## Risk Controls
|
||||
|
||||
- Do not implement a new flow system or presenter.
|
||||
- Do not move truth into Blade.
|
||||
- Do not add backend restore behavior.
|
||||
- Do not overclaim recovery, operation proof, post-run evidence, health, or customer safety.
|
||||
- Do not use raw IDs as primary labels.
|
||||
- Do not expose raw provider errors/payloads by default.
|
||||
- Do not broaden tests into unrelated surfaces.
|
||||
- Update spec/plan before implementation if repo truth differs from assumptions.
|
||||
@ -0,0 +1,286 @@
|
||||
# Restore Create State Contract - Spec 333
|
||||
|
||||
This contract describes the visible product-state matrix for the Restore Create wizard. It is a preparation artifact for implementation. Runtime truth must remain centralized in `RestoreRunCreatePresenter`; Blade components render this contract and must not invent independent status, proof, gate, or next-action truth.
|
||||
|
||||
## Contract Columns
|
||||
|
||||
| Column | Meaning |
|
||||
|---|---|
|
||||
| State | The product state represented in the current wizard step |
|
||||
| Visible status | The operator-facing state label |
|
||||
| Reason | Why the state is true |
|
||||
| Impact | What the state means for safe progression |
|
||||
| Primary next action | The one dominant action for the operator |
|
||||
| Gate state | Complete, required, blocked, warning, or unavailable |
|
||||
| Can continue? | Whether the wizard may advance from this state |
|
||||
| Proof claims allowed? | What the UI may claim without overclaiming |
|
||||
| Diagnostics | Default diagnostic visibility |
|
||||
|
||||
## Central Wizard Gate Contract
|
||||
|
||||
`RestoreRunCreatePresenter` must expose one `wizard_gate` section. Step views, compact safety evidence, button hooks, disabled states, and blocking notifications consume this section rather than recreating gate copy locally.
|
||||
|
||||
Required concepts:
|
||||
|
||||
- `current_gate` / `current_gate_label`
|
||||
- `next_gate` / `next_gate_label`
|
||||
- `blocking_prerequisite` / `blocking_prerequisite_label`
|
||||
- `required_action` / `required_action_label`
|
||||
- `can_continue`
|
||||
- `continue_label`
|
||||
- `continue_disabled_reason`
|
||||
- `execution_state` / `execution_label`
|
||||
- `can_execute`
|
||||
- `proof_state`
|
||||
- `preview_state`
|
||||
- `confirmation_state`
|
||||
|
||||
If validation and preview evidence are current but real execution prerequisites are unavailable, Step 4 may still advance to confirmation review. In that state, `next_gate_label` is `Confirmation required`, `can_continue` is `true`, `can_execute` is `false`, and `execution_label` must state that execution is unavailable until prerequisites are resolved. Step 4 must not emit a generic `Execution blocked` notification while advertising confirmation as the next gate.
|
||||
|
||||
## Step 1 - Select Backup Set
|
||||
|
||||
| State | Visible status | Reason | Impact | Primary next action | Gate state | Can continue? | Proof claims allowed? | Diagnostics |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| No backup selected | Source required | Select a completed backup set before restore validation. | Restore validation and preview cannot start without a source backup. | Select backup set. | required | no | Source backup pending; operation proof unavailable; post-run evidence unavailable. | Collapsed |
|
||||
| Backup selected with no captured/restorable items | Source not usable | Selected backup contains no captured restorable items. | Restore validation cannot prove a usable restore source. | Select another backup set. | blocked | no | Source backup selected but unusable; no execution or recovery proof. | Collapsed |
|
||||
| Backup selected and usable with quality/dependency issues | Source selected, review required | Source backup is usable but contains quality or dependency issues. | Validation and preview must resolve these issues before execution. | Continue to scope and resolve required mappings. | warning | yes, to scope only | Source backup usable; input quality caveat; no execution or recovery proof. | Collapsed |
|
||||
| Backup selected and clean | Source selected | A usable source backup is selected. | Restore scope and validation can continue. | Continue to scope. | complete | yes | Source backup selected and input quality shown; no execution or recovery proof. | Collapsed |
|
||||
|
||||
Step 1 default layout:
|
||||
|
||||
- Restore Safety decision card
|
||||
- Backup set selector
|
||||
- Backup quality summary
|
||||
- Full Restore Safety Gates flow
|
||||
- Restore Proof panel
|
||||
- Diagnostics collapsed
|
||||
|
||||
Step 1 must not show tenant-wide recoverability, technical startability, write-gate, hard-blocker, or claims that Graph works again.
|
||||
|
||||
## Step 2 - Define Restore Scope
|
||||
|
||||
| State | Visible status | Reason | Impact | Primary next action | Gate state | Can continue? | Proof claims allowed? | Diagnostics |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| Scope not defined | Scope required | Select all items or selected items before validation. | Validation cannot prove an undefined restore draft. | Define restore scope. | required | no | Source proof may be available; scope proof pending. | Collapsed |
|
||||
| All items in scope, no mappings required | Scope ready | All restorable items remain in scope and no unresolved mappings are detected. | Validation can run for the current draft. | Run safety checks. | complete | yes | Scope selected; dependency mapping not required; no execution/recovery proof. | Collapsed |
|
||||
| Selected items in scope, no mappings required | Scope ready | Selected restore items define the current draft and no unresolved mappings are detected. | Validation can run for the selected scope. | Run safety checks. | complete | yes | Scope selected; no execution/recovery proof. | Collapsed |
|
||||
| Required mappings unresolved | Dependency mappings required | Target mappings are unresolved for the current draft. | Validation cannot prove the current draft until required mappings are resolved or explicitly skipped where allowed. | Resolve mappings. | blocked | no | Source and scope may be selected; dependency proof pending. | Collapsed |
|
||||
| Mappings resolved | Dependency mappings resolved | Required target mappings are resolved or explicitly skipped where allowed. | Validation can run for the current draft. | Run safety checks. | complete | yes | Mapping decision recorded for draft; no execution/recovery proof. | Collapsed |
|
||||
| Resolver expanded | Resolve target mappings | Operator explicitly opened mapping details. | Row-level mapping decisions are visible for review. | Select target group, enter manual object ID, or skip where supported. | required or complete by row | no while unresolved | Source/target display names and IDs as metadata; no execution/recovery proof. | Row details visible; raw payloads hidden |
|
||||
| Group picker has current-environment results | Target group selection available | Cached directory groups exist for this environment. | Operator can choose a target group from cache. | Select target group. | required | returns to resolver | Cache availability only; no proof of execution safety. | Table visible |
|
||||
| Group picker cache empty | No directory group cache available | TenantPilot needs cached directory groups before target mappings can be selected. | Cached target selection cannot proceed until group sync runs, unless manual fallback is supported. | Open group sync or enter object ID manually. | blocked or fallback | no cached selection | Cache unavailable; manual fallback allowed only as explicit fallback. | Empty state visible |
|
||||
|
||||
Step 2 default layout:
|
||||
|
||||
- Define restore scope card
|
||||
- Scope options
|
||||
- Scope impact summary
|
||||
- Restore dependency mapping summary
|
||||
- Resolve mappings action
|
||||
- Compact Restore Safety Status
|
||||
- Restore Proof panel
|
||||
- Diagnostics collapsed
|
||||
|
||||
Step 2 default hidden content:
|
||||
|
||||
- full mapping details
|
||||
- 20+ raw mapping fields
|
||||
- full seven-gate safety flow
|
||||
- repeated GUID helper text
|
||||
- raw IDs as primary source labels
|
||||
|
||||
Mapping row identity:
|
||||
|
||||
- Source group display name first
|
||||
- Source ID secondary
|
||||
- Target group display name first when cached target selected
|
||||
- Target ID secondary
|
||||
- Manual fallback badge if GUID entered manually
|
||||
- Skipped state if intentionally skipped
|
||||
|
||||
Allowed fallback:
|
||||
|
||||
- Unknown source group
|
||||
- Source ID: `{id}`
|
||||
|
||||
Forbidden primary labels:
|
||||
|
||||
- raw GUID only
|
||||
- `Unresolved (...id)`
|
||||
- state label as group name
|
||||
- `SKIP` as primary UX
|
||||
|
||||
## Step 3 - Validate
|
||||
|
||||
| State | Visible status | Reason | Impact | Primary next action | Gate state | Can continue? | Proof claims allowed? | Diagnostics |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| No checks run | Validation required | Run checks after scope and required mappings are complete. | Preview and execution remain unavailable until validation runs. | Run safety checks. | required | no | Validation not recorded; no preview/execution proof. | Collapsed |
|
||||
| Provider credentials missing | Validation blocked | Provider credentials are not available for this environment. | Restore checks cannot run until provider connection is repaired. | Review provider connection. | blocked | no | Provider readiness currently prevents checks; no raw provider exception. | Collapsed |
|
||||
| Checks finished with blockers | Validation blocked | Resolve blocking validation checks before preview. | Preview or execution cannot be trusted while blockers remain. | Review blockers. | blocked | no | Validation ran and blockers exist; no execution/recovery proof. | Blockers visible; raw details collapsed |
|
||||
| Checks passed with warnings | Validation ready with warnings | Validation completed, but warnings require review before confirmation. | Preview can continue; execution remains gated by confirmation. | Review preview. | warning | yes | Validation complete with warnings; no execution/recovery proof. | Warnings visible; raw details collapsed |
|
||||
| Checks passed cleanly | Validation ready | Required validation checks completed for the current scope. | Preview can be reviewed before confirmation. | Review preview. | complete | yes | Validation complete; no execution/recovery proof. | Safe checks compact |
|
||||
|
||||
Step 3 must not show:
|
||||
|
||||
- `Safety checks completed` when blockers exist
|
||||
- `Graph works again`
|
||||
- `Technical startability`
|
||||
- `write-gate`
|
||||
- `hard-blocker`
|
||||
- raw provider credential exception
|
||||
- raw Graph exception
|
||||
|
||||
Product-safe provider wording:
|
||||
|
||||
`Provider readiness or restore prerequisites currently prevent real execution.`
|
||||
|
||||
## Step 4 - Preview
|
||||
|
||||
| State | Visible status | Reason | Impact | Primary next action | Gate state | Can continue? | Proof claims allowed? | Diagnostics |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| Validation incomplete | Preview unavailable | Run validation before generating preview. | Preview cannot be trusted without current validation. | Run safety checks. | blocked | no | No preview proof. | Collapsed |
|
||||
| Validation blockers exist | Preview blocked | Resolve blockers before preview can be generated. | Preview cannot proceed while blockers remain. | Review blockers. | blocked | no | No trusted preview proof. | Collapsed |
|
||||
| Preview not generated | Preview required | Generate a normalized preview before confirmation. | Confirmation remains unavailable until preview is current. | Generate preview. | required | no | Validation may be complete; preview proof unavailable. | Collapsed |
|
||||
| Preview generated with needs attention | Preview ready with review items | Preview is current and items require review. | Operator must review needs attention before confirmation. | Review preview and complete confirmation. | warning | yes if execution prerequisites allow | Current preview proof exists; no execution/recovery proof. | Needs attention visible; raw diff collapsed |
|
||||
| Preview generated cleanly | Preview ready | Preview is current for the selected scope. | Confirmation is the next gate before execution can be queued. | Complete confirmation. | complete | yes if execution prerequisites allow | Current preview proof exists; no execution/recovery proof. | Details collapsed |
|
||||
| Preview current but execution prerequisites unavailable | Preview ready, confirmation reviewable | Preview is current but real execution prerequisites are not available. | Confirmation can be reviewed, but real execution remains unavailable until prerequisites are resolved. | Review preview and continue to confirmation; resolve execution prerequisites before executing. | warning | yes, to confirmation only | Preview proof exists; execution proof unavailable. | Collapsed |
|
||||
|
||||
Preview decision summary must show:
|
||||
|
||||
- Total items reviewed
|
||||
- Items changed
|
||||
- Items unchanged
|
||||
- Items requiring review
|
||||
- Assignments changed
|
||||
- Scope tags changed
|
||||
- Blockers
|
||||
- Warnings
|
||||
- Next gate
|
||||
|
||||
Preview grouping order:
|
||||
|
||||
1. Needs attention
|
||||
2. Changes detected
|
||||
3. No changes detected
|
||||
4. All reviewed items
|
||||
|
||||
Preview must not render by default:
|
||||
|
||||
- every restore item as a large card
|
||||
- 100+ item cards
|
||||
- raw diff JSON
|
||||
- raw provider payload
|
||||
- repeated `Policy change preview` label for every item
|
||||
|
||||
## Step 5 - Confirm & Execute
|
||||
|
||||
| State | Visible status | Reason | Impact | Primary next action | Gate state | Can continue? | Proof claims allowed? | Diagnostics |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| Required gates incomplete | Confirmation unavailable | Required mappings, validation, preview, or prerequisites are incomplete. | Execution cannot be queued. | Complete required gate. | blocked | no | Operation proof unavailable; post-run evidence unavailable. | Collapsed |
|
||||
| Preview-only/dry-run selected | Preview-only ready | Operator is creating a preview-only restore run. | No real provider write will be queued from this state. | Create preview-only restore run. | complete for dry-run | yes if form valid | Preview-only intent; no execution/recovery proof. | Collapsed |
|
||||
| Execution prerequisites ready, acknowledgement missing | Confirmation required | Execution requires explicit acknowledgement and typed confirmation where supported. | Real execution remains unavailable until confirmation is satisfied. | Acknowledge impact and type confirmation. | required | no | Operation proof unavailable; post-run evidence unavailable. | Collapsed |
|
||||
| Confirmation satisfied | Ready to queue execution | Required gates are complete and operator confirmation is satisfied. | Real execution can be queued if RBAC/capability permits. | Create restore run / queue execution. | complete | yes | Execution intent may be queued; recovery still not verified until post-run evidence exists. | Collapsed |
|
||||
| Actor lacks capability | Execution unavailable | Current actor is not permitted to execute restore. | Execution cannot be queued for this actor. | Review permissions or keep preview-only. | blocked | no for execution | No execution proof or recovery proof. | Collapsed |
|
||||
|
||||
Required safety copy before execution:
|
||||
|
||||
- Operation proof unavailable before execution.
|
||||
- Post-run evidence unavailable before execution.
|
||||
- Recovery is not verified until post-run evidence exists.
|
||||
|
||||
Forbidden pre-execution claims:
|
||||
|
||||
- Recovery verified
|
||||
- Execution proof complete
|
||||
- Post-run evidence available
|
||||
- Customer-safe
|
||||
- Healthy
|
||||
- Fully restored
|
||||
|
||||
Execution availability requires:
|
||||
|
||||
- required mappings resolved or allowed skipped
|
||||
- validation not blocked
|
||||
- preview current
|
||||
- confirmation satisfied
|
||||
- RBAC/capability permits execution
|
||||
|
||||
## Restore Proof Panel Contract
|
||||
|
||||
Restore Proof is not the process flow. It shows proof basis:
|
||||
|
||||
| Proof item | Allowed pre-execution state | Allowed post-execution state only if repo-supported | Forbidden claim |
|
||||
|---|---|---|---|
|
||||
| Source backup | Selected / Available / Needs review / Blocked | N/A | source proves recovery |
|
||||
| Target environment | Selected / Available | N/A | target is healthy |
|
||||
| Requested by | Recorded / Unavailable | N/A | user approval beyond current action |
|
||||
| Restore scope | Selected / Needs review / Blocked | N/A | scope is safe to execute without validation |
|
||||
| Operation proof | Unavailable before execution | Available only after existing execution proof exists | execution proof complete before execution |
|
||||
| Post-run evidence | Unavailable before execution | Available / Unavailable only after existing repo-supported result evidence exists | recovery verified before evidence |
|
||||
| Diagnostics | Collapsed | Collapsed / support-gated | raw proof shown by default |
|
||||
|
||||
Allowed states:
|
||||
|
||||
- Available
|
||||
- Unavailable
|
||||
- Recorded
|
||||
- Selected
|
||||
- Needs review
|
||||
- Blocked
|
||||
- Collapsed
|
||||
|
||||
## Restore Safety Status Contract
|
||||
|
||||
| Step | Default safety display | Full gates visible by default? | Expansion behavior |
|
||||
|---|---|---|---|
|
||||
| Step 1 | Full Restore Safety Gates | yes | N/A |
|
||||
| Step 2 | Compact Restore Safety Status | no | Full gates only after explicit `View safety gates` |
|
||||
| Step 3 | Compact Restore Safety Status | no | Full gates only after explicit `View safety gates` |
|
||||
| Step 4 | Compact Restore Safety Status | no | Full gates only after explicit `View safety gates` |
|
||||
| Step 5 | Compact Restore Safety Status | no | Full gates only after explicit `View safety gates` |
|
||||
|
||||
Compact status must show:
|
||||
|
||||
- X of Y gates complete
|
||||
- Next gate
|
||||
- Execution state
|
||||
- View safety gates
|
||||
|
||||
The UI must not show compact and full gates by default at the same time for Step 2+.
|
||||
|
||||
## Navigation / Context Contract
|
||||
|
||||
Actions that leave the wizard must preserve context or be clearly secondary:
|
||||
|
||||
| Action | Context rule | Target behavior |
|
||||
|---|---|---|
|
||||
| Open group sync | Use active workspace/environment; no hardcoded IDs | Open new tab if leaving wizard |
|
||||
| View group sync operations | Use active workspace/environment and operation type filter if supported | Open new tab |
|
||||
| Open provider connection | Use active environment provider connection route | Open new tab |
|
||||
| Open operation proof | Use existing authorized OperationRun link | Open new tab if leaving wizard |
|
||||
| Open evidence | Use existing authorized evidence link only if repo-supported | Open new tab |
|
||||
| Open backup detail | Use existing authorized backup route only if repo-supported | Open new tab |
|
||||
|
||||
Rules:
|
||||
|
||||
- No hardcoded numeric workspace IDs.
|
||||
- No 404 URLs.
|
||||
- Use active workspace/environment context.
|
||||
- Label actions task-specifically.
|
||||
- Avoid generic primary CTAs such as `Operations`.
|
||||
|
||||
## Forbidden Default Copy
|
||||
|
||||
Default Restore Create UI must not show:
|
||||
|
||||
- `Graph works again`
|
||||
- `Technical startability`
|
||||
- `write-gate`
|
||||
- `hard-blocker`
|
||||
- raw provider credential exception
|
||||
- raw Graph exception
|
||||
- tenant as platform context when environment is meant
|
||||
- recovery verified before post-run evidence
|
||||
- execution proof complete before execution
|
||||
- post-run evidence available before execution
|
||||
- customer-safe/healthy/fully restored before proof exists
|
||||
462
specs/333-restore-create-ux-final-productization/spec.md
Normal file
@ -0,0 +1,462 @@
|
||||
# Feature Specification: Spec 333 - Restore Create UX Final Productization
|
||||
|
||||
- **Feature Branch**: `333-restore-create-ux-final-productization`
|
||||
- **Created**: 2026-05-26
|
||||
- **Status**: Draft
|
||||
- **Type**: Runtime UX productization / Restore Create wizard refinement / no new architecture
|
||||
- **Runtime posture**: Narrow UI/UX productization on top of Spec 332. No backend rewrite. No new flow system.
|
||||
- **Input**: User-provided Spec 333 draft, explicitly depending on Spec 332 Product Process Flow System v1.
|
||||
|
||||
## Dependency Context
|
||||
|
||||
Spec 333 builds on the completed Spec 332 foundation:
|
||||
|
||||
- `RestoreRunCreatePresenter` is the centralized Restore Create product-state contract.
|
||||
- Product Process Flow component/pattern already exists.
|
||||
- Step 1 full Restore Safety Gates already exists.
|
||||
- Step 2+ compact Restore Safety Status already exists.
|
||||
- Restore Proof panel already exists.
|
||||
- Mapping resolver, group picker, validation, preview, and confirm foundations already exist.
|
||||
|
||||
Spec 333 must not rebuild these foundations. It productizes the visible Restore Create wizard experience over the existing contract and components.
|
||||
|
||||
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: Restore Create now has the Spec 332 safety/proof foundation, but the visible wizard still needs final productization across all five steps so operators can decide whether it is safe to continue without reading raw fields, IDs, diagnostics, or internal validation details.
|
||||
- **Today's failure**: Operators can still encounter raw technical form sprawl, repeated helper text, item-flood preview, ambiguous next-gate copy, provider/startability wording, or proof language that could imply more certainty than the pre-execution state supports.
|
||||
- **User-visible improvement**: Every Restore Create step presents status, reason, impact, proof basis, blockers, and one primary next action using calm enterprise workflow hierarchy.
|
||||
- **Smallest enterprise-capable version**: Polish the existing Restore Create wizard, Blade components, presenter output usage, mapping resolver, group picker, validation, preview, confirmation, tests, and screenshots. Reuse Spec 332 state truth. No new persisted truth, backend behavior, migrations, packages, or architecture.
|
||||
- **Explicit non-goals**: No new Product Process Flow architecture, no new presenter, no backend rewrite, no OperationRun model changes, no ProviderGateway behavior changes, no Graph calls, no migrations, no packages, no Restore Run Detail productization, no post-execution result page productization, and no migration of unrelated surfaces into Product Process Flow.
|
||||
- **Permanent complexity imported**: Focused product copy, view layout refinements, deterministic presenter assertions, feature/browser coverage, and Spec 333 screenshots. No new source of truth or framework.
|
||||
- **Why now**: Restore is a high-risk operator workflow. Spec 332 made the state contract available; the next safe step is to remove final visible UX ambiguity before expanding restore proof/result work.
|
||||
- **Why not local**: The implementation stays local to Restore Create rendering, but it must use the existing centralized presenter contract so Blade fragments do not recreate independent gate/proof/next-action truth.
|
||||
- **Approval class**: Core Enterprise
|
||||
- **Red flags triggered**: High-risk restore workflow, dangerous action presentation, status/proof messaging. Defense: bounded UI productization, explicit proof boundaries, no backend rewrite, targeted tests, browser screenshots.
|
||||
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitat: 2 | Produktnahe: 2 | Wiederverwendung: 1 | **Gesamt: 11/12**
|
||||
- **Decision**: approve
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: tenant / environment-bound Restore Create wizard
|
||||
- **Primary Routes**:
|
||||
- `/admin/workspaces/{workspace}/environments/{environment}/restore-runs/create`
|
||||
- **Data Ownership**:
|
||||
- Existing tenant-owned `RestoreRun` draft state only.
|
||||
- Existing `BackupSet`, `BackupItem`, `EntraGroup`, provider connection, validation, preview, and restore safety derived state only.
|
||||
- No new table, migration, persisted state, queue, scheduler, storage, or env var.
|
||||
- **RBAC**:
|
||||
- Existing workspace membership and managed-environment capability checks remain authoritative.
|
||||
- Existing Create Restore Run access remains capability-gated.
|
||||
- Execution availability remains controlled by existing restore readiness, confirmation, and capability behavior.
|
||||
|
||||
## UI Surface Impact *(mandatory - UI-COV-001)*
|
||||
|
||||
Does this spec add, remove, rename, or materially change any reachable UI surface?
|
||||
|
||||
- [ ] No UI surface impact
|
||||
- [x] Existing page changed
|
||||
- [ ] New page/route added
|
||||
- [ ] Navigation changed
|
||||
- [ ] Filament panel/provider surface changed
|
||||
- [x] New modal/drawer/wizard/action added
|
||||
- [x] New table/form/state added
|
||||
- [ ] Customer-facing surface changed
|
||||
- [x] Dangerous action changed
|
||||
- [x] Status/evidence/review presentation changed
|
||||
- [x] Workspace/environment context presentation changed
|
||||
|
||||
## UI/Productization Coverage *(mandatory)*
|
||||
|
||||
- **Route/page/surface**: Restore Run Create wizard, including Select Backup Set, Define Restore Scope, Validate, Preview, and Confirm & Execute.
|
||||
- **Current or new page archetype**: Environment-bound dangerous workflow wizard.
|
||||
- **Design depth**: Strategic Surface / Manual Review Required because restore is operator-critical and high-risk.
|
||||
- **Repo-truth level**: repo-verified foundation via Spec 332, existing Restore Create code, and browser screenshots.
|
||||
- **Existing pattern reused**: Spec 332 Product Process Flow, RestoreRunCreatePresenter, restore safety/proof panels, Spec 325 restore safety target image direction, UI-014 restore runs page report.
|
||||
- **New pattern required**: none.
|
||||
- **Screenshot required**: yes, save to `specs/333-restore-create-ux-final-productization/artifacts/screenshots/`.
|
||||
- **Page audit required**: no new page audit required unless implementation discovers route/archetype drift; existing Restore Runs audit remains the coverage anchor.
|
||||
- **Customer-safe review required**: yes for proof and recovery claims; no customer-facing surface is added.
|
||||
- **Dangerous-action review required**: yes; confirmation/execution copy must not overclaim recovery, operation proof, post-run evidence, health, or customer safety.
|
||||
- **Coverage files updated or explicitly not needed**:
|
||||
- [ ] `docs/ui-ux-enterprise-audit/route-inventory.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/page-reports/...`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/strategic-surfaces.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/unresolved-pages.md`
|
||||
- [x] `N/A - existing route/archetype coverage remains valid; Spec 333 package screenshots provide focused proof unless implementation discovers coverage drift`
|
||||
- **No-impact rationale when applicable**: N/A - reachable UI is materially changed.
|
||||
|
||||
## Cross-Cutting / Shared Pattern Reuse *(mandatory)*
|
||||
|
||||
- **Cross-cutting feature?**: yes
|
||||
- **Interaction class(es)**: status messaging, wizard next actions, proof panels, diagnostics disclosure, action links, dangerous confirmation, browser screenshots.
|
||||
- **Systems touched**: Restore Create wizard, Product Process Flow, Restore Proof, restore safety copy, mapping resolver, group picker, validation summary, preview summary, confirm panel.
|
||||
- **Existing pattern(s) to extend**: Spec 332 Product Process Flow and Restore Create Presenter contract.
|
||||
- **Shared contract / presenter / builder / renderer to reuse**:
|
||||
- `apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-safety-gates.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-proof-aside.blade.php`
|
||||
- `apps/platform/resources/views/filament/forms/components/restore-run-*`
|
||||
- `BadgeRenderer` / `BadgeDomain` where badge semantics already exist
|
||||
- `OperationRunLinks` for operation navigation where already supported
|
||||
- **Why the existing shared path is sufficient or insufficient**: sufficient; Spec 332 created the shared flow/presenter foundation and this spec only tightens display, copy, grouping, tests, and screenshot proof.
|
||||
- **Allowed deviation and why**: none planned. If a view needs local layout polish, it must still render presenter contract state and not calculate independent truth.
|
||||
- **Consistency impact**: status, reason, impact, next action, gate state, proof, diagnostics, navigation labels, and copy guardrails must stay aligned across all five steps.
|
||||
- **Review focus**: prevent parallel gate/proof truth in Blade, prevent raw technical defaults, and verify no overclaiming before execution/post-run evidence.
|
||||
|
||||
## OperationRun UX Impact *(mandatory)*
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: yes, presentation only for pre-execution proof, execution readiness, and navigation links.
|
||||
- **Shared OperationRun UX contract/layer reused**: existing restore execution flow, `OperationRunLinks`, and existing OperationRun presentation paths only.
|
||||
- **Delegated start/completion UX behaviors**: existing create/execute behavior remains unchanged. Spec 333 may clarify visible readiness and pre-execution proof copy only.
|
||||
- **Local surface-owned behavior that remains**: restore initiation inputs, preview/dry-run toggle, typed confirmation, and wizard decision display.
|
||||
- **Queued DB-notification policy**: N/A - no new notification policy.
|
||||
- **Terminal notification path**: N/A - no terminal notification change.
|
||||
- **Exception required?**: none.
|
||||
|
||||
## Provider Boundary / Platform Core Check *(mandatory)*
|
||||
|
||||
- **Shared provider/platform boundary touched?**: yes, display-only provider readiness and directory group mapping copy.
|
||||
- **Boundary classification**: mixed, but display changes must preserve existing provider-owned seams.
|
||||
- **Seams affected**: provider connection readiness copy, directory group cache/group picker copy, target mapping labels, provider connection navigation.
|
||||
- **Neutral platform terms preserved or introduced**: environment, provider connection, target mapping, operation proof, post-run evidence, restore scope.
|
||||
- **Provider-specific semantics retained and why**: directory groups and object IDs remain provider-bound because the existing mapping workflow is Entra group based.
|
||||
- **Why this does not deepen provider coupling accidentally**: no Graph calls, no new contracts, no provider registry changes, and provider-specific language is limited to the existing group/provider boundary.
|
||||
- **Follow-up path**: none for Spec 333; broader provider readiness productization belongs to a later spec.
|
||||
|
||||
## UI / Surface Guardrail Impact *(mandatory)*
|
||||
|
||||
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Restore Create wizard | yes | Mixed Filament wizard plus existing custom Blade components | Product Process Flow, proof, status messaging, dangerous confirmation | page, wizard step, presenter contract, modal | no | High-risk productization surface |
|
||||
| Group picker modal | yes | Filament modal plus Livewire table | mapping resolver, navigation links | modal, Livewire table, environment context | no | Context and empty-state polish only |
|
||||
| Restore preview step | yes | Existing custom Blade component | preview summary, proof, diagnostics | wizard step, presenter preview summary | no | Summary-first layout, no backend change |
|
||||
| Confirm & Execute step | yes | Existing Filament fields plus custom panel | dangerous action / confirmation | wizard step, readiness and proof copy | no | High-friction proof-safe presentation |
|
||||
|
||||
## Decision-First Surface Role *(mandatory)*
|
||||
|
||||
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Restore Create wizard | Primary Decision Surface | Operator decides whether each restore gate can move forward | status, reason, impact, primary next action, gate status, proof basis | diagnostics, raw IDs, raw diffs, item detail | Primary because restore can change provider configuration | follows source -> scope -> validation -> preview -> confirmation | avoids raw form interpretation |
|
||||
| Restore Proof panel | Secondary Evidence / Proof | Operator checks what proof exists for the draft | source backup, target environment, requester, scope, operation proof, post-run evidence | diagnostics | Secondary because it supports the decision card | follows wizard step context | proof is visible without dominating |
|
||||
| Mapping resolver | Primary Decision Surface for Step 2 | Operator resolves dependencies before validation | resolved/unresolved/skipped counts, source group name, target group name/state | source/target IDs, manual fallback | Primary inside Step 2 only | prevents validation without dependency decision | hides rows until explicit action |
|
||||
| Preview detail | Secondary Context | Operator reviews what would change | decision summary, needs attention, change counts, next gate | item detail, raw diff JSON | Secondary to preview decision summary | preview precedes confirmation | avoids item-card flood |
|
||||
|
||||
## Audience-Aware Disclosure *(mandatory)*
|
||||
|
||||
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Restore Create wizard | operator-MSP, support-platform | status, reason, impact, next action, proof basis | validation groups, mapping rows, preview detail | raw IDs, raw diff JSON, provider payloads | state-specific next action | diagnostics and raw payloads | presenter contract owns truth once |
|
||||
| Group picker | operator-MSP | source group context and current environment cache state | cache staleness and operation links | object IDs as secondary metadata | choose cached target or enter manual object ID | raw IDs are secondary | source/target names are primary |
|
||||
| Confirm step | operator-MSP | confirmation decision summary and proof-safe warnings | readiness details | post-run evidence unavailable before execution | satisfy confirmation or keep preview-only | execution proof claims | proof copy explicitly says unavailable |
|
||||
|
||||
## UI/UX Surface Classification *(mandatory)*
|
||||
|
||||
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Restore Create wizard | Workflow / Wizard / Dangerous Action | Environment-bound restore workflow | continue to the next safe gate | wizard step progression | N/A | proof/diagnostics panels and modals | Confirm & Execute step with high friction | Restore Runs list | Restore Run detail after creation | workspace + environment route context | Restore Run | safety state, proof basis, blockers, next action | none |
|
||||
| Mapping resolver | Form / Modal / Dependency Resolution | Wizard subflow | resolve mappings | explicit resolver expansion and picker modal | table row selection in picker only | group sync links secondary/open new tab | skip assignment if supported and explicit | N/A | N/A | current environment cache | Mapping | resolved/unresolved/skipped and required state | none |
|
||||
|
||||
## Operator Surface Contract *(mandatory)*
|
||||
|
||||
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Restore Create wizard | Tenant operator / MSP operator | Decide whether restore can safely advance | Dangerous workflow wizard | Can this restore safely move forward, what blocks it, what proof exists, what next? | status, reason, impact, gate, proof, one next action | raw IDs, raw diffs, provider errors, raw payloads | source usability, dependency resolution, validation, preview, confirmation, execution/evidence | Preview-only simulation or provider restore execution depending on confirmation | select backup, define scope, resolve mappings, run checks, generate preview, confirm | real restore execution |
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: no
|
||||
- **New persisted entity/table/artifact?**: no, except Spec Kit screenshots/artifacts
|
||||
- **New abstraction?**: no
|
||||
- **New enum/state/reason family?**: no
|
||||
- **New cross-domain UI framework/taxonomy?**: no
|
||||
- **Current operator problem**: final Restore Create UX clarity and proof-safe decision support.
|
||||
- **Existing structure is insufficient because**: Spec 332 provides the contract/foundation, but visible step polish still needs explicit acceptance criteria and tests.
|
||||
- **Narrowest correct implementation**: display-only productization on existing presenter/components.
|
||||
- **Ownership cost**: focused tests and screenshots for a high-risk wizard.
|
||||
- **Alternative intentionally rejected**: new flow system, new presenter, backend rewrite, new state taxonomy, or new persisted proof model.
|
||||
- **Release truth**: current-release runtime UX productization.
|
||||
|
||||
### Compatibility posture
|
||||
|
||||
This feature assumes a pre-production environment. No backward compatibility shim, legacy alias, migration support, or historical-data compatibility path is in scope.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Feature/Livewire, Unit, Browser.
|
||||
- **Validation lane(s)**: confidence + browser; formatting with Pint; whitespace guard with `git diff --check`.
|
||||
- **Why this classification and these lanes are sufficient**: Restore Create is a high-risk Livewire/Filament wizard with presenter-driven state and browser-visible behavior. Unit tests prove presenter determinism; Feature/Livewire tests prove contract rendering and gating; Browser tests prove the visible workflow/screenshots.
|
||||
- **New or expanded test families**: Spec 333 focused Feature/Browser tests and presenter determinism additions.
|
||||
- **Fixture / helper cost impact**: use existing restore/backup/environment factories and browser helpers; avoid new broad fixture defaults.
|
||||
- **Heavy-family visibility / justification**: browser coverage is explicit and named because the wizard is a critical multi-step UI.
|
||||
- **Special surface test profile**: shared-detail-family / dangerous-workflow wizard / browser-smoke.
|
||||
- **Reviewer handoff**: reviewers must confirm lane fit, no hidden heavy-governance spread, no new backend behavior, and no false proof claims.
|
||||
- **Budget / baseline / trend impact**: expected bounded browser/feature additions only; no structural lane shift.
|
||||
- **Escalation needed**: document-in-feature.
|
||||
- **Active feature PR close-out entry**: Smoke Coverage.
|
||||
- **Planned validation commands**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Filament/Spec333RestoreCreateUxFinalProductizationTest.php tests/Feature/RestoreGroupMappingTest.php tests/Feature/Filament/RestoreWizardGraphSafetyTest.php tests/Feature/Filament/RestoreRunPreviewProductizationTest.php tests/Unit/Filament/RestoreRunCreatePresenterDeterminismTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec333RestoreCreateUxFinalProductizationSmokeTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=Spec332 --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec332* --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
|
||||
- `git diff --check`
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Judge source usability from Step 1 (Priority: P1)
|
||||
|
||||
As a restore operator, I want Step 1 to show whether a selected backup source is usable, why, what the impact is, what proof exists, and what I should do next.
|
||||
|
||||
**Why this priority**: Restore cannot safely proceed without a usable source. Step 1 is the first opportunity to prevent unsafe assumptions.
|
||||
|
||||
**Independent Test**: Render Step 1 with no backup, unusable backup, degraded usable backup, and clean usable backup; assert decision card, backup quality summary, full safety gates, Restore Proof, diagnostics collapsed, and no false recovery proof.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** no backup set is selected, **When** Step 1 renders, **Then** it shows `Source required`, reason, impact, and primary next action `Select backup set`.
|
||||
2. **Given** a selected backup has no captured/restorable items, **When** Step 1 renders, **Then** it shows `Source not usable` and directs the operator to select another backup.
|
||||
3. **Given** a selected usable backup has degraded or mapping issues, **When** Step 1 renders, **Then** it shows review-required source state and does not imply execution safety.
|
||||
4. **Given** a selected usable clean backup, **When** Step 1 renders, **Then** it shows source selected and continues to scope without claiming recovery is verified.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Define scope and resolve mappings without raw form sprawl (Priority: P1)
|
||||
|
||||
As a restore operator, I want Step 2 to show a calm scope summary and dependency mapping summary by default, then expand only when I explicitly choose to resolve mappings.
|
||||
|
||||
**Why this priority**: Dependency mapping must be resolved before validation can be trusted, but the default UI should not expose 20+ raw fields or raw GUIDs first.
|
||||
|
||||
**Independent Test**: Render Step 2 default and expanded resolver states with cached target groups, manual fallback, skipped mapping, and empty current-environment cache.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** unresolved mappings exist, **When** Step 2 renders by default, **Then** mapping details are hidden, summary counts are visible, compact safety status is visible, proof is visible, full gates are hidden, and `Resolve mappings` is available.
|
||||
2. **Given** the operator opens resolver mode, **When** mapping rows render, **Then** source group display name is primary, source ID is secondary, target group display name is primary when cached, target ID is secondary, and manual fallback/skipped states are explicit.
|
||||
3. **Given** the current environment has no cached groups, **When** the group picker opens, **Then** the empty state references current environment cache, shows context-safe CTAs, and manual fallback remains discoverable if supported.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Validate with product-safe blocker and provider readiness states (Priority: P1)
|
||||
|
||||
As a restore operator, I want validation to group blockers, warnings, and safe checks with product-safe copy so provider credential problems or validation blockers do not crash or overclaim success.
|
||||
|
||||
**Why this priority**: Validation gates preview and execution. Provider readiness and blocker messaging must be safe and understandable.
|
||||
|
||||
**Independent Test**: Render no-checks, provider credential missing, blocker, warning, and clean validation states; assert copy, grouped checks, next action, and no internal technical wording.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** no checks have run, **When** Step 3 renders, **Then** it shows `Validation required` and primary next action `Run safety checks`.
|
||||
2. **Given** provider credentials are missing, **When** Step 3 renders or checks are requested, **Then** the wizard shows `Validation blocked` with provider-safe copy and does not 500.
|
||||
3. **Given** checks finish with blockers, **When** the result appears, **Then** the UI and toast say blockers remain and do not show `Safety checks completed`.
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 - Review preview summary before item detail (Priority: P1)
|
||||
|
||||
As a restore operator, I want Preview to answer what will change before showing item details, so I can review needs attention, changes, unchanged items, blockers, warnings, and the next gate without item-card flooding.
|
||||
|
||||
**Why this priority**: Preview is the decision basis for confirmation; it must be summary-first and gate-consistent.
|
||||
|
||||
**Independent Test**: Generate preview with changed, unchanged, needs-attention, blocker, warning, and current/uncurrent validation states; assert summary-first order and state consistency.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** preview is current and validation is complete, **When** Step 4 renders, **Then** it shows Preview complete, Validation complete, Next gate confirmation required, and no stale validation-blocker copy.
|
||||
2. **Given** validation is incomplete, **When** Step 4 renders, **Then** preview is unavailable and Next gate is validation required.
|
||||
3. **Given** many unchanged items exist, **When** Step 4 renders, **Then** unchanged items are collapsed or compact and raw diff details are secondary.
|
||||
|
||||
---
|
||||
|
||||
### User Story 5 - Confirm execution with high friction and proof-safe copy (Priority: P1)
|
||||
|
||||
As a restore operator, I want Confirm & Execute to show exactly what is being approved and what proof does not exist yet before real execution can be queued.
|
||||
|
||||
**Why this priority**: Execution is the dangerous step. The UI must not imply recovery, operation proof, or post-run evidence before execution and proof exist.
|
||||
|
||||
**Independent Test**: Render confirm states before and after required gates are complete; assert high-friction acknowledgement, typed confirmation where supported, execution availability, dry-run state, and proof-safe copy.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** required gates are incomplete, **When** Confirm renders, **Then** execution is unavailable and the operator sees what remains blocked.
|
||||
2. **Given** mappings, validation, preview, confirmation, and capability requirements are satisfied, **When** Confirm renders, **Then** execution can be queued only after high-friction acknowledgement.
|
||||
3. **Given** execution has not run, **When** Confirm renders, **Then** operation proof and post-run evidence are unavailable and recovery is not verified.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
- **FR-001**: Step 1 MUST render Restore Safety decision card, backup selector, backup quality summary, full Restore Safety Gates, Restore Proof panel, and collapsed diagnostics.
|
||||
- **FR-002**: Step 1 MUST represent no backup selected as `Source required` with reason, impact, and primary next action to select a backup set.
|
||||
- **FR-003**: Step 1 MUST represent selected backup with no captured/restorable items as `Source not usable` and direct the operator to select another backup set.
|
||||
- **FR-004**: Step 1 MUST represent usable degraded source state as review required and avoid execution-safety or recovery-proof claims.
|
||||
- **FR-005**: Backup quality summary MUST show items captured, metadata-only items, degraded items, assignment/mapping issues, orphaned assignments, and the input-quality caveat.
|
||||
- **FR-006**: Step 2 default MUST show define-scope card, scope options, scope impact summary, mapping summary, resolve mappings action, compact Restore Safety Status, Restore Proof, and collapsed diagnostics.
|
||||
- **FR-007**: Step 2 default MUST NOT show full mapping details, 20+ raw mapping fields, full seven-gate flow, repeated GUID helper text, or raw IDs as primary labels.
|
||||
- **FR-008**: Step 2 scope summary MUST show all items, selected items only, scope impact, and next safety gate; if item-level selection is not supported at runtime, the UI MUST say it explicitly.
|
||||
- **FR-009**: Mapping summary MUST show resolved, unresolved, skipped, manual fallback count when used, and whether mapping is required before validation can run.
|
||||
- **FR-010**: Mapping resolver expanded mode MUST show `Resolve target mappings`, progress summary, rows, and exactly one collapse action.
|
||||
- **FR-011**: Mapping rows MUST show source group display name first, source ID as secondary metadata, cached target display name first, target ID as secondary metadata, manual fallback badge when manually entered, and skipped state when intentionally skipped.
|
||||
- **FR-012**: Mapping rows MUST NOT use raw GUIDs, state labels, or `Unresolved (...id)` as primary source labels.
|
||||
- **FR-013**: Skip assignment behavior MUST be explicit when supported or explicitly unavailable when not supported; `SKIP` MUST NOT be primary UX.
|
||||
- **FR-014**: Group picker MUST show source context, only current-environment cached groups, current-environment empty state, context-safe CTAs, no hardcoded workspace IDs, no 404 links, and manual fallback if supported.
|
||||
- **FR-015**: Step 3 MUST show validation summary, grouped blockers/warnings/safe checks, compact Restore Safety Status, Restore Proof, and collapsed diagnostics.
|
||||
- **FR-016**: Step 3 MUST show provider credential missing as product-safe validation blocked state, not a raw exception or 500.
|
||||
- **FR-017**: Step 3 toast/alert copy MUST NOT show `Safety checks completed` when blockers exist.
|
||||
- **FR-018**: Step 3 default UI MUST NOT show `Graph works again`, `technical startability`, `write-gate`, `hard-blocker`, raw provider credential exception, raw Graph exception, or platform-context misuse of `tenant`.
|
||||
- **FR-019**: Step 4 Preview MUST be summary-first with decision summary, needs attention, change summary, compact/collapsed unchanged items, compact Restore Safety Status, Restore Proof, and collapsed diagnostics.
|
||||
- **FR-020**: Step 4 MUST NOT render every restore item as a large card, raw diff JSON, raw provider payload, or repeated `Policy change preview` label by default.
|
||||
- **FR-021**: Step 4 MUST group preview content into Needs attention, Changes detected, No changes detected, and All reviewed items, with raw details behind disclosure.
|
||||
- **FR-022**: Step 4 MUST keep next-gate copy consistent with actual validation/preview state.
|
||||
- **FR-023**: Step 5 MUST show confirmation decision summary, preview summary, execution readiness, high-friction acknowledgement, dry-run/preview-only state if supported, typed confirmation/impact acknowledgement if supported, compact Restore Safety Status, Restore Proof, and collapsed diagnostics.
|
||||
- **FR-024**: Step 5 MUST show pre-execution proof-safe copy: operation proof unavailable before execution, post-run evidence unavailable before execution, and recovery not verified until post-run evidence exists.
|
||||
- **FR-025**: Step 5 MUST NOT claim recovery verified, execution proof complete, post-run evidence available, customer-safe, healthy, or fully restored before actual execution and proof exist.
|
||||
- **FR-026**: Execution availability MUST remain gated by required mappings resolved or explicitly allowed skipped, validation not blocked, preview current, confirmation satisfied, and existing RBAC/capability permission.
|
||||
- **FR-027**: Restore Proof MUST remain proof basis, not the process flow, and MUST use allowed states only.
|
||||
- **FR-028**: Step 1 MUST show full Restore Safety Gates; Step 2+ MUST show compact Restore Safety Status by default with full gates only after explicit View safety gates.
|
||||
- **FR-029**: Actions leaving the wizard MUST preserve active workspace/environment context, open secondary flows in a new tab when appropriate, and use task-specific labels.
|
||||
- **FR-030**: `RestoreRunCreatePresenter` MUST remain the single source of truth for gate/proof/next-action state; Blade components MUST render contract state only.
|
||||
- **FR-031**: Presenter behavior MUST remain deterministic with no static process-level memoization or fixture state leaks.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
- **NFR-001**: The UI MUST remain calm, enterprise-focused, scan-first, and low-noise.
|
||||
- **NFR-002**: Diagnostics, raw payloads, raw IDs, and raw diffs MUST be secondary or collapsed by default.
|
||||
- **NFR-003**: Long lists MUST render as tables, compact lists, or collapsed disclosures, not large repeated cards.
|
||||
- **NFR-004**: Copy MUST be provider-safe and avoid false recovery, health, compliance, or customer-safe claims.
|
||||
- **NFR-005**: No new application dependencies, assets, migrations, queues, scheduler changes, storage changes, or environment variables may be introduced.
|
||||
|
||||
## Data / Truth Source Requirements
|
||||
|
||||
- **DTR-001**: Restore Create product state truth remains derived from existing RestoreRun draft data, restore safety resolver state, preview/check integrity, backup quality, provider readiness, mapping progress, and existing route-bound environment context.
|
||||
- **DTR-002**: Pre-execution operation proof and post-run evidence MUST be represented as unavailable unless existing repo behavior proves otherwise after execution.
|
||||
- **DTR-003**: Mapping identity MUST prefer cached display names and use object IDs only as secondary metadata or manual fallback input.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- New Product Process Flow architecture.
|
||||
- New Restore Create presenter or replacement presenter.
|
||||
- Backend rewrite.
|
||||
- OperationRun model changes.
|
||||
- ProviderGateway behavior changes.
|
||||
- New Graph calls.
|
||||
- Migrations or model changes.
|
||||
- New packages or assets.
|
||||
- Scheduler, queue, storage, or env var changes.
|
||||
- Restore Run Detail productization.
|
||||
- Post-execution Restore Result page productization.
|
||||
- Baseline Compare, Evidence path, Customer Review, or unrelated surface migration to Product Process Flow.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- No backup set selected.
|
||||
- Selected backup has zero captured items.
|
||||
- Selected backup contains only metadata-only items.
|
||||
- Selected backup is usable but degraded.
|
||||
- Unresolved required mappings block validation.
|
||||
- Skipped mappings are supported or explicitly unavailable.
|
||||
- Manual target object ID entered without cache match.
|
||||
- Current environment has no cached directory groups.
|
||||
- Provider credentials are missing.
|
||||
- Validation finishes with blockers and warnings.
|
||||
- Preview is stale, missing, blocked, current, or generated with many unchanged items.
|
||||
- Confirmation is attempted before gates complete.
|
||||
- Actor lacks required capability.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Spec 332 code and tests are the foundation to preserve.
|
||||
- Restore Create item-level selection is repo-supported today; if implementation discovers a limited state, UI copy must say so rather than inventing support.
|
||||
- Skip assignment is repo-supported through existing mapping behavior; implementation must keep it explicit or document a tested unsupported branch if current behavior differs.
|
||||
- `RestoreRunResource` remains the relevant Filament resource and the Admin panel remains registered through existing Laravel 12 provider registration.
|
||||
- Browser screenshots may reuse existing Spec 332 fixture patterns but must save under the Spec 333 artifact path.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Presenter drift**: Blade components could recreate gate/proof truth. Mitigation: tests assert presenter contract usage and determinism.
|
||||
- **Overclaiming proof**: Copy could imply execution or recovery proof before execution. Mitigation: forbidden-copy tests and proof-state assertions.
|
||||
- **Browser fixture cost**: Nine screenshots can become heavy. Mitigation: explicit browser lane, shared setup, targeted states only.
|
||||
- **Scope creep**: Restore Run Detail or post-execution result productization could leak in. Mitigation: hard non-goals and tasks stop at Restore Create.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None blocking preparation. If implementation discovers a repo-truth mismatch in skip support, item-level selection, or proof-link availability, update this spec/plan before changing runtime behavior.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Product
|
||||
|
||||
- [ ] Restore Create Wizard feels like one coherent safety workflow.
|
||||
- [ ] Every step has clear status, reason, impact, and next action.
|
||||
- [ ] No step exposes raw technical form sprawl by default.
|
||||
- [ ] Safety status and restore proof are visible where needed.
|
||||
- [ ] Preview is summary-first, not item-flood-first.
|
||||
- [ ] Confirmation is high-friction and proof-safe.
|
||||
|
||||
### Safety
|
||||
|
||||
- [ ] No false recovery proof.
|
||||
- [ ] No false execution proof.
|
||||
- [ ] No false post-run evidence.
|
||||
- [ ] No unsafe progression through required mappings or validation blockers.
|
||||
- [ ] Provider credential failure is product-safe, not 500.
|
||||
|
||||
### UX
|
||||
|
||||
- [ ] Mapping details hidden by default.
|
||||
- [ ] Resolver is clear when expanded.
|
||||
- [ ] Group picker keeps restore context.
|
||||
- [ ] Long lists use tables/collapsed sections.
|
||||
- [ ] No repeated helper copy.
|
||||
- [ ] No clipped status badges.
|
||||
- [ ] No generic primary CTAs that lose context.
|
||||
|
||||
### Technical
|
||||
|
||||
- [ ] `RestoreRunCreatePresenter` remains single source of truth.
|
||||
- [ ] No process-level static memoization.
|
||||
- [ ] Blade components render contract state only.
|
||||
- [ ] No new backend model/migration.
|
||||
- [ ] No new packages/assets.
|
||||
- [ ] RBAC/capability behavior preserved.
|
||||
|
||||
### Validation
|
||||
|
||||
- [ ] Required feature tests pass.
|
||||
- [ ] Required browser tests pass.
|
||||
- [ ] Screenshots captured.
|
||||
- [ ] Pint passes.
|
||||
- [ ] `git diff --check` passes.
|
||||
- [ ] Full suite status honestly reported if run/not run.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- **SC-001**: A reviewer can inspect each wizard step and identify the visible status, reason, impact, proof basis, blocker, and next action within five seconds.
|
||||
- **SC-002**: Required tests prove no false pre-execution recovery, execution proof, or post-run evidence claims.
|
||||
- **SC-003**: Browser screenshots cover the nine required states in the Spec 333 artifact directory or document unreachable states with repo-truth reasons.
|
||||
- **SC-004**: No application diff outside the Restore Create UI/test/screenshot scope is required for implementation.
|
||||
|
||||
## Required Browser Screenshots
|
||||
|
||||
Save under `specs/333-restore-create-ux-final-productization/artifacts/screenshots/`:
|
||||
|
||||
1. `01-step-1-backup-selected.png`
|
||||
2. `02-step-2-scope-default.png`
|
||||
3. `03-step-2-resolver-expanded.png`
|
||||
4. `04-step-2-group-picker-results.png`
|
||||
5. `05-step-2-group-picker-empty.png`
|
||||
6. `06-step-3-validation-blocked.png`
|
||||
7. `07-step-3-validation-passed.png`
|
||||
8. `08-step-4-preview-generated.png`
|
||||
9. `09-step-5-confirm-ready.png`
|
||||
|
||||
If a state is not reachable, the implementation close-out must document why.
|
||||
|
||||
## Follow-Up Spec Candidates
|
||||
|
||||
Do not include these in Spec 333:
|
||||
|
||||
- Restore Run Detail / Post-Execution Proof Productization (number TBD; `334-*` already exists in this repo)
|
||||
- Baseline Compare Product Process Flow Alignment (number TBD)
|
||||
- Evidence Path Product Process Flow Alignment (number TBD)
|
||||
- Provider Readiness Productization (number TBD)
|
||||
170
specs/333-restore-create-ux-final-productization/tasks.md
Normal file
@ -0,0 +1,170 @@
|
||||
# Tasks: Spec 333 - Restore Create UX Final Productization
|
||||
|
||||
**Input**: `specs/333-restore-create-ux-final-productization/spec.md`, `specs/333-restore-create-ux-final-productization/plan.md`, `specs/333-restore-create-ux-final-productization/restore-create-state-contract.md`
|
||||
|
||||
**Tests**: Required. This spec changes an existing high-risk Filament/Livewire wizard and must include Feature/Livewire, Unit presenter determinism, and Browser smoke/screenshot coverage.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
|
||||
- [x] New or changed tests stay in the smallest honest family, and browser additions are explicit.
|
||||
- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default.
|
||||
- [x] Planned validation commands cover the change without pulling in unrelated lane cost.
|
||||
- [x] The dangerous-workflow/browser-smoke surface profile is explicit.
|
||||
- [x] Any material budget, baseline, trend, or escalation note is recorded in the active spec or PR.
|
||||
|
||||
## Phase 1: Repo Truth and State Contract
|
||||
|
||||
**Purpose**: Confirm Spec 333 is display/productization over Spec 332, not a new architecture.
|
||||
|
||||
- [x] T001 Re-read `specs/333-restore-create-ux-final-productization/spec.md`, `specs/333-restore-create-ux-final-productization/plan.md`, `specs/333-restore-create-ux-final-productization/tasks.md`, and `specs/333-restore-create-ux-final-productization/restore-create-state-contract.md`.
|
||||
- [x] T002 Re-read Spec 332 artifacts in `specs/332-product-process-flow-system-v1/spec.md`, `specs/332-product-process-flow-system-v1/plan.md`, and `specs/332-product-process-flow-system-v1/tasks.md`.
|
||||
- [x] T003 Inspect existing Restore Create surfaces before editing: `apps/platform/app/Filament/Resources/RestoreRunResource.php`, `apps/platform/app/Filament/Resources/RestoreRunResource/Pages/CreateRestoreRun.php`, and `apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php`.
|
||||
- [x] T004 Inspect existing Restore Create views under `apps/platform/resources/views/filament/forms/components/restore-run-*.blade.php` and group picker views `apps/platform/resources/views/livewire/entra-group-cache-picker-table.blade.php` and `apps/platform/resources/views/filament/modals/entra-group-cache-picker.blade.php`.
|
||||
- [x] T005 Confirm `RestoreRunCreatePresenter` remains the single source of truth for status, reason, impact, primary next action, gates, proof items, mapping summary, validation summary, preview summary, can-continue, blocked reason, and diagnostics state.
|
||||
- [x] T006 Confirm no static process-level memoization or fixture state leakage exists in `RestoreRunCreatePresenter`; document any issue in implementation notes before changing runtime.
|
||||
- [x] T007 Confirm implementation will not add migrations, models, packages, assets, env vars, queues, scheduler changes, storage changes, Graph calls, provider gateway behavior, or a new flow system.
|
||||
|
||||
## Phase 2: Tests First - Presenter and Step Contracts
|
||||
|
||||
**Purpose**: Add focused tests before or alongside UI changes.
|
||||
|
||||
- [x] T008 [P] Create `apps/platform/tests/Feature/Filament/Spec333RestoreCreateUxFinalProductizationTest.php` with Step 1-5 rendering and forbidden-copy coverage.
|
||||
- [x] T009 [P] Extend `apps/platform/tests/Unit/Filament/RestoreRunCreatePresenterDeterminismTest.php` for no static memoization, independent fixture state, metadata-only backup not leaking into usable backup state, and mapping state isolation.
|
||||
- [x] T010 [P] Extend `apps/platform/tests/Feature/RestoreGroupMappingTest.php` for mapping row identity, cached target label, manual fallback label, skip state, current-environment group cache, empty cache, and no hardcoded workspace IDs.
|
||||
- [x] T011 [P] Extend `apps/platform/tests/Feature/Filament/RestoreWizardGraphSafetyTest.php` for provider credential missing blocked state, product-safe validation copy, no raw provider/Graph exception, and no 500.
|
||||
- [x] T012 [P] Extend `apps/platform/tests/Feature/Filament/RestoreRunPreviewProductizationTest.php` for summary-first preview, needs-attention first, unchanged items collapsed/compact, no stale validation next-gate copy, and raw details behind disclosure.
|
||||
|
||||
## Phase 3: Step 1 - Select Backup Set Polish
|
||||
|
||||
**Purpose**: Make source usability clear before restore scope/validation.
|
||||
|
||||
- [x] T013 Update `apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php` display-only contract output as needed so no-backup state shows `Source required`, reason, impact, and primary next action `Select backup set`.
|
||||
- [x] T014 Update `RestoreRunCreatePresenter` source-state output so unusable backup with no captured/restorable items shows `Source not usable`, safe reason, impact, and primary next action `Select another backup set`.
|
||||
- [x] T015 Update `RestoreRunCreatePresenter` source-state output so usable degraded/mapping issue state shows review-required copy without implying execution safety.
|
||||
- [x] T016 Update `RestoreRunCreatePresenter` source-state output so clean usable backup state shows `Source selected` and primary next action `Continue to scope`.
|
||||
- [x] T017 Update `apps/platform/resources/views/filament/forms/components/restore-run-safety-decision.blade.php` only if needed to render the contract without adding independent gate/proof logic.
|
||||
- [x] T018 Update `apps/platform/resources/views/filament/forms/components/restore-run-backup-quality-summary.blade.php` so required quality counts and caveat are visible and no forbidden copy appears.
|
||||
- [x] T019 Verify Step 1 still renders full Restore Safety Gates via `apps/platform/resources/views/filament/forms/components/restore-run-safety-gates.blade.php` and Restore Proof via `apps/platform/resources/views/filament/forms/components/restore-run-proof-aside.blade.php`.
|
||||
|
||||
## Phase 4: Step 2 - Scope, Mapping Resolver, Group Picker
|
||||
|
||||
**Purpose**: Keep Step 2 calm by default and make dependency resolution operator-safe.
|
||||
|
||||
- [x] T020 Update `apps/platform/resources/views/filament/forms/components/restore-run-scope-summary.blade.php` to show scope options, scope impact, next safety gate, mapping summary, and `Resolve mappings` without exposing raw mapping rows by default.
|
||||
- [x] T021 Ensure Step 2 default hides full mapping details, full safety gates, repeated GUID helper text, and raw IDs as primary labels in `apps/platform/app/Filament/Resources/RestoreRunResource.php`.
|
||||
- [x] T022 Update `RestoreRunCreatePresenter` mapping summary output as needed to expose resolved, unresolved, skipped, manual fallback count, and required-before-validation copy.
|
||||
- [x] T023 Update `apps/platform/resources/views/filament/forms/components/restore-run-mapping-resolver-summary.blade.php` so expanded resolver mode shows `Resolve target mappings`, progress summary, and exactly one collapse action.
|
||||
- [x] T024 Update mapping row rendering in `apps/platform/app/Filament/Resources/RestoreRunResource.php` so source group display name is primary, source ID is secondary metadata, and `Unknown source group` fallback is allowed only with `Source ID: {id}`.
|
||||
- [x] T025 Update mapping helper/rendering so cached target display name is primary, target ID is secondary, and manual GUID entry is labeled `Manual fallback`.
|
||||
- [x] T026 Preserve explicit skip behavior in `apps/platform/app/Filament/Resources/RestoreRunResource.php` and `apps/platform/resources/views/filament/forms/components/restore-run-group-mapping-skipped.blade.php`, or update spec/plan first if repo truth proves skip is unsupported.
|
||||
- [x] T027 Update `apps/platform/resources/views/livewire/entra-group-cache-picker-table.blade.php` and `apps/platform/app/Livewire/EntraGroupCachePickerTable.php` so group picker results are scoped to the current environment and hide cross-workspace/environment groups.
|
||||
- [x] T028 Update `apps/platform/resources/views/filament/modals/entra-group-cache-picker.blade.php` and picker table empty state so no-cache copy, `Open group sync`, `View group sync operations`, and manual fallback are task-specific, context-safe, and open secondary flows in new tabs.
|
||||
- [x] T029 Verify group picker URLs use active workspace/environment context and no hardcoded numeric workspace IDs or 404 links.
|
||||
- [x] T030 Verify Next remains blocked when unresolved required mappings exist and the blocked copy is `Required before validation can run.`
|
||||
|
||||
## Phase 5: Step 3 - Validation Productization
|
||||
|
||||
**Purpose**: Make validation states product-safe and gate-correct.
|
||||
|
||||
- [x] T031 Update `RestoreRunCreatePresenter` validation summary output as needed for no-checks, provider-credentials-missing, blockers, warnings, and clean states.
|
||||
- [x] T032 Update `apps/platform/resources/views/filament/forms/components/restore-run-checks.blade.php` so blockers, warnings, and safe checks are grouped and diagnostics remain collapsed.
|
||||
- [x] T033 Update validation notification copy in `apps/platform/app/Filament/Resources/RestoreRunResource.php` so blockers never show `Safety checks completed`; use `Safety checks finished with blockers` or equivalent.
|
||||
- [x] T034 Ensure provider credentials missing shows `Validation blocked` with product-safe copy and `Review provider connection` action, without raw provider credential/Graph exception.
|
||||
- [x] T035 Verify Step 3 does not display forbidden terms: `Graph works again`, `Technical startability`, `write-gate`, `hard-blocker`, raw Provider credential exception, or raw Graph exception.
|
||||
- [x] T036 Verify Step 3 Next remains blocked when provider readiness or validation blockers exist.
|
||||
|
||||
## Phase 6: Step 4 - Preview Productization
|
||||
|
||||
**Purpose**: Make Preview summary-first and gate-consistent.
|
||||
|
||||
- [x] T037 Update `RestoreRunCreatePresenter` preview summary output as needed to expose total reviewed, changed, unchanged, needs review, assignments changed, scope tags changed, blockers, warnings, and next gate.
|
||||
- [x] T038 Update `apps/platform/resources/views/filament/forms/components/restore-run-preview.blade.php` so the restore preview decision summary appears before item details.
|
||||
- [x] T039 Update preview rendering so `Needs attention` is visible first, changed items are grouped, unchanged/no-change items are collapsed or compact, and all reviewed items remain available behind disclosure.
|
||||
- [x] T040 Ensure raw diff JSON, raw provider payload, and repeated item-card labels are hidden by default.
|
||||
- [x] T041 Fix preview next-gate copy so preview-current state shows validation complete, preview complete, and next gate confirmation required.
|
||||
- [x] T042 Fix preview blocked/not-generated copy so validation-required, blocker, and generate-preview states are accurate and do not show stale `resolve technical blockers` copy when preview is current.
|
||||
|
||||
## Phase 7: Step 5 - Confirmation Productization
|
||||
|
||||
**Purpose**: Preserve high friction and proof-safe execution copy.
|
||||
|
||||
- [x] T043 Update `RestoreRunCreatePresenter` confirmation/readiness output as needed so confirmation shows preview summary, execution readiness, and proof-safe pre-execution warnings.
|
||||
- [x] T044 Update `apps/platform/resources/views/filament/forms/components/restore-run-confirm-panel.blade.php` to show confirmation decision summary, dry-run/preview-only state, high-friction acknowledgement, and proof-safe copy.
|
||||
- [x] T045 Ensure `apps/platform/app/Filament/Resources/RestoreRunResource.php` keeps execution unavailable until required mappings resolved or explicitly skipped, validation not blocked, preview current, confirmation satisfied, and existing RBAC/capability permits execution.
|
||||
- [x] T046 Verify Confirm does not claim recovery verified, execution proof complete, post-run evidence available, customer-safe, healthy, or fully restored before execution and proof exist.
|
||||
|
||||
## Phase 8: Browser Screenshots
|
||||
|
||||
**Purpose**: Prove visible states required by Spec 333.
|
||||
|
||||
- [x] T047 Create `apps/platform/tests/Browser/Spec333RestoreCreateUxFinalProductizationSmokeTest.php` using existing Spec 332 browser conventions where possible.
|
||||
- [x] T048 Capture `specs/333-restore-create-ux-final-productization/artifacts/screenshots/01-step-1-backup-selected.png`.
|
||||
- [x] T049 Capture `specs/333-restore-create-ux-final-productization/artifacts/screenshots/02-step-2-scope-default.png`.
|
||||
- [x] T050 Capture `specs/333-restore-create-ux-final-productization/artifacts/screenshots/03-step-2-resolver-expanded.png`.
|
||||
- [x] T051 Capture `specs/333-restore-create-ux-final-productization/artifacts/screenshots/04-step-2-group-picker-results.png`.
|
||||
- [x] T052 Capture `specs/333-restore-create-ux-final-productization/artifacts/screenshots/05-step-2-group-picker-empty.png`.
|
||||
- [x] T053 Capture `specs/333-restore-create-ux-final-productization/artifacts/screenshots/06-step-3-validation-blocked.png`.
|
||||
- [x] T054 Capture `specs/333-restore-create-ux-final-productization/artifacts/screenshots/07-step-3-validation-passed.png`.
|
||||
- [x] T054a Capture `specs/333-restore-create-ux-final-productization/artifacts/screenshots/07-step-3-validation-passed-dark.png`.
|
||||
- [x] T055 Capture `specs/333-restore-create-ux-final-productization/artifacts/screenshots/08-step-4-preview-generated.png`.
|
||||
- [x] T056 Capture `specs/333-restore-create-ux-final-productization/artifacts/screenshots/09-step-5-confirm-ready.png`.
|
||||
- [x] T057 If a screenshot state is not reachable, document the repo-truth reason in `specs/333-restore-create-ux-final-productization/tasks.md` close-out before marking validation complete.
|
||||
|
||||
## Phase 9: Final Validation
|
||||
|
||||
**Purpose**: Prove the bounded implementation and report honestly.
|
||||
|
||||
- [x] T058 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Filament/Spec333RestoreCreateUxFinalProductizationTest.php tests/Feature/RestoreGroupMappingTest.php tests/Feature/Filament/RestoreWizardGraphSafetyTest.php tests/Feature/Filament/RestoreRunPreviewProductizationTest.php tests/Unit/Filament/RestoreRunCreatePresenterDeterminismTest.php --compact`.
|
||||
- [x] T059 Run `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec333RestoreCreateUxFinalProductizationSmokeTest.php --compact`.
|
||||
- [x] T060 Run `cd apps/platform && ./vendor/bin/sail artisan test --filter=Spec332 --compact` if shared Spec 332 presenter/components changed.
|
||||
- [x] T061 Run `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec332* --compact` if shared Spec 332 browser-covered components changed.
|
||||
- [x] T062 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`.
|
||||
- [x] T063 Run `git diff --check`.
|
||||
- [x] T064 Report full suite status honestly if not run.
|
||||
- [x] T065 Confirm no migrations, models, packages, assets, env vars, queues, scheduler, storage, provider gateway behavior, Graph calls, new presenter, or new flow system were added.
|
||||
|
||||
## Close-out Evidence
|
||||
|
||||
- Lane assignment: focused Feature/Livewire + Unit presenter determinism + Browser smoke/screenshot coverage for a dangerous restore-create workflow.
|
||||
- Existing `RestoreGroupMappingTest.php` and `RestoreWizardGraphSafetyTest.php` already covered the requested group-picker/provider-safety surfaces; Spec 333 adds direct Step 1-5 productization coverage in `Spec333RestoreCreateUxFinalProductizationTest.php`.
|
||||
- Follow-up enterprise UI review adjustments: Step 1 now keeps the selected backup source as the primary decision, clean backup quality renders as `Available`, the create-wizard header uses compact fixed-width step pills with explicit light/dark surface separation and Filament `--primary-*` tokens, and Step 1 safety gates/proof are collapsed into a compact evidence summary instead of rendering full evidence surfaces by default.
|
||||
- Step 2 follow-up productization: scope now starts with a compact decision bar, the separate `Resolve mappings` CTA was removed, the `Resolve target mappings` section is the central mapping work area, restore gates/proof are collapsed into Step 2 `Restore evidence`, raw object IDs moved behind details, and `Skip assignment` uses warning treatment with explicit consequence copy.
|
||||
- Step 3 follow-up productization: validation now renders a compact `Validation decision`, provider-credential blocked state hides `Run checks` and shows only repair guidance plus `Review provider connection`, passed validation summarizes blockers/warnings/safe checks with safe details collapsed, stale helper text was replaced by state-aware copy, validation gates/proof are collapsed into `Validation evidence`, and light/dark surfaces have tested card/background separation.
|
||||
- Step 4 follow-up productization: preview now leads with `Preview evidence`, reads next gate/CTA/execution state from the central `wizard_gate` contract, allows confirmation review when preview evidence is current while real execution prerequisites remain unavailable, switches the hint action to `Regenerate preview` after evidence exists, keeps safety gates and restore proof behind a closed `View safety gates and restore proof` disclosure, removes the competing visible `Restore safety status` rail from the preview step, shows per-policy `Restore action`, `Policy diff`, `Assignments`, `Scope tags`, `Review reason`, and `Action` evidence in Preview details, renders diff parts as semantic chips (`added` success, `removed` danger, `changed` warning), and keeps warning/diff surfaces readable in dark mode.
|
||||
- Environment note: Sail could not run because Docker was not running, so equivalent local PHP/Pest commands were used.
|
||||
- Validation run: `cd apps/platform && php artisan test tests/Feature/Filament/Spec333RestoreCreateUxFinalProductizationTest.php tests/Feature/Filament/Spec332ProductProcessFlowSystemTest.php tests/Feature/Filament/RestoreRunPreviewProductizationTest.php tests/Unit/Filament/RestoreRunCreatePresenterDeterminismTest.php --compact` passed: 28 tests, 441 assertions.
|
||||
- Adjacent safety run: `cd apps/platform && php artisan test tests/Feature/RestoreGroupMappingTest.php tests/Feature/Filament/RestoreWizardGraphSafetyTest.php --compact` passed: 7 tests, 66 assertions.
|
||||
- Browser run: `cd apps/platform && php vendor/bin/pest tests/Browser/Spec333RestoreCreateUxFinalProductizationSmokeTest.php --compact` passed: 6 tests, 169 assertions.
|
||||
- Browser screenshot/shared-flow run: `cd apps/platform && php vendor/bin/pest tests/Browser/Spec332RestoreRunWizardProductProcessFlowSmokeTest.php tests/Browser/Spec332RestoreRunWizardProductProcessFlowScreenshotsTest.php --compact` passed: 13 tests, 208 assertions.
|
||||
- Formatting: `cd apps/platform && vendor/bin/pint app/Filament/Resources/RestoreRunResource.php app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php tests/Feature/Filament/Spec333RestoreCreateUxFinalProductizationTest.php tests/Feature/Filament/Spec332ProductProcessFlowSystemTest.php tests/Feature/Filament/RestoreRunPreviewProductizationTest.php tests/Browser/Spec333RestoreCreateUxFinalProductizationSmokeTest.php tests/Browser/Spec332RestoreRunWizardProductProcessFlowSmokeTest.php` passed.
|
||||
- Whitespace: `git diff --check` passed.
|
||||
- Screenshot artifacts captured: `01-step-1-backup-selected.png`, `02-step-2-scope-default.png`, `03-step-2-resolver-expanded.png`, `04-step-2-group-picker-results.png`, `05-step-2-group-picker-empty.png`, `06-step-3-validation-blocked.png`, `07-step-3-validation-passed.png`, `07-step-3-validation-passed-dark.png`, `08-step-4-preview-generated.png`, `09-step-5-confirm-ready.png`, `10-step-4-preview-execution-prerequisites-unavailable.png`, and `11-step-5-execution-prerequisites-locked.png`.
|
||||
- Scope confirmation: no migrations, models, packages, assets, env vars, queues, scheduler changes, storage changes, Graph calls, provider gateway behavior changes, new presenter, or new flow system were added.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Phase 1 blocks all runtime edits.
|
||||
- Phase 2 should be completed before or alongside Phases 3-7.
|
||||
- Phases 3-7 may be worked in focused slices but must preserve presenter-contract truth.
|
||||
- Phase 8 requires runtime UI states from Phases 3-7.
|
||||
- Phase 9 closes the spec.
|
||||
|
||||
## Parallel Work Examples
|
||||
|
||||
- T008, T009, T010, T011, and T012 can be drafted in parallel because they target different test files.
|
||||
- Step 1 view polish and Step 3 validation copy can be reviewed independently if they do not change shared presenter keys.
|
||||
- Browser screenshot capture must wait until the visible states exist.
|
||||
|
||||
## Explicit Non-Goals
|
||||
|
||||
- [x] NT001 Do not create a new Product Process Flow architecture.
|
||||
- [x] NT002 Do not create a new Restore Create presenter.
|
||||
- [x] NT003 Do not rewrite restore backend behavior.
|
||||
- [x] NT004 Do not change `OperationRun` model semantics.
|
||||
- [x] NT005 Do not change ProviderGateway behavior or add Graph calls.
|
||||
- [x] NT006 Do not create migrations or model changes.
|
||||
- [x] NT007 Do not add packages, assets, env vars, queues, scheduler, or storage changes.
|
||||
- [x] NT008 Do not productize Restore Run Detail or post-execution result pages in this spec.
|
||||
- [x] NT009 Do not migrate Baseline Compare, Evidence path, Customer Review, or other surfaces into Product Process Flow in this spec.
|
||||
- [x] NT010 Do not introduce false recovery-proof, execution-proof, post-run evidence, health, compliance, or customer-safe claims.
|
||||