From c0c3286a8036ed49dbe06c1d62df20f00cd8969d Mon Sep 17 00:00:00 2001 From: ahmido Date: Sat, 20 Jun 2026 12:51:12 +0000 Subject: [PATCH] feat: add restore readiness resolution adapter improvements (#461) Automated PR created by Codex via Gitea API. Co-authored-by: Ahmed Darrazi Reviewed-on: https://git.cloudarix.de/ahmido/TenantAtlas/pulls/461 --- .../Filament/Resources/RestoreRunResource.php | 2 + .../Presenters/RestoreRunCreatePresenter.php | 12 + .../Presenters/RestoreRunDetailPresenter.php | 9 + .../EnvironmentDashboardSummaryBuilder.php | 7 +- .../RestoreGuidanceBasis.php | 40 + .../RestoreReadinessAction.php | 64 + .../RestoreReadinessReason.php | 47 + .../RestoreReadinessResolver.php | 247 +++ .../RestoreReadinessState.php | 45 + .../RestoreReadinessSummary.php | 39 + .../restore-run-confirm-panel.blade.php | 21 + .../restore-run-safety-decision.blade.php | 21 +- .../entries/restore-results.blade.php | 13 + ...reCreateUxFinalProductizationSmokeTest.php | 61 +- ...estoreRunDetailProductizationSmokeTest.php | 8 + ...nantDashboardProductizationSummaryTest.php | 37 +- .../Spec390RestoreReadinessGuidanceTest.php | 268 +++ .../RestoreReadinessResolverTest.php | 205 ++ .../design-coverage-matrix.md | 4 +- .../page-reports/ui-014-restore-runs.md | 6 +- .../ui-ux-enterprise-audit/route-inventory.md | 2 +- .../restore-safety-workflow.md | 4 +- .../unresolved-pages.md | 2 +- .../current-restore-flow-inventory.md | 35 + .../checklists/requirements.md | 92 + .../restore-readiness-state-matrix.md | 21 + .../contracts/restore-requirement-map.md | 15 + .../contracts/restore-ui-copy-contract.md | 32 + .../plan.md | 329 +++ .../spec.md | 355 +++ .../tasks.md | 156 ++ .../browser-bug-report.md | 556 +++++ .../logs/console-warnings-final.txt | 1974 +++++++++++++++++ .../logs/console-warnings.txt | 1974 +++++++++++++++++ .../logs/network-requests-final.txt | 4 + .../logs/network-requests.txt | 3 + .../BUG-001-operations-500-debug-page.png | Bin 0 -> 820729 bytes ...stomer-workspace-cta-opens-review-pack.png | Bin 0 -> 176751 bytes ...pack-download-enabled-while-not-usable.png | Bin 0 -> 177417 bytes ...ence-cta-points-to-superseded-snapshot.png | Bin 0 -> 360710 bytes ...ermissions-zero-present-despite-grants.png | Bin 0 -> 188891 bytes ...mer-review-download-and-stale-evidence.png | Bin 0 -> 336110 bytes ...ealth-healthy-while-verification-stale.png | Bin 0 -> 88843 bytes ...-system-login-default-laravel-branding.png | Bin 0 -> 16316 bytes .../screenshots/ROUTE-admin-dashboard.png | Bin 0 -> 419271 bytes .../screenshots/ROUTE-alerts.png | Bin 0 -> 49315 bytes .../screenshots/ROUTE-audit-log.png | Bin 0 -> 479880 bytes .../screenshots/ROUTE-backup-schedules.png | Bin 0 -> 37646 bytes .../screenshots/ROUTE-backup-sets.png | Bin 0 -> 86135 bytes .../screenshots/ROUTE-baseline-compare.png | Bin 0 -> 585978 bytes .../screenshots/ROUTE-baseline-profiles.png | Bin 0 -> 46550 bytes .../screenshots/ROUTE-baseline-snapshots.png | Bin 0 -> 51315 bytes .../screenshots/ROUTE-choose-environment.png | Bin 0 -> 93761 bytes .../screenshots/ROUTE-choose-workspace.png | Bin 0 -> 28002 bytes .../ROUTE-current-evidence-snapshot.png | Bin 0 -> 400239 bytes .../screenshots/ROUTE-customer-reviews.png | Bin 0 -> 336103 bytes .../screenshots/ROUTE-decision-register.png | Bin 0 -> 83380 bytes .../screenshots/ROUTE-entra-groups.png | Bin 0 -> 153765 bytes .../ROUTE-environment-dashboard.png | Bin 0 -> 359953 bytes .../screenshots/ROUTE-environment-reviews.png | Bin 0 -> 79748 bytes .../screenshots/ROUTE-evidence-overview.png | Bin 0 -> 310995 bytes .../ROUTE-evidence-snapshot-detail.png | Bin 0 -> 399799 bytes .../screenshots/ROUTE-evidence-snapshots.png | Bin 0 -> 94863 bytes .../ROUTE-finding-exceptions-queue.png | Bin 0 -> 105921 bytes .../screenshots/ROUTE-findings.png | Bin 0 -> 276508 bytes .../screenshots/ROUTE-governance-inbox.png | Bin 0 -> 348947 bytes .../screenshots/ROUTE-inventory-coverage.png | Bin 0 -> 398957 bytes .../screenshots/ROUTE-inventory-items.png | Bin 0 -> 256429 bytes .../ROUTE-managed-environments.png | Bin 0 -> 65201 bytes .../screenshots/ROUTE-operations.png | Bin 0 -> 821609 bytes .../screenshots/ROUTE-policies.png | Bin 0 -> 228242 bytes .../screenshots/ROUTE-policy-versions.png | Bin 0 -> 412031 bytes .../ROUTE-provider-connections.png | Bin 0 -> 88843 bytes .../ROUTE-required-permissions.png | Bin 0 -> 188940 bytes .../screenshots/ROUTE-restore-runs.png | Bin 0 -> 42184 bytes .../screenshots/ROUTE-review-packs.png | Bin 0 -> 135936 bytes .../ROUTE-reviews-workspace-filtered.png | Bin 0 -> 111987 bytes .../screenshots/ROUTE-risk-exceptions.png | Bin 0 -> 60919 bytes .../screenshots/ROUTE-stored-reports.png | Bin 0 -> 52352 bytes .../screenshots/ROUTE-system-access-logs.png | Bin 0 -> 16316 bytes .../screenshots/ROUTE-system-dashboard.png | Bin 0 -> 16316 bytes .../screenshots/ROUTE-system-ops-controls.png | Bin 0 -> 16316 bytes .../screenshots/ROUTE-system-ops-failures.png | Bin 0 -> 16316 bytes .../screenshots/ROUTE-system-ops-runbooks.png | Bin 0 -> 16316 bytes .../screenshots/ROUTE-system-ops-runs.png | Bin 0 -> 16316 bytes .../screenshots/ROUTE-system-ops-stuck.png | Bin 0 -> 16316 bytes .../screenshots/ROUTE-system-tenants.png | Bin 0 -> 16316 bytes .../screenshots/ROUTE-system-workspaces.png | Bin 0 -> 16316 bytes .../screenshots/ROUTE-workspace-overview.png | Bin 0 -> 419214 bytes .../screenshots/ROUTE-workspace-settings.png | Bin 0 -> 445632 bytes .../screenshots/ROUTE-workspaces.png | Bin 0 -> 30787 bytes ...tpilot-environment-overview-playwright.png | Bin 0 -> 357673 bytes tenantpilot-evidence-basis-playwright.png | Bin 0 -> 399760 bytes tenantpilot-login-playwright.png | Bin 0 -> 25724 bytes 94 files changed, 6682 insertions(+), 28 deletions(-) create mode 100644 apps/platform/app/Support/RestoreReadinessResolution/RestoreGuidanceBasis.php create mode 100644 apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessAction.php create mode 100644 apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessReason.php create mode 100644 apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessResolver.php create mode 100644 apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessState.php create mode 100644 apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessSummary.php create mode 100644 apps/platform/tests/Feature/Filament/Spec390RestoreReadinessGuidanceTest.php create mode 100644 apps/platform/tests/Unit/Support/RestoreReadinessResolution/RestoreReadinessResolverTest.php create mode 100644 specs/390-restore-readiness-resolution-adapter-v1/artifacts/current-restore-flow-inventory.md create mode 100644 specs/390-restore-readiness-resolution-adapter-v1/checklists/requirements.md create mode 100644 specs/390-restore-readiness-resolution-adapter-v1/contracts/restore-readiness-state-matrix.md create mode 100644 specs/390-restore-readiness-resolution-adapter-v1/contracts/restore-requirement-map.md create mode 100644 specs/390-restore-readiness-resolution-adapter-v1/contracts/restore-ui-copy-contract.md create mode 100644 specs/390-restore-readiness-resolution-adapter-v1/plan.md create mode 100644 specs/390-restore-readiness-resolution-adapter-v1/spec.md create mode 100644 specs/390-restore-readiness-resolution-adapter-v1/tasks.md create mode 100644 specs/browser-productization-bug-audit/browser-bug-report.md create mode 100644 specs/browser-productization-bug-audit/logs/console-warnings-final.txt create mode 100644 specs/browser-productization-bug-audit/logs/console-warnings.txt create mode 100644 specs/browser-productization-bug-audit/logs/network-requests-final.txt create mode 100644 specs/browser-productization-bug-audit/logs/network-requests.txt create mode 100644 specs/browser-productization-bug-audit/screenshots/BUG-001-operations-500-debug-page.png create mode 100644 specs/browser-productization-bug-audit/screenshots/BUG-002-customer-workspace-cta-opens-review-pack.png create mode 100644 specs/browser-productization-bug-audit/screenshots/BUG-003-internal-pack-download-enabled-while-not-usable.png create mode 100644 specs/browser-productization-bug-audit/screenshots/BUG-004-primary-evidence-cta-points-to-superseded-snapshot.png create mode 100644 specs/browser-productization-bug-audit/screenshots/BUG-005-required-permissions-zero-present-despite-grants.png create mode 100644 specs/browser-productization-bug-audit/screenshots/BUG-006-customer-review-download-and-stale-evidence.png create mode 100644 specs/browser-productization-bug-audit/screenshots/BUG-007-provider-health-healthy-while-verification-stale.png create mode 100644 specs/browser-productization-bug-audit/screenshots/BUG-008-system-login-default-laravel-branding.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-admin-dashboard.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-alerts.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-audit-log.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-backup-schedules.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-backup-sets.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-baseline-compare.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-baseline-profiles.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-baseline-snapshots.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-choose-environment.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-choose-workspace.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-current-evidence-snapshot.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-customer-reviews.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-decision-register.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-entra-groups.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-environment-dashboard.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-environment-reviews.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-evidence-overview.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-evidence-snapshot-detail.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-evidence-snapshots.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-finding-exceptions-queue.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-findings.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-governance-inbox.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-inventory-coverage.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-inventory-items.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-managed-environments.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-operations.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-policies.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-policy-versions.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-provider-connections.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-required-permissions.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-restore-runs.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-review-packs.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-reviews-workspace-filtered.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-risk-exceptions.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-stored-reports.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-system-access-logs.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-system-dashboard.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-system-ops-controls.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-system-ops-failures.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-system-ops-runbooks.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-system-ops-runs.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-system-ops-stuck.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-system-tenants.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-system-workspaces.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-workspace-overview.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-workspace-settings.png create mode 100644 specs/browser-productization-bug-audit/screenshots/ROUTE-workspaces.png create mode 100644 tenantpilot-environment-overview-playwright.png create mode 100644 tenantpilot-evidence-basis-playwright.png create mode 100644 tenantpilot-login-playwright.png diff --git a/apps/platform/app/Filament/Resources/RestoreRunResource.php b/apps/platform/app/Filament/Resources/RestoreRunResource.php index 3dcd78d1..319e9474 100644 --- a/apps/platform/app/Filament/Resources/RestoreRunResource.php +++ b/apps/platform/app/Filament/Resources/RestoreRunResource.php @@ -102,6 +102,8 @@ class RestoreRunResource extends Resource protected static ?string $tenantOwnershipRelationshipName = 'tenant'; + protected static bool $isGloballySearchable = false; + public static function shouldRegisterNavigation(): bool { return NavigationScope::shouldRegisterEnvironmentNavigation() diff --git a/apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php b/apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php index d8bdcb50..2f968d27 100644 --- a/apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php +++ b/apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php @@ -17,6 +17,7 @@ use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; use App\Support\OperationRunLinks; +use App\Support\RestoreReadinessResolution\RestoreReadinessResolver; use App\Support\RestoreSafety\ChecksIntegrityState; use App\Support\RestoreSafety\PreviewIntegrityState; use App\Support\RestoreSafety\RestoreSafetyCopy; @@ -230,6 +231,15 @@ public static function contract( draft: $draft, tenant: $tenant, ); + $readinessGuidance = app(RestoreReadinessResolver::class) + ->forWizardData( + data: $draft, + hasUsableSource: $hasUsableSource, + scopeDefined: $scopeDefined, + scopeDependencyResolved: $scopeDependencyResolved, + executionReadiness: is_array($executionReadiness) ? $executionReadiness : [], + ) + ->toArray(); $decisionCard = self::restoreWizardDecisionCard( backupSet: $backupSet, @@ -409,6 +419,7 @@ public static function contract( 'mapping_summary' => $mappingResolver, 'validation_summary' => $validationSummary, 'preview_summary' => $previewSummary, + 'readiness_guidance' => $readinessGuidance, 'wizard_gate' => $wizardGate, 'can_continue' => $canContinue, 'blocked_reason' => $blockedReason, @@ -427,6 +438,7 @@ public static function contract( ], 'validationSummary' => $validationSummary, 'previewSummary' => $previewSummary, + 'readinessGuidance' => $readinessGuidance, 'wizardGate' => $wizardGate, 'mappingResolver' => $mappingResolver, 'currentScope' => $scope, diff --git a/apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunDetailPresenter.php b/apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunDetailPresenter.php index 8c4d348f..43f21e37 100644 --- a/apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunDetailPresenter.php +++ b/apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunDetailPresenter.php @@ -16,6 +16,7 @@ use App\Support\OperationRunLinks; use App\Support\OperationRunOutcome; use App\Support\OperationRunStatus; +use App\Support\RestoreReadinessResolution\RestoreReadinessResolver; use App\Support\RestoreRunStatus; use App\Support\RestoreSafety\RestoreResultAttention; use App\Support\RestoreSafety\RestoreSafetyCopy; @@ -40,6 +41,13 @@ public function forRun(RestoreRun $restoreRun): array $operationRun = $this->scopedOperationRun($restoreRun); $operationProof = $this->operationProof($operationRun); $postRunEvidence = $this->postRunEvidence($restoreRun, $operationRun); + $readinessGuidance = app(RestoreReadinessResolver::class) + ->forRestoreRun( + restoreRun: $restoreRun, + operationUrl: is_string($operationProof['url'] ?? null) ? $operationProof['url'] : null, + evidenceUrl: is_string($postRunEvidence['url'] ?? null) ? $postRunEvidence['url'] : null, + ) + ->toArray(); $decision = $this->decision($restoreRun, $attention, $operationProof, $postRunEvidence); $resultSummary = $this->resultSummary($restoreRun); $itemOutcomes = $this->itemOutcomes($restoreRun); @@ -48,6 +56,7 @@ public function forRun(RestoreRun $restoreRun): array return [ 'decision' => $decision, + 'readinessGuidance' => $readinessGuidance, 'operationProof' => $operationProof, 'postRunEvidence' => $postRunEvidence, 'resultSummary' => $resultSummary, diff --git a/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php b/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php index eada5da7..6fe9ea47 100644 --- a/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php +++ b/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php @@ -828,7 +828,8 @@ private function supportingSignals( $overview = is_array($requiredPermissions['overview'] ?? null) ? $requiredPermissions['overview'] : []; - $providerPermissionsReady = $this->providerPermissionsTone($overview) === 'success'; + $providerPermissionsValue = $this->providerPermissionsValue($overview); + $providerPermissionsTone = $this->providerPermissionsTone($overview); $operationCount = (int) ($activeOperationSummary['count'] ?? 0); $operationsAction = $operationCount > 0 && is_string($activeOperationSummary['secondaryActionUrl'] ?? null) ? [ @@ -878,8 +879,8 @@ private function supportingSignals( $this->supportingSignal( key: 'provider_permissions', label: 'Provider permissions', - value: $providerPermissionsReady ? 'Ready' : 'Missing', - tone: $providerPermissionsReady ? 'success' : 'danger', + value: $providerPermissionsValue, + tone: $providerPermissionsTone, action: $this->requiredPermissionsAction($tenant, $user, $this->overviewText('action_open_required_permissions')), ), $this->supportingSignal( diff --git a/apps/platform/app/Support/RestoreReadinessResolution/RestoreGuidanceBasis.php b/apps/platform/app/Support/RestoreReadinessResolution/RestoreGuidanceBasis.php new file mode 100644 index 00000000..d8cc2da0 --- /dev/null +++ b/apps/platform/app/Support/RestoreReadinessResolution/RestoreGuidanceBasis.php @@ -0,0 +1,40 @@ + $scope + */ + public function __construct( + public string $fingerprint, + public array $scope, + ) {} + + /** + * @param array $data + */ + public static function fromData(RestoreSafetyResolver $resolver, array $data): self + { + $scope = $resolver->scopeFingerprintFromData($data)->toArray(); + $fingerprint = is_string($scope['fingerprint'] ?? null) ? $scope['fingerprint'] : ''; + + return new self( + fingerprint: $fingerprint, + scope: $scope, + ); + } + + /** + * @param array $data + */ + public function matchesData(RestoreSafetyResolver $resolver, array $data): bool + { + return hash_equals($this->fingerprint, self::fromData($resolver, $data)->fingerprint); + } +} diff --git a/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessAction.php b/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessAction.php new file mode 100644 index 00000000..8be7116a --- /dev/null +++ b/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessAction.php @@ -0,0 +1,64 @@ + 'Review backup selection', + self::DefineScope => 'Define restore scope', + self::ReviewGroupMappings => 'Review group mappings', + self::RunReadinessChecks => 'Run readiness checks', + self::ReviewValidationBlockers => 'Review validation blockers', + self::GeneratePreview => 'Generate preview', + self::RegeneratePreview => 'Regenerate preview', + self::ContinueToConfirmation => 'Continue to final confirmation', + self::OpenOperation => 'Open operation', + self::InspectRestoreResults => 'Inspect restore results', + self::InspectEvidence => 'Open evidence', + self::None => 'No safe action available', + }; + } + + public function mutatesPreparation(): bool + { + return in_array($this, [ + self::RunReadinessChecks, + self::GeneratePreview, + self::RegeneratePreview, + ], true); + } + + public function safetyCopy(): string + { + if ($this->mutatesPreparation()) { + return 'This will not execute the restore.'; + } + + return match ($this) { + self::ContinueToConfirmation => 'The restore still requires final confirmation before execution.', + self::OpenOperation => 'This guidance only opens existing execution evidence.', + self::InspectEvidence, + self::InspectRestoreResults => 'This guidance is inspection-only for an existing restore record.', + self::None => 'No preparation mutation is available from this state.', + default => 'This guidance does not execute the restore.', + }; + } +} diff --git a/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessReason.php b/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessReason.php new file mode 100644 index 00000000..3112b100 --- /dev/null +++ b/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessReason.php @@ -0,0 +1,47 @@ + 'Select a backup set before restore readiness can be judged.', + self::SourceUnusable => 'The selected backup does not contain usable restore payloads.', + self::ScopeRequired => 'Define the restore scope before readiness checks can prove the draft.', + self::GroupMappingRequired => 'Required group mappings must be resolved before validation can run.', + self::ChecksNotRun => 'Readiness checks have not run for the current restore scope.', + self::ChecksStale => 'The last readiness checks no longer match the current restore scope.', + self::ChecksBlocking => 'Readiness checks found blockers that must be resolved before execution.', + self::PreviewMissing => 'A restore preview has not been generated for the current scope.', + self::PreviewStale => 'The last restore preview no longer matches the current restore scope.', + self::ExecutionPrerequisiteBlocked => 'Execution prerequisites are blocked even though preparation evidence is available.', + self::ReadyForConfirmation => 'Current checks and preview evidence match the selected restore scope.', + self::ExecutionInProgress => 'Execution truth is owned by the linked OperationRun.', + self::ExecutionCompleted => 'This record is terminal; inspect result and evidence rather than preparation actions.', + self::ExecutionFailed => 'This restore did not complete successfully; inspect failure details and evidence.', + self::ExecutionCancelled => 'This restore did not execute to completion because it was cancelled or blocked.', + self::HistoricalNonActionable => 'This record is historical and must not expose preparation mutations.', + }; + } +} diff --git a/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessResolver.php b/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessResolver.php new file mode 100644 index 00000000..a338b717 --- /dev/null +++ b/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessResolver.php @@ -0,0 +1,247 @@ + $data + * @param array $executionReadiness + */ + public function forWizardData( + array $data, + bool $hasUsableSource, + bool $scopeDefined, + bool $scopeDependencyResolved, + array $executionReadiness = [], + ): RestoreReadinessSummary { + $basis = $this->basisForData($data); + $backupSetId = $data['backup_set_id'] ?? null; + + if (! is_numeric($backupSetId)) { + return $this->summary( + RestoreReadinessState::Blocked, + RestoreReadinessReason::SourceRequired, + RestoreReadinessAction::ReviewBackupSelection, + $basis, + ); + } + + if (! $hasUsableSource) { + return $this->summary( + RestoreReadinessState::Blocked, + RestoreReadinessReason::SourceUnusable, + RestoreReadinessAction::ReviewBackupSelection, + $basis, + ); + } + + if (! $scopeDefined) { + return $this->summary( + RestoreReadinessState::NeedsPreparation, + RestoreReadinessReason::ScopeRequired, + RestoreReadinessAction::DefineScope, + $basis, + ); + } + + if (! $scopeDependencyResolved) { + return $this->summary( + RestoreReadinessState::NeedsPreparation, + RestoreReadinessReason::GroupMappingRequired, + RestoreReadinessAction::ReviewGroupMappings, + $basis, + ); + } + + $checks = $this->restoreSafetyResolver->checksIntegrityFromData($data); + $preview = $this->restoreSafetyResolver->previewIntegrityFromData($data); + + if (! $checks->isCurrent()) { + return $this->summary( + RestoreReadinessState::Blocked, + $checks->state === ChecksIntegrityState::STATE_NOT_RUN + ? RestoreReadinessReason::ChecksNotRun + : RestoreReadinessReason::ChecksStale, + RestoreReadinessAction::RunReadinessChecks, + $basis, + ); + } + + if ($checks->blockingCount > 0) { + return $this->summary( + RestoreReadinessState::Blocked, + RestoreReadinessReason::ChecksBlocking, + RestoreReadinessAction::ReviewValidationBlockers, + $basis, + ); + } + + if (! $preview->isCurrent()) { + return $this->summary( + RestoreReadinessState::NeedsPreparation, + $preview->state === PreviewIntegrityState::STATE_NOT_GENERATED + ? RestoreReadinessReason::PreviewMissing + : RestoreReadinessReason::PreviewStale, + $preview->state === PreviewIntegrityState::STATE_NOT_GENERATED + ? RestoreReadinessAction::GeneratePreview + : RestoreReadinessAction::RegeneratePreview, + $basis, + ); + } + + if (array_key_exists('allowed', $executionReadiness) && ! (bool) $executionReadiness['allowed']) { + return $this->summary( + RestoreReadinessState::Blocked, + RestoreReadinessReason::ExecutionPrerequisiteBlocked, + RestoreReadinessAction::ReviewValidationBlockers, + $basis, + ); + } + + return $this->summary( + RestoreReadinessState::ReadyForConfirmation, + RestoreReadinessReason::ReadyForConfirmation, + RestoreReadinessAction::ContinueToConfirmation, + $basis, + ); + } + + public function forRestoreRun( + RestoreRun $restoreRun, + ?string $operationUrl = null, + ?string $evidenceUrl = null, + ): RestoreReadinessSummary { + $restoreRun->loadMissing(['operationRun']); + + $data = $this->dataFromRestoreRun($restoreRun); + $basis = $this->basisForData($data); + $status = RestoreRunStatus::fromString((string) $restoreRun->status); + + if ($status === RestoreRunStatus::Pending || $status === RestoreRunStatus::Queued || $status === RestoreRunStatus::Running) { + return $this->summary( + RestoreReadinessState::Executing, + RestoreReadinessReason::ExecutionInProgress, + RestoreReadinessAction::OpenOperation, + $basis, + $operationUrl, + 'OperationRun evidence', + ); + } + + if ($status === RestoreRunStatus::Failed) { + return $this->summary( + RestoreReadinessState::Failed, + RestoreReadinessReason::ExecutionFailed, + RestoreReadinessAction::InspectRestoreResults, + $basis, + $operationUrl, + 'Failure evidence', + ); + } + + if ($status === RestoreRunStatus::Cancelled || $status === RestoreRunStatus::Aborted) { + return $this->summary( + RestoreReadinessState::Cancelled, + RestoreReadinessReason::ExecutionCancelled, + RestoreReadinessAction::InspectRestoreResults, + $basis, + $operationUrl, + 'Cancellation evidence', + ); + } + + if (in_array($status, [ + RestoreRunStatus::Completed, + RestoreRunStatus::Partial, + RestoreRunStatus::CompletedWithErrors, + ], true)) { + return $this->summary( + RestoreReadinessState::Completed, + RestoreReadinessReason::ExecutionCompleted, + $evidenceUrl === null ? RestoreReadinessAction::InspectRestoreResults : RestoreReadinessAction::InspectEvidence, + $basis, + $evidenceUrl ?? $operationUrl, + $evidenceUrl === null ? 'Restore result evidence' : 'Post-run evidence', + ); + } + + return $this->forWizardData( + data: $data, + hasUsableSource: is_numeric($restoreRun->backup_set_id), + scopeDefined: true, + scopeDependencyResolved: true, + ); + } + + /** + * @param array $currentData + */ + public function actionBasisMatches(?string $expectedFingerprint, array $currentData): bool + { + if (! is_string($expectedFingerprint) || $expectedFingerprint === '') { + return false; + } + + return RestoreGuidanceBasis::fromData($this->restoreSafetyResolver, $currentData)->fingerprint === $expectedFingerprint; + } + + /** + * @param array $data + */ + public function basisForData(array $data): RestoreGuidanceBasis + { + return RestoreGuidanceBasis::fromData($this->restoreSafetyResolver, $data); + } + + private function summary( + RestoreReadinessState $state, + RestoreReadinessReason $reason, + RestoreReadinessAction $nextAction, + RestoreGuidanceBasis $basis, + ?string $evidenceUrl = null, + ?string $evidenceLabel = null, + ): RestoreReadinessSummary { + return new RestoreReadinessSummary( + state: $state, + reason: $reason, + nextAction: $nextAction, + basis: $basis, + evidenceUrl: $evidenceUrl, + evidenceLabel: $evidenceLabel, + ); + } + + /** + * @return array + */ + private function dataFromRestoreRun(RestoreRun $restoreRun): array + { + $metadata = is_array($restoreRun->metadata) ? $restoreRun->metadata : []; + + return [ + 'backup_set_id' => $restoreRun->backup_set_id, + 'scope_mode' => (string) (($restoreRun->scopeBasis()['scope_mode'] ?? null) ?: ((is_array($restoreRun->requested_items) && $restoreRun->requested_items !== []) ? 'selected' : 'all')), + 'backup_item_ids' => is_array($restoreRun->requested_items) ? $restoreRun->requested_items : [], + 'group_mapping' => is_array($restoreRun->group_mapping) ? $restoreRun->group_mapping : [], + 'check_basis' => $restoreRun->checkBasis(), + 'preview_basis' => $restoreRun->previewBasis(), + 'check_summary' => is_array($metadata['check_summary'] ?? null) ? $metadata['check_summary'] : [], + 'checks_ran_at' => $restoreRun->checkBasis()['ran_at'] ?? ($metadata['checks_ran_at'] ?? null), + 'preview_summary' => is_array($metadata['preview_summary'] ?? null) ? $metadata['preview_summary'] : [], + 'preview_ran_at' => $restoreRun->previewBasis()['generated_at'] ?? ($metadata['preview_ran_at'] ?? null), + ]; + } +} diff --git a/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessState.php b/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessState.php new file mode 100644 index 00000000..a37e4879 --- /dev/null +++ b/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessState.php @@ -0,0 +1,45 @@ + "Restore can't continue yet.", + self::NeedsPreparation => 'Restore needs preparation.', + self::ReadyForConfirmation => 'Ready for final confirmation.', + self::Executing => 'Restore execution is in progress.', + self::Completed => 'Restore completed.', + self::Failed => 'Restore failed.', + self::Cancelled => 'Restore cancelled.', + self::Historical => 'Historical restore record.', + }; + } + + public function tone(): string + { + return match ($this) { + self::ReadyForConfirmation, + self::Completed => 'success', + self::NeedsPreparation, + self::Executing, + self::Historical => 'warning', + self::Blocked, + self::Failed, + self::Cancelled => 'danger', + }; + } +} diff --git a/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessSummary.php b/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessSummary.php new file mode 100644 index 00000000..8f321532 --- /dev/null +++ b/apps/platform/app/Support/RestoreReadinessResolution/RestoreReadinessSummary.php @@ -0,0 +1,39 @@ + + */ + public function toArray(): array + { + return [ + 'state' => $this->state->value, + 'stateLabel' => $this->state->label(), + 'tone' => $this->state->tone(), + 'reason' => $this->reason->value, + 'reasonSummary' => $this->reason->summary(), + 'nextAction' => $this->nextAction->value, + 'nextActionLabel' => $this->nextAction->label(), + 'actionSafetyCopy' => $this->nextAction->safetyCopy(), + 'mutatesPreparation' => $this->nextAction->mutatesPreparation(), + 'basisFingerprint' => $this->basis->fingerprint, + 'basisScope' => $this->basis->scope, + 'evidenceUrl' => $this->evidenceUrl, + 'evidenceLabel' => $this->evidenceLabel, + ]; + } +} diff --git a/apps/platform/resources/views/filament/forms/components/restore-run-confirm-panel.blade.php b/apps/platform/resources/views/filament/forms/components/restore-run-confirm-panel.blade.php index 3b77ecb6..e90275be 100644 --- a/apps/platform/resources/views/filament/forms/components/restore-run-confirm-panel.blade.php +++ b/apps/platform/resources/views/filament/forms/components/restore-run-confirm-panel.blade.php @@ -3,6 +3,7 @@ $decisionCard = is_array($decisionCard ?? null) ? $decisionCard : []; $processFlow = is_array($processFlow ?? null) ? $processFlow : []; + $readinessGuidance = is_array($readinessGuidance ?? null) ? $readinessGuidance : []; $wizardGate = is_array($wizardGate ?? null) ? $wizardGate : []; $statusTone = (string) ($wizardGate['confirmation_state_tone'] ?? ($decisionCard['tone'] ?? 'gray')); @@ -80,6 +81,26 @@ +
+
+
+ Restore readiness +
+ + {{ $readinessGuidance['stateLabel'] ?? 'Restore readiness unavailable.' }} + +
+

+ {{ $readinessGuidance['reasonSummary'] ?? 'This guidance is based on the current restore scope and preview state.' }} +

+

+ Next safe action: {{ $readinessGuidance['nextActionLabel'] ?? ($decisionCard['nextAction'] ?? 'Review the current restore state.') }} +

+

+ {{ $readinessGuidance['actionSafetyCopy'] ?? 'This guidance does not execute the restore.' }} +

+
+
Operation proof is unavailable before execution. Post-run evidence is unavailable before execution. Recovery is not verified until post-run evidence exists.
diff --git a/apps/platform/resources/views/filament/forms/components/restore-run-safety-decision.blade.php b/apps/platform/resources/views/filament/forms/components/restore-run-safety-decision.blade.php index 6d2f9dd7..492f5650 100644 --- a/apps/platform/resources/views/filament/forms/components/restore-run-safety-decision.blade.php +++ b/apps/platform/resources/views/filament/forms/components/restore-run-safety-decision.blade.php @@ -1,6 +1,7 @@ @php $fieldWrapperView = $getFieldWrapperView(); $decisionCard = is_array($decisionCard ?? null) ? $decisionCard : []; + $readinessGuidance = is_array($readinessGuidance ?? null) ? $readinessGuidance : []; $statusBadgeClasses = static function (string $tone): string { return match ($tone) { @@ -134,10 +135,24 @@