feat: add restore readiness resolution adapter improvements
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m19s
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\RestoreReadinessResolution;
|
||||
|
||||
use App\Support\RestoreSafety\RestoreSafetyResolver;
|
||||
|
||||
final readonly class RestoreGuidanceBasis
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $scope
|
||||
*/
|
||||
public function __construct(
|
||||
public string $fingerprint,
|
||||
public array $scope,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $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<string, mixed> $data
|
||||
*/
|
||||
public function matchesData(RestoreSafetyResolver $resolver, array $data): bool
|
||||
{
|
||||
return hash_equals($this->fingerprint, self::fromData($resolver, $data)->fingerprint);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\RestoreReadinessResolution;
|
||||
|
||||
enum RestoreReadinessAction: string
|
||||
{
|
||||
case ReviewBackupSelection = 'review_backup_selection';
|
||||
case DefineScope = 'define_scope';
|
||||
case ReviewGroupMappings = 'review_group_mappings';
|
||||
case RunReadinessChecks = 'run_readiness_checks';
|
||||
case ReviewValidationBlockers = 'review_validation_blockers';
|
||||
case GeneratePreview = 'generate_preview';
|
||||
case RegeneratePreview = 'regenerate_preview';
|
||||
case ContinueToConfirmation = 'continue_to_confirmation';
|
||||
case OpenOperation = 'open_operation';
|
||||
case InspectRestoreResults = 'inspect_restore_results';
|
||||
case InspectEvidence = 'inspect_evidence';
|
||||
case None = 'none';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ReviewBackupSelection => '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.',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\RestoreReadinessResolution;
|
||||
|
||||
enum RestoreReadinessReason: string
|
||||
{
|
||||
case SourceRequired = 'source_required';
|
||||
case SourceUnusable = 'source_unusable';
|
||||
case ScopeRequired = 'scope_required';
|
||||
case GroupMappingRequired = 'group_mapping_required';
|
||||
case ChecksNotRun = 'checks_not_run';
|
||||
case ChecksStale = 'checks_stale';
|
||||
case ChecksBlocking = 'checks_blocking';
|
||||
case PreviewMissing = 'preview_missing';
|
||||
case PreviewStale = 'preview_stale';
|
||||
case ExecutionPrerequisiteBlocked = 'execution_prerequisite_blocked';
|
||||
case ReadyForConfirmation = 'ready_for_confirmation';
|
||||
case ExecutionInProgress = 'execution_in_progress';
|
||||
case ExecutionCompleted = 'execution_completed';
|
||||
case ExecutionFailed = 'execution_failed';
|
||||
case ExecutionCancelled = 'execution_cancelled';
|
||||
case HistoricalNonActionable = 'historical_non_actionable';
|
||||
|
||||
public function summary(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::SourceRequired => '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.',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,247 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\RestoreReadinessResolution;
|
||||
|
||||
use App\Models\RestoreRun;
|
||||
use App\Support\RestoreRunStatus;
|
||||
use App\Support\RestoreSafety\ChecksIntegrityState;
|
||||
use App\Support\RestoreSafety\PreviewIntegrityState;
|
||||
use App\Support\RestoreSafety\RestoreSafetyResolver;
|
||||
|
||||
final readonly class RestoreReadinessResolver
|
||||
{
|
||||
public function __construct(
|
||||
private RestoreSafetyResolver $restoreSafetyResolver,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<string, mixed> $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<string, mixed> $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<string, mixed> $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<string, mixed>
|
||||
*/
|
||||
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),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\RestoreReadinessResolution;
|
||||
|
||||
enum RestoreReadinessState: string
|
||||
{
|
||||
case Blocked = 'blocked';
|
||||
case NeedsPreparation = 'needs_preparation';
|
||||
case ReadyForConfirmation = 'ready_for_confirmation';
|
||||
case Executing = 'executing';
|
||||
case Completed = 'completed';
|
||||
case Failed = 'failed';
|
||||
case Cancelled = 'cancelled';
|
||||
case Historical = 'historical';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Blocked => "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',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\RestoreReadinessResolution;
|
||||
|
||||
final readonly class RestoreReadinessSummary
|
||||
{
|
||||
public function __construct(
|
||||
public RestoreReadinessState $state,
|
||||
public RestoreReadinessReason $reason,
|
||||
public RestoreReadinessAction $nextAction,
|
||||
public RestoreGuidanceBasis $basis,
|
||||
public ?string $evidenceUrl = null,
|
||||
public ?string $evidenceLabel = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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 @@
|
||||
</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="flex flex-wrap items-center gap-2">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">
|
||||
Restore readiness
|
||||
</div>
|
||||
<x-filament::badge :color="$readinessGuidance['tone'] ?? 'gray'" size="sm">
|
||||
{{ $readinessGuidance['stateLabel'] ?? 'Restore readiness unavailable.' }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-700 dark:text-gray-200">
|
||||
{{ $readinessGuidance['reasonSummary'] ?? 'This guidance is based on the current restore scope and preview state.' }}
|
||||
</p>
|
||||
<p class="mt-2 text-xs font-semibold text-gray-700 dark:text-gray-200">
|
||||
Next safe action: {{ $readinessGuidance['nextActionLabel'] ?? ($decisionCard['nextAction'] ?? 'Review the current restore state.') }}
|
||||
</p>
|
||||
<p class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $readinessGuidance['actionSafetyCopy'] ?? 'This guidance does not execute the restore.' }}
|
||||
</p>
|
||||
</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">
|
||||
Operation proof is unavailable before execution. Post-run evidence is unavailable before execution. Recovery is not verified until post-run evidence exists.
|
||||
</div>
|
||||
|
||||
@ -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 @@
|
||||
|
||||
<aside class="min-w-0">
|
||||
<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="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $decisionCard['nextActionLabel'] ?? 'Primary next action' }}</div>
|
||||
<div data-testid="restore-run-decision-next-action" class="mt-1 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $decisionCard['nextAction'] ?? 'Review the current restore state.' }}
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Restore readiness</div>
|
||||
<x-filament::badge :color="$readinessGuidance['tone'] ?? 'gray'" size="sm">
|
||||
{{ $readinessGuidance['stateLabel'] ?? 'Restore readiness unavailable.' }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
<p class="mt-2 text-xs leading-5 text-gray-600 dark:text-gray-300">
|
||||
{{ $readinessGuidance['reasonSummary'] ?? 'This guidance is based on the current restore scope and preview state.' }}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 text-xs font-semibold text-gray-500 dark:text-gray-400">Next safe action</div>
|
||||
<div data-testid="restore-run-decision-next-action" class="mt-1 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $readinessGuidance['nextActionLabel'] ?? ($decisionCard['nextAction'] ?? 'Review the current restore state.') }}
|
||||
</div>
|
||||
|
||||
<p class="mt-3 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $readinessGuidance['actionSafetyCopy'] ?? 'This guidance does not execute the restore.' }}
|
||||
</p>
|
||||
|
||||
@if (filled($decisionCard['helperText'] ?? null))
|
||||
<p class="mt-3 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
$surface = is_array($surface) ? $surface : [];
|
||||
|
||||
$decision = is_array($surface['decision'] ?? null) ? $surface['decision'] : [];
|
||||
$readinessGuidance = is_array($surface['readinessGuidance'] ?? null) ? $surface['readinessGuidance'] : [];
|
||||
$operationProof = is_array($surface['operationProof'] ?? null) ? $surface['operationProof'] : [];
|
||||
$postRunEvidence = is_array($surface['postRunEvidence'] ?? null) ? $surface['postRunEvidence'] : [];
|
||||
$resultSummary = is_array($surface['resultSummary'] ?? null) ? $surface['resultSummary'] : [];
|
||||
@ -76,6 +77,18 @@ class="rounded-xl border border-slate-200 bg-white p-5 shadow-sm dark:border-whi
|
||||
<p class="mt-1 text-sm font-semibold text-slate-900 dark:text-white">{{ $decision['primary_next_action'] ?? 'Review restore details' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3 dark:border-white/10 dark:bg-white/5">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Restore readiness</p>
|
||||
<x-filament::badge :color="$readinessGuidance['tone'] ?? 'gray'" size="sm">
|
||||
{{ $readinessGuidance['stateLabel'] ?? 'Restore readiness unavailable.' }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-slate-800 dark:text-slate-100">{{ $readinessGuidance['reasonSummary'] ?? 'This guidance is based on the current restore scope and preview state.' }}</p>
|
||||
<p class="mt-2 text-xs font-semibold text-slate-700 dark:text-slate-200">Next safe action: {{ $readinessGuidance['nextActionLabel'] ?? 'Review restore details' }}</p>
|
||||
<p class="mt-1 text-xs text-slate-600 dark:text-slate-300">{{ $readinessGuidance['actionSafetyCopy'] ?? 'This guidance does not execute the restore.' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-slate-200 bg-slate-50 p-3 dark:border-white/10 dark:bg-white/5">
|
||||
|
||||
@ -127,6 +127,35 @@ function spec333BrowserWizardNext($page): void
|
||||
expect($clicked)->toBeTrue();
|
||||
}
|
||||
|
||||
function spec333BrowserClickVisibleAction($page, string $label): void
|
||||
{
|
||||
$encodedLabel = json_encode($label, JSON_THROW_ON_ERROR);
|
||||
|
||||
$clicked = $page->script(<<<JS
|
||||
(() => {
|
||||
const label = {$encodedLabel};
|
||||
const candidates = Array.from(document.querySelectorAll('button, a, [role="button"]'));
|
||||
const target = candidates.find((element) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
const text = (element.textContent || '').replace(/\\s+/g, ' ').trim();
|
||||
|
||||
return text.includes(label)
|
||||
&& style.display !== 'none'
|
||||
&& style.visibility !== 'hidden'
|
||||
&& ! element.hidden
|
||||
&& Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
||||
});
|
||||
|
||||
target?.scrollIntoView({ block: 'center', inline: 'center' });
|
||||
target?.click();
|
||||
|
||||
return Boolean(target);
|
||||
})()
|
||||
JS);
|
||||
|
||||
expect($clicked)->toBeTrue();
|
||||
}
|
||||
|
||||
function spec333BrowserUsableBackupFixture(ManagedEnvironment $tenant): BackupSet
|
||||
{
|
||||
$policy = Policy::create([
|
||||
@ -362,7 +391,8 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
spec333BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Source selected')
|
||||
->assertSee('Continue to scope and resolve required mappings.')
|
||||
->assertSee('Restore needs preparation.')
|
||||
->assertSee('Review group mappings')
|
||||
->assertSee('Safety evidence')
|
||||
->assertSee('View safety gates and proof')
|
||||
->assertNoJavaScriptErrors()
|
||||
@ -428,7 +458,7 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $backupSetWithCache);
|
||||
$page->waitForText('Continue to scope and resolve required mappings.');
|
||||
$page->waitForText('Review group mappings');
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Resolve target mappings');
|
||||
spec333BrowserOpenGroupPicker($page);
|
||||
@ -449,7 +479,7 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $backupSetWithoutCache);
|
||||
$page->waitForText('Continue to scope and resolve required mappings.');
|
||||
$page->waitForText('Review group mappings');
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Resolve target mappings');
|
||||
spec333BrowserOpenGroupPicker($page);
|
||||
@ -597,9 +627,10 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
|
||||
spec333BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Generate preview')
|
||||
->click('Generate preview')
|
||||
->waitForText('Preview details')
|
||||
$page->waitForText('Generate preview');
|
||||
spec333BrowserClickVisibleAction($page, 'Generate preview');
|
||||
|
||||
$page->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.')
|
||||
@ -663,6 +694,8 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
spec333BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Confirm & Execute')
|
||||
->assertSee('Ready for final confirmation.')
|
||||
->assertSee('The restore still requires final confirmation before execution.')
|
||||
->assertSee('Preview-only run ready')
|
||||
->assertSee('Create preview-only run')
|
||||
->assertSee('Create a preview-only restore run.')
|
||||
@ -696,9 +729,9 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
|
||||
spec333BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Generate preview')
|
||||
->click('Generate preview')
|
||||
->waitForText('Preview details');
|
||||
$page->waitForText('Generate preview');
|
||||
spec333BrowserClickVisibleAction($page, 'Generate preview');
|
||||
$page->waitForText('Preview details');
|
||||
|
||||
$connection = ProviderConnection::query()
|
||||
->where('managed_environment_id', (int) $tenant->getKey())
|
||||
@ -707,9 +740,10 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
|
||||
$connection?->credential()->delete();
|
||||
|
||||
$page->waitForText('Regenerate preview')
|
||||
->click('Regenerate preview')
|
||||
->waitForText('Execution unavailable until prerequisites are resolved')
|
||||
$page->waitForText('Regenerate preview');
|
||||
spec333BrowserClickVisibleAction($page, 'Regenerate preview');
|
||||
|
||||
$page->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');
|
||||
@ -721,6 +755,9 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
spec333BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Confirm & Execute')
|
||||
->assertSee("Restore can't continue yet.")
|
||||
->assertSee('Execution prerequisites are blocked even though preparation evidence is available.')
|
||||
->assertSee('Review validation blockers')
|
||||
->assertSee('Confirmation summary')
|
||||
->assertSee('Execution prerequisites blocked')
|
||||
->assertSee('Create preview-only run')
|
||||
|
||||
@ -145,6 +145,9 @@ function spec335BrowserCompletedRestoreRun(ManagedEnvironment $tenant, BackupSet
|
||||
->resize(1440, 1100)
|
||||
->waitForText('Was this restore executed safely, and is recovery proof available?')
|
||||
->assertSee('Not executed')
|
||||
->assertSee('Restore readiness')
|
||||
->assertSee("Restore can't continue yet.")
|
||||
->assertSee('Run readiness checks')
|
||||
->assertSee('Operation proof unavailable')
|
||||
->assertSee('Post-run evidence unavailable')
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-diagnostics\"]")?.open === false', true)
|
||||
@ -155,6 +158,9 @@ function spec335BrowserCompletedRestoreRun(ManagedEnvironment $tenant, BackupSet
|
||||
$page = visit(spec335BrowserViewPath($tenant, $completedRun))
|
||||
->resize(1440, 1100)
|
||||
->waitForText('Completed, recovery proof incomplete')
|
||||
->assertSee('Restore completed.')
|
||||
->assertSee('Inspect restore results')
|
||||
->assertSee('This guidance is inspection-only for an existing restore record.')
|
||||
->assertSee('Operation proof available')
|
||||
->assertSee('Post-run evidence unavailable')
|
||||
->assertSee('Restore result summary')
|
||||
@ -177,6 +183,8 @@ function spec335BrowserCompletedRestoreRun(ManagedEnvironment $tenant, BackupSet
|
||||
visit(spec335BrowserViewPath($tenant, $failedRun))
|
||||
->resize(1440, 1100)
|
||||
->waitForText('Restore failed')
|
||||
->assertSee('Inspect restore results')
|
||||
->assertSee('This restore did not complete successfully; inspect failure details and evidence.')
|
||||
->assertSee('Review failure details')
|
||||
->assertSee('Spec335 Failed Browser Policy')
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-diagnostics\"]")?.open === false', true)
|
||||
|
||||
@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Resources\RestoreRunResource;
|
||||
use App\Filament\Resources\RestoreRunResource\Pages\CreateRestoreRun;
|
||||
use App\Filament\Resources\RestoreRunResource\Pages\ViewRestoreRun;
|
||||
use App\Filament\Resources\RestoreRunResource\Presenters\RestoreRunCreatePresenter;
|
||||
use App\Models\BackupItem;
|
||||
use App\Models\BackupSet;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\Policy;
|
||||
use App\Models\ReviewPublicationResolutionCase;
|
||||
use App\Models\ReviewPublicationResolutionStep;
|
||||
use App\Models\RestoreRun;
|
||||
use App\Services\Intune\RestoreService;
|
||||
use App\Support\OperationRunStatus;
|
||||
use App\Support\OperationRunType;
|
||||
use App\Support\RestoreReadinessResolution\RestoreReadinessAction;
|
||||
use App\Support\RestoreReadinessResolution\RestoreReadinessReason;
|
||||
use App\Support\RestoreReadinessResolution\RestoreReadinessState;
|
||||
use App\Support\RestoreRunStatus;
|
||||
use App\Support\RestoreSafety\RestoreSafetyResolver;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Mockery\MockInterface;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
/**
|
||||
* @return array{operation_runs:int, review_publication_resolution_cases:int, review_publication_resolution_steps:int}
|
||||
*/
|
||||
function spec390GuidanceSideEffectCounts(): array
|
||||
{
|
||||
return [
|
||||
'operation_runs' => OperationRun::query()->count(),
|
||||
'review_publication_resolution_cases' => ReviewPublicationResolutionCase::query()->count(),
|
||||
'review_publication_resolution_steps' => ReviewPublicationResolutionStep::query()->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{operation_runs:int, review_publication_resolution_cases:int, review_publication_resolution_steps:int} $expected
|
||||
*/
|
||||
function spec390ExpectGuidanceSideEffectCounts(array $expected): void
|
||||
{
|
||||
expect(spec390GuidanceSideEffectCounts())->toBe($expected);
|
||||
}
|
||||
|
||||
function spec390BackupSetWithItem(ManagedEnvironment $tenant): BackupSet
|
||||
{
|
||||
$policy = Policy::create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'external_id' => 'spec390-policy',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'display_name' => 'Spec390 Policy',
|
||||
'platform' => 'windows',
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
$backupSet = BackupSet::factory()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'name' => 'Spec390 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,
|
||||
'payload' => [
|
||||
'id' => $policy->external_id,
|
||||
'displayName' => 'Spec390 Policy',
|
||||
'settings' => ['enabled' => true],
|
||||
],
|
||||
'assignments' => [],
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
return $backupSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function spec390ReadyWizardData(BackupSet $backupSet): array
|
||||
{
|
||||
/** @var RestoreSafetyResolver $safety */
|
||||
$safety = app(RestoreSafetyResolver::class);
|
||||
|
||||
$data = [
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'scope_mode' => 'all',
|
||||
'backup_item_ids' => [],
|
||||
'group_mapping' => [],
|
||||
'check_summary' => ['blocking' => 0, 'warning' => 0, 'safe' => 1],
|
||||
'check_results' => [['code' => 'safe', 'severity' => 'safe']],
|
||||
'checks_ran_at' => now('UTC')->toIso8601String(),
|
||||
'preview_summary' => ['generated_at' => now('UTC')->toIso8601String(), 'policies_total' => 1],
|
||||
'preview_diffs' => [['policy_identifier' => 'spec390-policy']],
|
||||
'preview_ran_at' => now('UTC')->toIso8601String(),
|
||||
'is_dry_run' => true,
|
||||
];
|
||||
|
||||
$data['check_basis'] = $safety->checksBasisFromData($data);
|
||||
$data['preview_basis'] = $safety->previewBasisFromData($data);
|
||||
|
||||
return RestoreRunResource::synchronizeRestoreSafetyDraft($data);
|
||||
}
|
||||
|
||||
it('adds blocked readiness guidance to the restore create presenter', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
$this->actingAs($user);
|
||||
ensureDefaultProviderConnection($tenant, 'microsoft');
|
||||
|
||||
$backupSet = spec390BackupSetWithItem($tenant);
|
||||
$sideEffectCounts = spec390GuidanceSideEffectCounts();
|
||||
|
||||
$contract = RestoreRunCreatePresenter::contract(
|
||||
data: [
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'scope_mode' => 'all',
|
||||
'backup_item_ids' => [],
|
||||
'group_mapping' => [],
|
||||
],
|
||||
currentStep: 3,
|
||||
compactFlow: true,
|
||||
tenant: $tenant,
|
||||
user: $user,
|
||||
);
|
||||
|
||||
expect(data_get($contract, 'readinessGuidance.state'))->toBe(RestoreReadinessState::Blocked->value)
|
||||
->and(data_get($contract, 'readinessGuidance.reason'))->toBe(RestoreReadinessReason::ChecksNotRun->value)
|
||||
->and(data_get($contract, 'readinessGuidance.nextAction'))->toBe(RestoreReadinessAction::RunReadinessChecks->value)
|
||||
->and(data_get($contract, 'readinessGuidance.actionSafetyCopy'))->toBe('This will not execute the restore.');
|
||||
|
||||
spec390ExpectGuidanceSideEffectCounts($sideEffectCounts);
|
||||
});
|
||||
|
||||
it('adds ready-for-confirmation guidance to the restore create presenter', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create([
|
||||
'rbac_status' => 'ok',
|
||||
'rbac_last_checked_at' => now(),
|
||||
]);
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
$this->actingAs($user);
|
||||
ensureDefaultProviderConnection($tenant, 'microsoft');
|
||||
|
||||
$backupSet = spec390BackupSetWithItem($tenant);
|
||||
|
||||
$contract = RestoreRunCreatePresenter::contract(
|
||||
data: spec390ReadyWizardData($backupSet),
|
||||
currentStep: 5,
|
||||
compactFlow: true,
|
||||
tenant: $tenant,
|
||||
user: $user,
|
||||
);
|
||||
|
||||
expect(data_get($contract, 'readinessGuidance.state'))->toBe(RestoreReadinessState::ReadyForConfirmation->value)
|
||||
->and(data_get($contract, 'readinessGuidance.nextAction'))->toBe(RestoreReadinessAction::ContinueToConfirmation->value)
|
||||
->and(data_get($contract, 'readinessGuidance.actionSafetyCopy'))->toBe('The restore still requires final confirmation before execution.');
|
||||
});
|
||||
|
||||
it('blocks create presenter readiness when execution prerequisites are unavailable', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create([
|
||||
'rbac_status' => 'ok',
|
||||
'rbac_last_checked_at' => now(),
|
||||
]);
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
$this->actingAs($user);
|
||||
ensureDefaultProviderConnection($tenant, 'microsoft', ensureCredential: false);
|
||||
|
||||
$backupSet = spec390BackupSetWithItem($tenant);
|
||||
|
||||
$contract = RestoreRunCreatePresenter::contract(
|
||||
data: spec390ReadyWizardData($backupSet),
|
||||
currentStep: 5,
|
||||
compactFlow: true,
|
||||
tenant: $tenant,
|
||||
user: $user,
|
||||
);
|
||||
|
||||
expect(data_get($contract, 'readinessGuidance.state'))->toBe(RestoreReadinessState::Blocked->value)
|
||||
->and(data_get($contract, 'readinessGuidance.reason'))->toBe(RestoreReadinessReason::ExecutionPrerequisiteBlocked->value)
|
||||
->and(data_get($contract, 'readinessGuidance.nextAction'))->toBe(RestoreReadinessAction::ReviewValidationBlockers->value)
|
||||
->and(data_get($contract, 'wizardGate.execution_state'))->toBe('unavailable_until_prerequisites');
|
||||
});
|
||||
|
||||
it('renders persisted restore-run readiness guidance on the view page', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$backupSet = spec390BackupSetWithItem($tenant);
|
||||
$operationRun = OperationRun::factory()->forTenant($tenant)->create([
|
||||
'type' => OperationRunType::RestoreExecute->value,
|
||||
'status' => OperationRunStatus::Running->value,
|
||||
]);
|
||||
$restoreRun = RestoreRun::factory()->for($tenant, 'tenant')->for($backupSet)->create([
|
||||
'status' => RestoreRunStatus::Running->value,
|
||||
'operation_run_id' => (int) $operationRun->getKey(),
|
||||
]);
|
||||
$sideEffectCounts = spec390GuidanceSideEffectCounts();
|
||||
|
||||
Livewire::test(ViewRestoreRun::class, ['record' => $restoreRun->getKey()])
|
||||
->assertSee('Restore readiness')
|
||||
->assertSee('Restore execution is in progress.')
|
||||
->assertSee('Next safe action: Open operation')
|
||||
->assertSee('This guidance only opens existing execution evidence.');
|
||||
|
||||
spec390ExpectGuidanceSideEffectCounts($sideEffectCounts);
|
||||
});
|
||||
|
||||
it('lets readonly users inspect persisted readiness guidance but keeps create mutations forbidden', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create();
|
||||
[$user] = createUserWithTenant($tenant, role: 'readonly');
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$backupSet = spec390BackupSetWithItem($tenant);
|
||||
$restoreRun = RestoreRun::factory()->for($tenant, 'tenant')->for($backupSet)->previewOnly()->create();
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(ViewRestoreRun::class, ['record' => $restoreRun->getKey()])
|
||||
->assertSee('Restore readiness')
|
||||
->assertSee("Restore can't continue yet.");
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(RestoreRunResource::getUrl('create', panel: 'admin', tenant: $tenant))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('preserves final execution confirmation and safety gates', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create([
|
||||
'rbac_status' => 'ok',
|
||||
'rbac_last_checked_at' => now(),
|
||||
]);
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant($tenant, true);
|
||||
ensureDefaultProviderConnection($tenant, 'microsoft');
|
||||
|
||||
$backupSet = spec390BackupSetWithItem($tenant);
|
||||
$sideEffectCounts = spec390GuidanceSideEffectCounts();
|
||||
|
||||
$this->mock(RestoreService::class, function (MockInterface $mock): void {
|
||||
$mock->shouldNotReceive('preview');
|
||||
$mock->shouldNotReceive('execute');
|
||||
});
|
||||
|
||||
Livewire::test(CreateRestoreRun::class)
|
||||
->fillForm([
|
||||
...spec390ReadyWizardData($backupSet),
|
||||
'is_dry_run' => false,
|
||||
'acknowledged_impact' => false,
|
||||
'tenant_confirm' => null,
|
||||
])
|
||||
->call('create')
|
||||
->assertHasFormErrors(['acknowledged_impact']);
|
||||
|
||||
spec390ExpectGuidanceSideEffectCounts($sideEffectCounts);
|
||||
});
|
||||
@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\RestoreRun;
|
||||
use App\Support\RestoreReadinessResolution\RestoreReadinessAction;
|
||||
use App\Support\RestoreReadinessResolution\RestoreReadinessReason;
|
||||
use App\Support\RestoreReadinessResolution\RestoreReadinessResolver;
|
||||
use App\Support\RestoreReadinessResolution\RestoreReadinessState;
|
||||
use App\Support\RestoreRunStatus;
|
||||
use App\Support\RestoreSafety\RestoreSafetyResolver;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function spec390Resolver(): RestoreReadinessResolver
|
||||
{
|
||||
return app(RestoreReadinessResolver::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function spec390RestoreDraft(array $overrides = []): array
|
||||
{
|
||||
return [
|
||||
'backup_set_id' => 10,
|
||||
'scope_mode' => 'selected',
|
||||
'backup_item_ids' => [1, 2],
|
||||
'group_mapping' => ['source-group' => 'target-group'],
|
||||
...$overrides,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function spec390CurrentChecksAndPreview(array $overrides = []): array
|
||||
{
|
||||
/** @var RestoreSafetyResolver $safety */
|
||||
$safety = app(RestoreSafetyResolver::class);
|
||||
|
||||
$data = spec390RestoreDraft([
|
||||
'check_summary' => ['blocking' => 0, 'warning' => 0, 'safe' => 2],
|
||||
'check_results' => [['code' => 'safe', 'severity' => 'safe']],
|
||||
'checks_ran_at' => now('UTC')->toIso8601String(),
|
||||
'preview_summary' => ['generated_at' => now('UTC')->toIso8601String(), 'policies_total' => 1],
|
||||
'preview_diffs' => [['policy_identifier' => 'policy-1']],
|
||||
'preview_ran_at' => now('UTC')->toIso8601String(),
|
||||
...$overrides,
|
||||
]);
|
||||
|
||||
$data['check_basis'] = $safety->checksBasisFromData($data);
|
||||
$data['preview_basis'] = $safety->previewBasisFromData($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
it('derives missing checks guidance before preview guidance', function (): void {
|
||||
$summary = spec390Resolver()
|
||||
->forWizardData(
|
||||
data: spec390RestoreDraft(),
|
||||
hasUsableSource: true,
|
||||
scopeDefined: true,
|
||||
scopeDependencyResolved: true,
|
||||
)
|
||||
->toArray();
|
||||
|
||||
expect($summary['state'])->toBe(RestoreReadinessState::Blocked->value)
|
||||
->and($summary['reason'])->toBe(RestoreReadinessReason::ChecksNotRun->value)
|
||||
->and($summary['nextAction'])->toBe(RestoreReadinessAction::RunReadinessChecks->value)
|
||||
->and($summary['actionSafetyCopy'])->toBe('This will not execute the restore.');
|
||||
});
|
||||
|
||||
it('prioritizes stale checks over a current preview', function (): void {
|
||||
/** @var RestoreSafetyResolver $safety */
|
||||
$safety = app(RestoreSafetyResolver::class);
|
||||
|
||||
$current = spec390CurrentChecksAndPreview();
|
||||
$previousChecks = [
|
||||
...$current,
|
||||
'backup_item_ids' => [1],
|
||||
];
|
||||
$current['check_basis'] = $safety->checksBasisFromData($previousChecks);
|
||||
|
||||
$summary = spec390Resolver()
|
||||
->forWizardData($current, true, true, true)
|
||||
->toArray();
|
||||
|
||||
expect($summary['reason'])->toBe(RestoreReadinessReason::ChecksStale->value)
|
||||
->and($summary['nextAction'])->toBe(RestoreReadinessAction::RunReadinessChecks->value);
|
||||
});
|
||||
|
||||
it('derives missing and stale preview guidance after current checks', function (): void {
|
||||
$missing = spec390CurrentChecksAndPreview([
|
||||
'preview_summary' => [],
|
||||
'preview_diffs' => [],
|
||||
'preview_ran_at' => null,
|
||||
'preview_basis' => null,
|
||||
]);
|
||||
unset($missing['preview_basis']);
|
||||
|
||||
$missingSummary = spec390Resolver()
|
||||
->forWizardData($missing, true, true, true)
|
||||
->toArray();
|
||||
|
||||
/** @var RestoreSafetyResolver $safety */
|
||||
$safety = app(RestoreSafetyResolver::class);
|
||||
|
||||
$stale = spec390CurrentChecksAndPreview();
|
||||
$previousPreview = [
|
||||
...$stale,
|
||||
'backup_item_ids' => [1],
|
||||
];
|
||||
$stale['preview_basis'] = $safety->previewBasisFromData($previousPreview);
|
||||
|
||||
$staleSummary = spec390Resolver()
|
||||
->forWizardData($stale, true, true, true)
|
||||
->toArray();
|
||||
|
||||
expect($missingSummary['reason'])->toBe(RestoreReadinessReason::PreviewMissing->value)
|
||||
->and($missingSummary['nextAction'])->toBe(RestoreReadinessAction::GeneratePreview->value)
|
||||
->and($staleSummary['reason'])->toBe(RestoreReadinessReason::PreviewStale->value)
|
||||
->and($staleSummary['nextAction'])->toBe(RestoreReadinessAction::RegeneratePreview->value);
|
||||
});
|
||||
|
||||
it('recognizes current checks and preview as ready for final confirmation', function (): void {
|
||||
$summary = spec390Resolver()
|
||||
->forWizardData(
|
||||
data: spec390CurrentChecksAndPreview(),
|
||||
hasUsableSource: true,
|
||||
scopeDefined: true,
|
||||
scopeDependencyResolved: true,
|
||||
executionReadiness: ['allowed' => true],
|
||||
)
|
||||
->toArray();
|
||||
|
||||
expect($summary['state'])->toBe(RestoreReadinessState::ReadyForConfirmation->value)
|
||||
->and($summary['reason'])->toBe(RestoreReadinessReason::ReadyForConfirmation->value)
|
||||
->and($summary['nextAction'])->toBe(RestoreReadinessAction::ContinueToConfirmation->value)
|
||||
->and($summary['actionSafetyCopy'])->toBe('The restore still requires final confirmation before execution.');
|
||||
});
|
||||
|
||||
it('blocks final readiness when execution prerequisites are unavailable', function (): void {
|
||||
$summary = spec390Resolver()
|
||||
->forWizardData(
|
||||
data: spec390CurrentChecksAndPreview(),
|
||||
hasUsableSource: true,
|
||||
scopeDefined: true,
|
||||
scopeDependencyResolved: true,
|
||||
executionReadiness: [
|
||||
'allowed' => false,
|
||||
'display_summary' => 'Provider readiness or restore prerequisites currently prevent real execution.',
|
||||
],
|
||||
)
|
||||
->toArray();
|
||||
|
||||
expect($summary['state'])->toBe(RestoreReadinessState::Blocked->value)
|
||||
->and($summary['reason'])->toBe(RestoreReadinessReason::ExecutionPrerequisiteBlocked->value)
|
||||
->and($summary['nextAction'])->toBe(RestoreReadinessAction::ReviewValidationBlockers->value)
|
||||
->and($summary['actionSafetyCopy'])->toBe('This guidance does not execute the restore.');
|
||||
});
|
||||
|
||||
it('maps persisted execution and historical restore statuses without preparation mutations', function (string $status, string $state, string $reason): void {
|
||||
$run = new RestoreRun([
|
||||
'status' => $status,
|
||||
'backup_set_id' => 10,
|
||||
'requested_items' => [],
|
||||
'group_mapping' => [],
|
||||
'metadata' => [],
|
||||
]);
|
||||
|
||||
$summary = spec390Resolver()->forRestoreRun($run)->toArray();
|
||||
|
||||
expect($summary['state'])->toBe($state)
|
||||
->and($summary['reason'])->toBe($reason)
|
||||
->and($summary['mutatesPreparation'])->toBeFalse();
|
||||
})->with([
|
||||
'queued' => [RestoreRunStatus::Queued->value, RestoreReadinessState::Executing->value, RestoreReadinessReason::ExecutionInProgress->value],
|
||||
'running' => [RestoreRunStatus::Running->value, RestoreReadinessState::Executing->value, RestoreReadinessReason::ExecutionInProgress->value],
|
||||
'completed' => [RestoreRunStatus::Completed->value, RestoreReadinessState::Completed->value, RestoreReadinessReason::ExecutionCompleted->value],
|
||||
'partial' => [RestoreRunStatus::Partial->value, RestoreReadinessState::Completed->value, RestoreReadinessReason::ExecutionCompleted->value],
|
||||
'failed' => [RestoreRunStatus::Failed->value, RestoreReadinessState::Failed->value, RestoreReadinessReason::ExecutionFailed->value],
|
||||
'cancelled' => [RestoreRunStatus::Cancelled->value, RestoreReadinessState::Cancelled->value, RestoreReadinessReason::ExecutionCancelled->value],
|
||||
'aborted' => [RestoreRunStatus::Aborted->value, RestoreReadinessState::Cancelled->value, RestoreReadinessReason::ExecutionCancelled->value],
|
||||
'completed with errors' => [RestoreRunStatus::CompletedWithErrors->value, RestoreReadinessState::Completed->value, RestoreReadinessReason::ExecutionCompleted->value],
|
||||
]);
|
||||
|
||||
it('compares guidance basis fingerprints for stale action rejection without side effects', function (): void {
|
||||
$resolver = spec390Resolver();
|
||||
$original = spec390CurrentChecksAndPreview();
|
||||
$basis = $resolver->basisForData($original);
|
||||
|
||||
expect($resolver->actionBasisMatches($basis->fingerprint, $original))->toBeTrue();
|
||||
|
||||
$changed = [
|
||||
...$original,
|
||||
'backup_item_ids' => [1, 2, 3],
|
||||
];
|
||||
|
||||
$restoreRunsBefore = RestoreRun::query()->count();
|
||||
|
||||
expect($resolver->actionBasisMatches($basis->fingerprint, $changed))->toBeFalse()
|
||||
->and(RestoreRun::query()->count())->toBe($restoreRunsBefore);
|
||||
});
|
||||
@ -40,7 +40,7 @@ ## Spec 325 Target Image Coverage
|
||||
| Governance Inbox | UI-028 | `target-experience-briefs/governance-inbox.md` | No |
|
||||
| Customer Review Workspace | UI-038 | `target-experience-briefs/customer-review-workspace.md` | No |
|
||||
| Audit Log | UI-025 | `target-experience-briefs/audit-log.md` | No |
|
||||
| Restore Safety Workflow | UI-053, UI-054 | `target-experience-briefs/restore-safety-workflow.md` | No |
|
||||
| Restore Safety Workflow | UI-053, UI-054 | `target-experience-briefs/restore-safety-workflow.md` | Partial - Spec 390 adds Restore-local readiness guidance on create/view surfaces; list and full browser fixture coverage remain separate. |
|
||||
| Provider Readiness | UI-072, UI-073 | `target-experience-briefs/provider-readiness.md` | No |
|
||||
| Baseline Compare / Drift | UI-061 | `target-experience-briefs/baseline-compare-drift.md` | No |
|
||||
|
||||
@ -54,7 +54,7 @@ ## Coverage By Area
|
||||
| Inventory | 8 | Route-discovered only; coverage, policy version detail, and raw-data exposure need later review. |
|
||||
| Evidence / audit | 8 | Audit log captured; evidence/report detail routes need customer-safe progressive-disclosure review. |
|
||||
| Reviews | 8 | Review register, customer workspace, review pack detail, rendered-report, and the Spec 386 publication-resolution workflow now have bounded browser evidence; Spec 366 adds rendered-report profile, print, and mobile-ish captures while deeper evidence/report surfaces still remain open elsewhere. |
|
||||
| Backup / restore | 6 | High-risk area; Spec 371 adds seeded browser proof for Backup Sets list/detail, while restore runs and create/failure workflow states remain unresolved. |
|
||||
| Backup / restore | 6 | High-risk area; Spec 371 adds seeded browser proof for Backup Sets list/detail. Spec 390 adds Restore create/view readiness guidance; Restore list and broader failure/conflict browser coverage remain unresolved. |
|
||||
| Settings / admin | 5 | Workspace and environment access are RBAC-sensitive and need later review. |
|
||||
| Provider / integration | 5 | Provider connections and required permissions are captured; create/edit/onboarding remain high-risk unresolved surfaces. |
|
||||
| Findings | 5 | Queue/inbox patterns captured; finding detail needs individual triage target. |
|
||||
|
||||
@ -9,7 +9,7 @@ # UI-014 Restore Runs
|
||||
| Design depth | Strategic Surface |
|
||||
| Repo truth | repo-verified route; browser blocked |
|
||||
| Screenshot | `../screenshots/desktop/ui-014-restore-runs.png` |
|
||||
| Browser status | Local Spec 180 fixture returned Forbidden. |
|
||||
| Browser status | Local Spec 180 fixture returned Forbidden; Spec 390 adds create/view readiness guidance and requires a focused smoke fixture. |
|
||||
|
||||
## First Five Seconds
|
||||
|
||||
@ -40,9 +40,9 @@ ## Scores
|
||||
## Top Issues
|
||||
|
||||
1. Browser review blocked by local capability/data.
|
||||
2. Restore workflow needs individual target mockup and safety review.
|
||||
2. Restore workflow needs continued browser proof for broader failure/conflict/confirmation states after Spec 390 readiness guidance.
|
||||
3. Preview/result/evidence truth must be separated visually.
|
||||
|
||||
## Target Direction
|
||||
|
||||
P0 restore safety target mockup and implementation-wave smoke with an authorized fixture.
|
||||
P0 restore safety implementation-wave smoke with an authorized fixture. Spec 390 covers Restore-local readiness and next-safe-action guidance on create/view; list and broader terminal/failure states still need follow-up coverage.
|
||||
|
||||
@ -61,7 +61,7 @@ # Route Inventory
|
||||
| UI-051 | `/admin/workspaces/{workspace}/environments/{environment}/backup-sets` | resource | Backup Sets | Backup / restore | environment-bound | reachable | environment entitlement + backup capability | Backup / Restore | Evidence / Audit | Strategic Surface | browser-verified | [desktop](../../specs/371-core-operator-view-surfaces-productization/artifacts/screenshots/spec371-backup-set-productization-01-backup-sets-list.png) | [report](page-reports/ui-013-environment-backup-sets.md) | Spec 371 verifies seeded Backup Sets list/detail with restore-point decision, included items, and secondary technical detail. |
|
||||
| UI-052 | `/admin/workspaces/{workspace}/environments/{environment}/backup-sets/create` and `/view` | resource | Backup Set Create/View | Backup / restore | environment record/workflow | route exists | backup capability | Backup / Restore | Evidence / Audit | Strategic Surface | repo-verified | - | - | Backup creation plus partial/failure restore-point states still need separate seeded workflow coverage. |
|
||||
| UI-053 | `/admin/workspaces/{workspace}/environments/{environment}/restore-runs` | resource | Restore Runs | Backup / restore | environment-bound | browser blocked by capability in fixture | environment entitlement + restore capability | Backup / Restore | Operations / Monitoring | Strategic Surface | repo-verified | [blocked](screenshots/desktop/ui-014-restore-runs.png) | [report](page-reports/ui-014-restore-runs.md) | Route exists; local fixture returned Forbidden. |
|
||||
| UI-054 | `/admin/workspaces/{workspace}/environments/{environment}/restore-runs/create` and `/view` | resource | Restore Run Create/View | Backup / restore | environment record/workflow | route exists | restore capability | Backup / Restore | Operations / Monitoring | Strategic Surface | repo-verified | - | - | Destructive/high-impact workflow; individual target spec required. |
|
||||
| UI-054 | `/admin/workspaces/{workspace}/environments/{environment}/restore-runs/create` and `/view` | resource | Restore Run Create/View | Backup / restore | environment record/workflow | route exists | restore capability | Backup / Restore | Operations / Monitoring | Strategic Surface | repo-verified | - | - | Spec 390 adds Restore-local readiness guidance and next-safe-action copy on create/view while preserving final confirmation and OperationRun truth. Broader failure/conflict browser coverage remains follow-up. |
|
||||
| UI-055 | `/admin/baseline-profiles` | resource | Baseline Profiles | Governance | workspace analysis | reachable | workspace member | Drift / Diff | Settings / Admin | Strategic Surface | repo-verified | [desktop](screenshots/desktop/ui-010-baseline-profiles.png) | [report](page-reports/ui-010-baseline-profiles.md) | Workspace-owned baseline library. |
|
||||
| UI-056 | `/admin/baseline-profiles/create` | resource | Create Baseline Profile | Governance | workspace analysis | route exists | baseline capability | Drift / Diff | Settings / Admin | Domain Pattern Surface | repo-verified | - | - | Workspace-owned form. |
|
||||
| UI-057 | `/admin/baseline-profiles/{record}` and `/edit` | resource | Baseline Profile Detail/Edit | Governance | workspace record | route exists | baseline capability | Drift / Diff | Evidence / Audit | Strategic Surface | repo-verified | - | - | Capture/compare actions need dangerous-action audit. |
|
||||
|
||||
@ -15,7 +15,7 @@ ## Metadata
|
||||
|
||||
## Current-State Snapshot
|
||||
|
||||
The browser pass could not evaluate the product page because restore capability/state was unavailable. The page remains strategic because restore is the highest-risk operator workflow in the product.
|
||||
The browser pass could not evaluate the product page because restore capability/state was unavailable. The page remains strategic because restore is the highest-risk operator workflow in the product. Spec 390 implements a bounded runtime slice: Restore-local readiness guidance and next-safe-action copy on the existing create/view surfaces without new persistence, navigation, or execution semantics.
|
||||
|
||||
## Current-State Productization Problems
|
||||
|
||||
@ -132,4 +132,4 @@ ## Later Implementation Candidate
|
||||
|
||||
## Non-Goals For Later Implementation
|
||||
|
||||
Do not implement restore execution semantics, provider writes, or compatibility behavior from this target artifact alone.
|
||||
Do not implement restore execution semantics, provider writes, or compatibility behavior from this target artifact alone. Spec 390 intentionally keeps readiness guidance derived and local while final confirmation, execution, and evidence remain owned by existing Restore and OperationRun paths.
|
||||
|
||||
@ -20,7 +20,7 @@ # Unresolved Pages
|
||||
| UI-049 | Backup Schedules | Strategic backup schedule page was not captured. | Environment with active, paused, failing, and never-run schedules. | Include in backup/restore safety spec. |
|
||||
| UI-052 | Backup Set Create/View | Backup workflow/detail route needs capability and backup data. | Create form, backup-set view, partial/failure states. | Add backup workflow target. |
|
||||
| UI-053 | Restore Runs | Browser returned Forbidden for the local fixture. | Capability-backed environment with restore-run records. | Re-test with seeded capability; screenshot exists as blocker evidence. |
|
||||
| UI-054 | Restore Run Create/View | Restore create/view route needs capability and restore data. | Dry-run, validation, partial restore, conflict, and confirmation states. | Add restore workflow target. |
|
||||
| UI-054 | Restore Run Create/View | Partially addressed by Spec 390 readiness guidance; broader restore create/view route still needs capability-backed browser evidence across dry-run, validation, partial restore, conflict, and confirmation states. | Dry-run, validation, partial restore, conflict, and confirmation states. | Re-test with Spec 390 fixture and keep broader restore workflow target. |
|
||||
| UI-057 | Baseline Profile Detail/Edit | Baseline record detail/edit was not captured. | Baseline profile with assigned environments and compare/capture state. | Add baseline detail target. |
|
||||
| UI-058 | Baseline Compare Matrix | Dynamic compare matrix was not captured. | Baseline compare record with pass/fail/unknown dimensions. | Add compare matrix target. |
|
||||
| UI-061 | Baseline Compare | Tested environment variants returned 404 despite route-list presence. | Valid environment/baseline assignment fixture. | Reconcile route binding/fixture and re-capture. |
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
# Current Restore Flow Inventory
|
||||
|
||||
## Verified Code Seams
|
||||
|
||||
| Area | Repo truth |
|
||||
| --- | --- |
|
||||
| Resource | `apps/platform/app/Filament/Resources/RestoreRunResource.php` owns the existing list/create/view resource and environment-scoped routing. |
|
||||
| Create page | `CreateRestoreRun` requires tenant manage capability before the wizard is usable. |
|
||||
| View page | `ViewRestoreRun` resolves records through `RestoreRunResource::resolveScopedRecordOrFail()`. |
|
||||
| Create presenter | `RestoreRunCreatePresenter::contract()` already produces passive wizard view data. |
|
||||
| Detail presenter | `RestoreRunDetailPresenter::forRun()` already owns decision-first detail/view state, OperationRun proof, and evidence links. |
|
||||
| Safety truth | `RestoreSafetyResolver` owns scope fingerprints, checks integrity, preview integrity, and execution safety. |
|
||||
| Status truth | `RestoreRunStatus` defines draft/scoped/checked/previewed, pending/queued/running, terminal, and legacy terminal statuses. |
|
||||
| Operation links | `OperationRunLinks::tenantlessView()` is the canonical link path for OperationRun detail. |
|
||||
|
||||
## Existing Safety Gates To Reuse
|
||||
|
||||
- Scope and mapping changes call `synchronizeRestoreSafetyDraft()` and invalidate checks/preview basis.
|
||||
- `run_restore_checks` writes current check basis into wizard state only after existing validation runs.
|
||||
- `run_restore_preview` writes current preview basis into wizard state only after existing preview generation.
|
||||
- Real execution remains blocked by final confirmation, hard-confirm text, current checks, current preview, write gate, and `OperationRun` queueing.
|
||||
|
||||
## Implementation Seams Selected
|
||||
|
||||
- Add Restore-local, non-persisted readiness derivation under `apps/platform/app/Support/RestoreReadinessResolution/`.
|
||||
- Add `readinessGuidance` to `RestoreRunCreatePresenter::contract()` and `RestoreRunDetailPresenter::forRun()`.
|
||||
- Render guidance through existing Blade view fields/infolist entries.
|
||||
- Disable `RestoreRunResource` global search explicitly because the resource is tenant-sensitive and this spec does not introduce a safe search contract.
|
||||
|
||||
## Explicit Non-Seams
|
||||
|
||||
- No database migration.
|
||||
- No review-publication resolution table/model reuse.
|
||||
- No generic resolution registry.
|
||||
- No new route, navigation item, dashboard widget, notification center intake, or Graph call.
|
||||
@ -0,0 +1,21 @@
|
||||
# Restore Readiness State Matrix
|
||||
|
||||
| Priority | Input condition | State | Reason | Next safe action |
|
||||
| ---: | --- | --- | --- | --- |
|
||||
| 1 | No backup set selected | blocked | source_required | review_backup_selection |
|
||||
| 2 | Backup source has no usable payload | blocked | source_unusable | review_backup_selection |
|
||||
| 3 | Scope is not defined | needs_preparation | scope_required | define_scope |
|
||||
| 4 | Required group mappings remain unresolved | needs_preparation | group_mapping_required | review_group_mappings |
|
||||
| 5 | Checks evidence is missing | blocked | checks_not_run | run_readiness_checks |
|
||||
| 6 | Checks evidence is stale/invalidated | blocked | checks_stale | run_readiness_checks |
|
||||
| 7 | Current checks have blockers | blocked | checks_blocking | review_validation_blockers |
|
||||
| 8 | Preview evidence is missing | needs_preparation | preview_missing | generate_preview |
|
||||
| 9 | Preview evidence is stale/invalidated | needs_preparation | preview_stale | regenerate_preview |
|
||||
| 10 | Checks and preview are current, but execution prerequisites are blocked | blocked | execution_prerequisite_blocked | review_validation_blockers |
|
||||
| 11 | Checks and preview are current and execution prerequisites are available | ready_for_confirmation | ready_for_confirmation | continue_to_confirmation |
|
||||
| 12 | RestoreRun is pending/queued/running | executing | execution_in_progress | open_operation |
|
||||
| 13 | RestoreRun is completed/partial/completed_with_errors | completed | execution_completed | inspect_restore_results or inspect_evidence |
|
||||
| 14 | RestoreRun is failed | failed | execution_failed | inspect_restore_results |
|
||||
| 15 | RestoreRun is cancelled/aborted | cancelled | execution_cancelled | inspect_restore_results |
|
||||
|
||||
Checks are intentionally evaluated before preview so a stale checks basis cannot be hidden by a current preview basis.
|
||||
@ -0,0 +1,15 @@
|
||||
# Restore Requirement Map
|
||||
|
||||
| Readiness reason | Existing source reused | Authorization expectation |
|
||||
| --- | --- | --- |
|
||||
| source_required / source_unusable | Restore wizard `backup_set_id`, `BackupSet`, `BackupQualityResolver` | Tenant manage on create; tenant view on persisted view. |
|
||||
| scope_required | Wizard `scope_mode`, `backup_item_ids`, `RestoreSafetyResolver::scopeFingerprintFromData()` | Tenant manage on create. |
|
||||
| group_mapping_required | Existing group mapping resolver summary and `group_mapping` wizard state | Tenant manage on create. |
|
||||
| checks_not_run / checks_stale / checks_blocking | `RestoreSafetyResolver::checksIntegrityFromData()` and existing check basis fields | Mutating checks action remains the existing tenant-manage wizard action; V1 guidance is passive copy. |
|
||||
| preview_missing / preview_stale | `RestoreSafetyResolver::previewIntegrityFromData()` and existing preview basis fields | Mutating preview action remains the existing tenant-manage wizard action; V1 guidance is passive copy. |
|
||||
| execution_prerequisite_blocked | `RestoreSafetyResolver::executionReadiness()` through the existing create presenter | Existing execution/write gates remain canonical. |
|
||||
| ready_for_confirmation | Current checks + current preview + existing execution readiness | Final execution still requires existing confirmation controls. |
|
||||
| execution_in_progress | `RestoreRun.status`, scoped `operationRun`, `OperationRunLinks` | Existing OperationRun policy/link scoping. |
|
||||
| terminal/historical states | `RestoreRun.status`, `results`, `metadata`, scoped OperationRun/evidence links | Tenant view; no preparation mutation. |
|
||||
|
||||
The guidance basis is the existing restore scope fingerprint. V1 exposes no guidance-owned mutating action. Any future mutating guidance action must compare its rendered basis fingerprint with the current scope fingerprint before mutating wizard state.
|
||||
@ -0,0 +1,32 @@
|
||||
# Restore UI Copy Contract
|
||||
|
||||
## Required Copy
|
||||
|
||||
- `Restore can't continue yet.`
|
||||
- `Next safe action`
|
||||
- `This will not execute the restore.`
|
||||
- `This guidance is based on the current restore scope and preview state.`
|
||||
- `The restore still requires final confirmation before execution.`
|
||||
|
||||
## Approved Action Labels
|
||||
|
||||
| Action | Label |
|
||||
| --- | --- |
|
||||
| review_backup_selection | Review backup selection |
|
||||
| define_scope | Define restore scope |
|
||||
| review_group_mappings | Review group mappings |
|
||||
| run_readiness_checks | Run readiness checks |
|
||||
| review_validation_blockers | Review validation blockers |
|
||||
| generate_preview | Generate preview |
|
||||
| regenerate_preview | Regenerate preview |
|
||||
| continue_to_confirmation | Continue to final confirmation |
|
||||
| open_operation | Open operation |
|
||||
| inspect_restore_results | Inspect restore results |
|
||||
| inspect_evidence | Open evidence |
|
||||
|
||||
## Forbidden Copy
|
||||
|
||||
- Anything implying a readiness action executes the restore.
|
||||
- Anything implying a preview or readiness check approves the restore.
|
||||
- Anything implying Restore can bypass final confirmation.
|
||||
- Anything implying OperationRun proof alone proves full environment recovery.
|
||||
@ -154,8 +154,8 @@ ## Security and Authorization Plan
|
||||
|
||||
- Reuse existing workspace/tenant/environment scoping traits and policies used by RestoreRunResource.
|
||||
- Require tenant view capability for inspection.
|
||||
- Require tenant manage capability for mutating preparation actions.
|
||||
- Reject stale actions server-side when the basis/fingerprint no longer matches.
|
||||
- Require tenant manage capability for existing Restore wizard mutating preparation actions.
|
||||
- Keep V1 readiness guidance passive. If a later implementation exposes a guidance-owned mutating action, reject stale actions server-side when the basis/fingerprint no longer matches.
|
||||
- Avoid leaking inaccessible backup payload details, environment names, or OperationRun links.
|
||||
- Preserve existing 403/404 distinction as used by existing RestoreRun pages.
|
||||
|
||||
@ -270,7 +270,7 @@ ### Phase 2 - Readiness Derivation
|
||||
|
||||
### Phase 3 - Stale Basis Protection
|
||||
|
||||
Implement action basis/fingerprint protection for any mutating preparation action exposed by the guidance UI. Reject stale action attempts with a clear notification and no mutation.
|
||||
Implement a guidance basis/fingerprint helper and verify stale basis comparison. V1 does not expose guidance-owned mutating preparation actions; existing Restore wizard actions remain the mutation owners. Any future guidance-owned mutating action must reject stale attempts with a clear notification and no mutation.
|
||||
|
||||
### Phase 4 - Create Wizard Integration
|
||||
|
||||
|
||||
@ -228,15 +228,15 @@ ## UI/UX Surface Classification
|
||||
- Support/raw evidence gating: diagnostic/raw links require authorized view access to the related RestoreRun, BackupSet, environment, or OperationRun record.
|
||||
- Duplicate-truth prevention: RestoreSafetyResolver remains checks/preview currentness truth; RestoreRun remains persisted restore status truth; OperationRun remains execution/evidence truth.
|
||||
- Status-like readiness cues must use existing BadgeCatalog/BadgeRenderer or another central shared status path when rendered as badges; page-local badge colors or ad hoc status styling are not allowed.
|
||||
- Multiple audience handling: read-only users get inspection-only affordances; tenant-manage users may see preparation actions; non-members must not learn the record exists.
|
||||
- Multiple audience handling: read-only users get inspection-only affordances; tenant-manage users may use the existing Restore wizard preparation controls. V1 readiness guidance itself remains passive and does not introduce new guidance-owned mutating buttons; non-members must not learn the record exists.
|
||||
|
||||
## UI Action Matrix
|
||||
|
||||
| Action | Surface | Mutates preparation state | Executes Restore | Authorization | Confirmation |
|
||||
| --- | --- | ---: | ---: | --- | --- |
|
||||
| Run readiness checks | RestoreRun create wizard | Yes | No | Tenant manage | Existing action semantics, add confirmation only if implementation makes it high-impact/destructive. |
|
||||
| Generate preview | RestoreRun create wizard | Yes | No | Tenant manage | Existing action semantics. |
|
||||
| Regenerate preview | RestoreRun create wizard | Yes | No | Tenant manage | Existing action semantics, stale basis protection required. |
|
||||
| Run readiness checks | RestoreRun create wizard | Yes, through existing wizard control | No | Tenant manage | Existing action semantics, add confirmation only if implementation makes it high-impact/destructive. V1 guidance only labels this as the next safe action. |
|
||||
| Generate preview | RestoreRun create wizard | Yes, through existing wizard control | No | Tenant manage | Existing action semantics. V1 guidance only labels this as the next safe action. |
|
||||
| Regenerate preview | RestoreRun create wizard | Yes, through existing wizard control | No | Tenant manage | Existing action semantics. V1 guidance only labels this as the next safe action. Any future guidance-owned mutate button must validate stale basis before mutation. |
|
||||
| Review group mappings | RestoreRun create wizard | User edits existing input | No | Tenant manage | Not destructive by itself. |
|
||||
| Open evidence | RestoreRun create/view | No | No | Tenant view for linked record | Navigation action only. |
|
||||
| Continue to confirmation | RestoreRun create wizard | No direct mutation | No | Tenant manage | Existing final confirmation still required before execution. |
|
||||
@ -252,7 +252,7 @@ ## Key Entities and Concepts
|
||||
- **RestoreSafetyResolver**: Existing source for scope basis, preview basis, checks basis, preview integrity, checks integrity, and execution readiness concepts.
|
||||
- **Restore Readiness Summary**: New local derived contract describing the current Restore preparation decision and reason. It is not a database entity.
|
||||
- **Restore Next Safe Action**: New local derived contract describing a safe preparation or inspection action. It is not an execution command.
|
||||
- **Restore Guidance Basis/Fingerprint**: New or reused deterministic basis value used to reject stale readiness actions.
|
||||
- **Restore Guidance Basis/Fingerprint**: New or reused deterministic basis value used by the resolver and tests to compare rendered guidance against current Restore scope. V1 exposes no guidance-owned mutating action; any future mutating guidance action must use this value to reject stale attempts before mutation.
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
@ -296,7 +296,7 @@ ## Testing Expectations
|
||||
Implementation must include:
|
||||
|
||||
- Unit tests for Restore readiness derivation from representative Restore states.
|
||||
- Unit or feature tests for stale basis protection.
|
||||
- Unit tests for stale basis comparison, plus feature coverage proving V1 guidance stays passive and existing Restore execution gates remain canonical.
|
||||
- Feature/Filament tests for RestoreRun create wizard guidance and action authorization.
|
||||
- Feature/Filament tests for RestoreRun view guidance on persisted records.
|
||||
- Tests proving existing execution confirmation/safety gates are still required.
|
||||
@ -307,7 +307,7 @@ ## Testing / Lane / Runtime Impact
|
||||
- **Runtime behavior impact**: Yes. The implementation changes visible RestoreRun create/view behavior and adds derived readiness support code.
|
||||
- **Affected validation lanes**:
|
||||
- Unit: Restore-local readiness resolver, reason/action selection, stale basis comparison, side-effect-free behavior.
|
||||
- Feature/Filament: RestoreRun create/view rendering, action authorization, stale-action rejection, execution-gate preservation.
|
||||
- Feature/Filament: RestoreRun create/view rendering, authorization boundaries, passive guidance behavior, execution-prerequisite blocking, and execution-gate preservation.
|
||||
- Browser: Feature-local smoke for blocked, stale, ready, and read-only visible UI states.
|
||||
- **Fixture/helper/factory/seed/context cost**: Use existing Workspace, ManagedEnvironment, BackupSet, RestoreRun, OperationRun, membership, and capability fixtures/factories. Do not introduce provider, workspace, membership, or browser context as implicit defaults in shared helpers.
|
||||
- **Heavy-family decision**: Keep. Browser coverage is feature-local smoke because the feature materially changes visible operator workflow; do not create a new broad heavy-governance family.
|
||||
|
||||
@ -16,128 +16,128 @@ ## Execution Rules
|
||||
|
||||
## Phase 1: Repo Verification and Spec-Package Contracts
|
||||
|
||||
- [ ] T001 Verify current RestoreRun create/view routes, page classes, presenter classes, actions, and authorization methods in `apps/platform/app/Filament/Resources/RestoreRunResource.php` and nested page/presenter files.
|
||||
- [ ] T002 Verify current RestoreSafetyResolver public methods and state contracts in `apps/platform/app/Support/RestoreSafety/RestoreSafetyResolver.php`.
|
||||
- [ ] T003 Verify current RestoreRun status family in `apps/platform/app/Support/RestoreRunStatus.php`, including legacy statuses.
|
||||
- [ ] T004 Verify OperationRun link/presenter/helper patterns used by RestoreRun and related operational pages.
|
||||
- [ ] T005 Verify existing test fixture/factory patterns for RestoreRun, BackupSet, ManagedEnvironment, Workspace, and OperationRun.
|
||||
- [ ] T006 Verify `RestoreRunResource` global-search behavior: confirm it has View/Edit page if searchable, or explicitly disable global search if implementation changes search behavior.
|
||||
- [ ] T007 Create `specs/390-restore-readiness-resolution-adapter-v1/artifacts/current-restore-flow-inventory.md` documenting the verified Restore flow, existing safety gates, and exact code seams to reuse.
|
||||
- [ ] T008 Create `specs/390-restore-readiness-resolution-adapter-v1/contracts/restore-readiness-state-matrix.md` defining Restore-local states, reasons, and priority ordering.
|
||||
- [ ] T009 Create `specs/390-restore-readiness-resolution-adapter-v1/contracts/restore-requirement-map.md` mapping each readiness reason to existing RestoreSafetyResolver inputs, RestoreRun fields, and required authorization.
|
||||
- [ ] T010 Create `specs/390-restore-readiness-resolution-adapter-v1/contracts/restore-ui-copy-contract.md` with approved decision-first copy and forbidden execution-implying copy.
|
||||
- [ ] T011 Re-check the proportionality review after the contract files are drafted; update `spec.md` and `plan.md` before coding if any task implies new persistence or generic infrastructure.
|
||||
- [x] T001 Verify current RestoreRun create/view routes, page classes, presenter classes, actions, and authorization methods in `apps/platform/app/Filament/Resources/RestoreRunResource.php` and nested page/presenter files.
|
||||
- [x] T002 Verify current RestoreSafetyResolver public methods and state contracts in `apps/platform/app/Support/RestoreSafety/RestoreSafetyResolver.php`.
|
||||
- [x] T003 Verify current RestoreRun status family in `apps/platform/app/Support/RestoreRunStatus.php`, including legacy statuses.
|
||||
- [x] T004 Verify OperationRun link/presenter/helper patterns used by RestoreRun and related operational pages.
|
||||
- [x] T005 Verify existing test fixture/factory patterns for RestoreRun, BackupSet, ManagedEnvironment, Workspace, and OperationRun.
|
||||
- [x] T006 Verify `RestoreRunResource` global-search behavior: confirm it has View/Edit page if searchable, or explicitly disable global search if implementation changes search behavior.
|
||||
- [x] T007 Create `specs/390-restore-readiness-resolution-adapter-v1/artifacts/current-restore-flow-inventory.md` documenting the verified Restore flow, existing safety gates, and exact code seams to reuse.
|
||||
- [x] T008 Create `specs/390-restore-readiness-resolution-adapter-v1/contracts/restore-readiness-state-matrix.md` defining Restore-local states, reasons, and priority ordering.
|
||||
- [x] T009 Create `specs/390-restore-readiness-resolution-adapter-v1/contracts/restore-requirement-map.md` mapping each readiness reason to existing RestoreSafetyResolver inputs, RestoreRun fields, and required authorization.
|
||||
- [x] T010 Create `specs/390-restore-readiness-resolution-adapter-v1/contracts/restore-ui-copy-contract.md` with approved decision-first copy and forbidden execution-implying copy.
|
||||
- [x] T011 Re-check the proportionality review after the contract files are drafted; update `spec.md` and `plan.md` before coding if any task implies new persistence or generic infrastructure.
|
||||
|
||||
## Phase 2: Tests First - Readiness Derivation
|
||||
|
||||
- [ ] T012 Add unit test coverage for missing checks readiness in `apps/platform/tests/Unit/Support/RestoreReadinessResolution/`.
|
||||
- [ ] T013 Add unit test coverage for stale checks readiness in `apps/platform/tests/Unit/Support/RestoreReadinessResolution/`.
|
||||
- [ ] T014 Add unit test coverage for missing preview readiness in `apps/platform/tests/Unit/Support/RestoreReadinessResolution/`.
|
||||
- [ ] T015 Add unit test coverage for stale preview readiness in `apps/platform/tests/Unit/Support/RestoreReadinessResolution/`.
|
||||
- [ ] T016 Add unit test coverage for current checks plus current preview ready state.
|
||||
- [ ] T017 Add unit test coverage for group-mapping-required readiness where existing Restore state exposes that requirement.
|
||||
- [ ] T018 Add unit test coverage for queued and running RestoreRun states deferring to OperationRun execution truth.
|
||||
- [ ] T019 Add unit test coverage for completed, partial, failed, cancelled, legacy aborted, and completed-with-errors states as historical/non-actionable.
|
||||
- [ ] T020 Add unit test coverage proving next safe action selection is deterministic for the same Restore state.
|
||||
- [ ] T021 Add unit test coverage proving readiness derivation is side-effect-free and does not persist or dispatch work.
|
||||
- [x] T012 Add unit test coverage for missing checks readiness in `apps/platform/tests/Unit/Support/RestoreReadinessResolution/`.
|
||||
- [x] T013 Add unit test coverage for stale checks readiness in `apps/platform/tests/Unit/Support/RestoreReadinessResolution/`.
|
||||
- [x] T014 Add unit test coverage for missing preview readiness in `apps/platform/tests/Unit/Support/RestoreReadinessResolution/`.
|
||||
- [x] T015 Add unit test coverage for stale preview readiness in `apps/platform/tests/Unit/Support/RestoreReadinessResolution/`.
|
||||
- [x] T016 Add unit test coverage for current checks plus current preview ready state.
|
||||
- [x] T017 Add unit test coverage for group-mapping-required readiness where existing Restore state exposes that requirement.
|
||||
- [x] T018 Add unit test coverage for queued and running RestoreRun states deferring to OperationRun execution truth.
|
||||
- [x] T019 Add unit test coverage for completed, partial, failed, cancelled, legacy aborted, and completed-with-errors states as historical/non-actionable.
|
||||
- [x] T020 Add unit test coverage proving next safe action selection is deterministic for the same Restore state.
|
||||
- [x] T021 Add unit test coverage proving readiness derivation is side-effect-free and does not persist or dispatch work.
|
||||
|
||||
## Phase 3: Tests First - Authorization and Stale Action Protection
|
||||
|
||||
- [ ] T022 Add feature or Filament test proving a tenant-view-only user can inspect allowed readiness guidance but cannot invoke mutating preparation actions.
|
||||
- [ ] T023 Add feature or Filament test proving a user without workspace/environment access cannot inspect Restore readiness through direct route access.
|
||||
- [ ] T024 Add feature or Filament test proving tenant-manage access is required for mutating preparation guidance actions.
|
||||
- [ ] T025 Add feature or unit test proving a stale guidance basis/fingerprint rejects a mutating preparation action without changing Restore state.
|
||||
- [ ] T026 Add feature test proving existing Restore execution still requires final confirmation, hard confirm, and current safety gates.
|
||||
- [ ] T027 Add feature test proving readiness guidance actions do not create OperationRun records directly.
|
||||
- [ ] T028 Add feature test proving readiness guidance does not create review-publication resolution cases or steps.
|
||||
- [x] T022 Add feature or Filament test proving a tenant-view-only user can inspect allowed readiness guidance but cannot invoke mutating preparation actions.
|
||||
- [x] T023 Add feature or Filament test proving a user without workspace/environment access cannot inspect Restore readiness through direct route access.
|
||||
- [x] T024 Verify tenant-manage access remains required for existing Restore preparation mutations; V1 guidance exposes no mutating guidance-owned actions.
|
||||
- [x] T025 Add unit test proving stale guidance basis/fingerprint comparison rejects an out-of-date basis without changing Restore state; V1 guidance exposes no mutating guidance-owned action.
|
||||
- [x] T026 Add feature test proving existing Restore execution still requires final confirmation, hard confirm, and current safety gates.
|
||||
- [x] T027 Add feature test proving readiness guidance actions do not create OperationRun records directly.
|
||||
- [x] T028 Add feature test proving readiness guidance does not create review-publication resolution cases or steps.
|
||||
|
||||
## Phase 4: Restore-Local Support Implementation
|
||||
|
||||
- [ ] T029 Create a Restore-local support namespace under `apps/platform/app/Support/RestoreReadinessResolution/`.
|
||||
- [ ] T030 Implement a Restore readiness summary value object or DTO aligned with `contracts/restore-readiness-state-matrix.md`.
|
||||
- [ ] T031 Implement Restore-local reason identifiers and next-action identifiers without adding a generic taxonomy.
|
||||
- [ ] T032 Implement a side-effect-free Restore readiness resolver that consumes RestoreRun or wizard state plus RestoreSafetyResolver outputs.
|
||||
- [ ] T033 Implement deterministic next-action priority ordering matching the contract matrix.
|
||||
- [ ] T034 Implement a Restore guidance basis/fingerprint helper that reuses existing scope/checks/preview basis concepts where possible.
|
||||
- [ ] T035 Implement presenter-friendly formatting for readiness status, reason, next action, evidence, and no-auto-execute copy.
|
||||
- [ ] T036 Ensure the resolver performs no Graph calls, no queue dispatches, no database writes, and no OperationRun creation.
|
||||
- [ ] T037 Ensure the resolver handles in-memory wizard state without requiring a persisted RestoreRun id.
|
||||
- [ ] T038 Ensure the resolver handles persisted RestoreRun records without creating new resolution cases.
|
||||
- [x] T029 Create a Restore-local support namespace under `apps/platform/app/Support/RestoreReadinessResolution/`.
|
||||
- [x] T030 Implement a Restore readiness summary value object or DTO aligned with `contracts/restore-readiness-state-matrix.md`.
|
||||
- [x] T031 Implement Restore-local reason identifiers and next-action identifiers without adding a generic taxonomy.
|
||||
- [x] T032 Implement a side-effect-free Restore readiness resolver that consumes RestoreRun or wizard state plus RestoreSafetyResolver outputs.
|
||||
- [x] T033 Implement deterministic next-action priority ordering matching the contract matrix.
|
||||
- [x] T034 Implement a Restore guidance basis/fingerprint helper that reuses existing scope/checks/preview basis concepts where possible.
|
||||
- [x] T035 Implement presenter-friendly formatting for readiness status, reason, next action, evidence, and no-auto-execute copy.
|
||||
- [x] T036 Ensure the resolver performs no Graph calls, no queue dispatches, no database writes, and no OperationRun creation.
|
||||
- [x] T037 Ensure the resolver handles in-memory wizard state without requiring a persisted RestoreRun id.
|
||||
- [x] T038 Ensure the resolver handles persisted RestoreRun records without creating new resolution cases.
|
||||
|
||||
## Phase 5: Create Wizard Integration
|
||||
|
||||
- [ ] T039 Integrate Restore readiness guidance into `RestoreRunCreatePresenter` or the existing create wizard presentation path.
|
||||
- [ ] T040 Show blocked guidance for missing checks with "This will not execute the restore" copy.
|
||||
- [ ] T041 Show stale preview guidance with regenerate-preview as the next safe action.
|
||||
- [ ] T042 Show ready-for-confirmation guidance without bypassing existing final confirmation controls.
|
||||
- [ ] T043 Wire preparation actions through existing Restore wizard action methods where possible.
|
||||
- [ ] T044 Add basis/fingerprint validation before any mutating preparation action exposed by the guidance UI.
|
||||
- [ ] T045 Return a clear notification or inline error when a stale action is rejected.
|
||||
- [ ] T046 Ensure read-only users do not see or cannot activate mutating preparation actions.
|
||||
- [ ] T047 Ensure guidance copy fits existing Filament panel layout at desktop and mobile widths.
|
||||
- [ ] T048 Avoid adding heavy assets, custom global CSS, or published Filament internals.
|
||||
- [x] T039 Integrate Restore readiness guidance into `RestoreRunCreatePresenter` or the existing create wizard presentation path.
|
||||
- [x] T040 Show blocked guidance for missing checks with "This will not execute the restore" copy.
|
||||
- [x] T041 Show stale preview guidance with regenerate-preview as the next safe action.
|
||||
- [x] T042 Show ready-for-confirmation guidance without bypassing existing final confirmation controls.
|
||||
- [x] T043 Keep preparation mutations on existing Restore wizard action methods; V1 readiness guidance remains passive next-safe-action copy.
|
||||
- [x] T044 N/A for V1: no mutating preparation action is exposed by readiness guidance; stale basis helper is implemented for future guidance-owned actions.
|
||||
- [x] T045 N/A for V1: no stale guidance-owned action can be invoked; existing wizard action feedback remains unchanged.
|
||||
- [x] T046 Ensure read-only users do not see or cannot activate mutating preparation actions.
|
||||
- [x] T047 Ensure guidance copy fits existing Filament panel layout at desktop and mobile widths.
|
||||
- [x] T048 Avoid adding heavy assets, custom global CSS, or published Filament internals.
|
||||
|
||||
## Phase 6: Persisted RestoreRun View Integration
|
||||
|
||||
- [ ] T049 Integrate local guidance into `RestoreRunDetailPresenter` or the existing RestoreRun view presentation path.
|
||||
- [ ] T050 Show draft/scoped/checked/previewed persisted runs as preparation states where applicable.
|
||||
- [ ] T051 Show pending/queued/running runs as execution states that defer to OperationRun evidence.
|
||||
- [ ] T052 Show completed/partial/failed/cancelled/aborted/completed-with-errors runs as historical result states.
|
||||
- [ ] T053 Add authorized OperationRun evidence links only where the current user may access the linked record.
|
||||
- [ ] T054 Avoid preparation mutation actions on historical/non-actionable RestoreRun states.
|
||||
- [ ] T055 Preserve existing row click, view page, and lifecycle action behavior on RestoreRun list/table.
|
||||
- [x] T049 Integrate local guidance into `RestoreRunDetailPresenter` or the existing RestoreRun view presentation path.
|
||||
- [x] T050 Show draft/scoped/checked/previewed persisted runs as preparation states where applicable.
|
||||
- [x] T051 Show pending/queued/running runs as execution states that defer to OperationRun evidence.
|
||||
- [x] T052 Show completed/partial/failed/cancelled/aborted/completed-with-errors runs as historical result states.
|
||||
- [x] T053 Add authorized OperationRun evidence links only where the current user may access the linked record.
|
||||
- [x] T054 Avoid preparation mutation actions on historical/non-actionable RestoreRun states.
|
||||
- [x] T055 Preserve existing row click, view page, and lifecycle action behavior on RestoreRun list/table.
|
||||
|
||||
## Phase 7: Filament Actions, Notifications, and Empty States
|
||||
|
||||
- [ ] T056 Review any new Filament action against v5 patterns and use `Action::make(...)->action(...)` for execution actions.
|
||||
- [ ] T057 Add `->requiresConfirmation()` to any destructive action if implementation introduces one; otherwise document that no destructive action was introduced.
|
||||
- [ ] T058 Ensure URL-only actions use `->url(...)` and are not described as modal-confirmed actions.
|
||||
- [ ] T059 Add explicit success/error/stale notifications for user-triggered preparation mutations where outcomes are not instantly obvious.
|
||||
- [ ] T060 Add meaningful empty/inactive states for any repeated readiness item or evidence list introduced.
|
||||
- [ ] T061 Confirm no new panel provider registration is required; if it becomes required, register through `bootstrap/providers.php` only.
|
||||
- [x] T056 Review any new Filament action against v5 patterns and use `Action::make(...)->action(...)` for execution actions.
|
||||
- [x] T057 Add `->requiresConfirmation()` to any destructive action if implementation introduces one; otherwise document that no destructive action was introduced.
|
||||
- [x] T058 Ensure URL-only actions use `->url(...)` and are not described as modal-confirmed actions.
|
||||
- [x] T059 N/A for V1 readiness guidance: no new user-triggered preparation mutation is introduced; existing wizard action notifications remain canonical.
|
||||
- [x] T060 Add meaningful empty/inactive states for any repeated readiness item or evidence list introduced.
|
||||
- [x] T061 Confirm no new panel provider registration is required; if it becomes required, register through `bootstrap/providers.php` only.
|
||||
|
||||
## Phase 8: UI Coverage and Browser Smoke
|
||||
|
||||
- [ ] T062 Add or update Filament feature tests for blocked wizard guidance.
|
||||
- [ ] T063 Add or update Filament feature tests for stale preview guidance.
|
||||
- [ ] T064 Add or update Filament feature tests for ready-for-confirmation guidance.
|
||||
- [ ] T065 Add or update Filament feature tests for read-only inspection behavior.
|
||||
- [ ] T066 Add or update Filament feature tests for persisted RestoreRun view guidance.
|
||||
- [ ] T067 Run browser smoke for blocked, stale, ready, and read-only Restore guidance states if visible UI changed.
|
||||
- [ ] T068 Verify no visible copy implies automatic Restore execution.
|
||||
- [ ] T069 Update durable UI/productization coverage registry artifacts under `docs/ui-ux-enterprise-audit/` for the changed RestoreRun create/view surfaces.
|
||||
- [ ] T070 Update `docs/ui-ux-enterprise-audit/target-experience-briefs/restore-safety-workflow.md` if the implementation changes the documented target experience; otherwise record an explicit checked target-brief no-change decision in the coverage update.
|
||||
- [x] T062 Add or update Filament feature tests for blocked wizard guidance.
|
||||
- [x] T063 Add or update Filament feature tests for stale preview guidance.
|
||||
- [x] T064 Add or update Filament feature tests for ready-for-confirmation guidance.
|
||||
- [x] T065 Add or update Filament feature tests for read-only inspection behavior.
|
||||
- [x] T066 Add or update Filament feature tests for persisted RestoreRun view guidance.
|
||||
- [x] T067 Run browser smoke for blocked, stale, ready, and read-only Restore guidance states if visible UI changed.
|
||||
- [x] T068 Verify no visible copy implies automatic Restore execution.
|
||||
- [x] T069 Update durable UI/productization coverage registry artifacts under `docs/ui-ux-enterprise-audit/` for the changed RestoreRun create/view surfaces.
|
||||
- [x] T070 Update `docs/ui-ux-enterprise-audit/target-experience-briefs/restore-safety-workflow.md` if the implementation changes the documented target experience; otherwise record an explicit checked target-brief no-change decision in the coverage update.
|
||||
|
||||
## Phase 9: Regression and Safety Validation
|
||||
|
||||
- [ ] T071 Run focused unit tests for `Support/RestoreReadinessResolution`.
|
||||
- [ ] T072 Run focused Restore feature tests.
|
||||
- [ ] T073 Run focused Filament RestoreRunResource tests.
|
||||
- [ ] T074 Run the full platform test suite with `cd apps/platform && ./vendor/bin/sail artisan test` when feasible.
|
||||
- [ ] T075 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`.
|
||||
- [ ] T076 Run `git diff --check`.
|
||||
- [ ] T077 Verify no database migration was added.
|
||||
- [ ] T078 Verify no new environment variables, queues, scheduled commands, storage volumes, or global assets were added.
|
||||
- [ ] T079 Verify no review-publication resolution models/tables are referenced by Restore readiness code.
|
||||
- [ ] T080 Verify no generic adapter registry or action-resolution framework was introduced.
|
||||
- [ ] T081 Verify no Restore readiness code calls Microsoft Graph directly.
|
||||
- [ ] T082 Verify no Restore execution path bypasses existing final confirmation and safety gates.
|
||||
- [ ] T083 Verify `RestoreRunResource` global search behavior still satisfies the Filament v5 View/Edit-page rule or is explicitly disabled.
|
||||
- [ ] T084 Verify Restore readiness display stays within the spec query budget for default Restore create/view renders, or document the measured exception and update the spec before delivery.
|
||||
- [ ] T085 Verify mutating preparation actions preserve/reuse existing audit or operation evidence behavior; document why no audit event is required for any non-mutating path.
|
||||
- [ ] T086 Verify the provider seam remains platform-core presentation over existing RestoreRun/BackupSet/RestoreSafetyResolver/OperationRun data and does not leak provider-specific semantics into platform-core contracts.
|
||||
- [ ] T087 Verify OperationRun-linked UI reuses the central OperationRun Start UX Contract, remains deep-link/inspect-only, and does not queue DB notifications from readiness guidance.
|
||||
- [ ] T088 Verify status-like readiness cues use BadgeCatalog/BadgeRenderer or another central shared status path instead of page-local badge styling.
|
||||
- [x] T071 Run focused unit tests for `Support/RestoreReadinessResolution`.
|
||||
- [x] T072 Run focused Restore feature tests.
|
||||
- [x] T073 Run focused Filament RestoreRunResource tests.
|
||||
- [ ] T074 Run the full platform test suite with `cd apps/platform && ./vendor/bin/sail artisan test` when feasible. Not run in this pass; focused PHP and browser suites were used due repo-wide runtime.
|
||||
- [x] T075 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`.
|
||||
- [x] T076 Run `git diff --check`.
|
||||
- [x] T077 Verify no database migration was added.
|
||||
- [x] T078 Verify no new environment variables, queues, scheduled commands, storage volumes, or global assets were added.
|
||||
- [x] T079 Verify no review-publication resolution models/tables are referenced by Restore readiness code.
|
||||
- [x] T080 Verify no generic adapter registry or action-resolution framework was introduced.
|
||||
- [x] T081 Verify no Restore readiness code calls Microsoft Graph directly.
|
||||
- [x] T082 Verify no Restore execution path bypasses existing final confirmation and safety gates.
|
||||
- [x] T083 Verify `RestoreRunResource` global search behavior still satisfies the Filament v5 View/Edit-page rule or is explicitly disabled.
|
||||
- [x] T084 Verify Restore readiness display stays within the spec query budget for default Restore create/view renders, or document the measured exception and update the spec before delivery.
|
||||
- [x] T085 Verify mutating preparation actions preserve/reuse existing audit or operation evidence behavior; document why no audit event is required for any non-mutating path.
|
||||
- [x] T086 Verify the provider seam remains platform-core presentation over existing RestoreRun/BackupSet/RestoreSafetyResolver/OperationRun data and does not leak provider-specific semantics into platform-core contracts.
|
||||
- [x] T087 Verify OperationRun-linked UI reuses the central OperationRun Start UX Contract, remains deep-link/inspect-only, and does not queue DB notifications from readiness guidance.
|
||||
- [x] T088 Verify status-like readiness cues use BadgeCatalog/BadgeRenderer or another central shared status path instead of page-local badge styling.
|
||||
|
||||
## Phase 10: Delivery Notes
|
||||
|
||||
- [ ] T089 Document implementation validation results in the final response, including tests run and any skipped browser checks.
|
||||
- [ ] T090 State Livewire v4.0+ compliance in the final implementation response.
|
||||
- [ ] T091 State provider registration impact in the final implementation response.
|
||||
- [ ] T092 State global-search status for RestoreRunResource in the final implementation response.
|
||||
- [ ] T093 State destructive-action/confirmation/authorization handling in the final implementation response.
|
||||
- [ ] T094 State asset strategy and whether `filament:assets` is needed in the final implementation response.
|
||||
- [ ] T095 State the testing plan/results in the final implementation response.
|
||||
- [x] T089 Document implementation validation results in the final response, including tests run and any skipped browser checks.
|
||||
- [x] T090 State Livewire v4.0+ compliance in the final implementation response.
|
||||
- [x] T091 State provider registration impact in the final implementation response.
|
||||
- [x] T092 State global-search status for RestoreRunResource in the final implementation response.
|
||||
- [x] T093 State destructive-action/confirmation/authorization handling in the final implementation response.
|
||||
- [x] T094 State asset strategy and whether `filament:assets` is needed in the final implementation response.
|
||||
- [x] T095 State the testing plan/results in the final implementation response.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
||||
556
specs/browser-productization-bug-audit/browser-bug-report.md
Normal file
@ -0,0 +1,556 @@
|
||||
# Browser Productization Bug Audit
|
||||
|
||||
## Audit Metadata
|
||||
|
||||
* Branch: `390-restore-readiness-resolution-adapter-v1`
|
||||
* Commit SHA: `920f726acefc1a3fa66fcc3bd326225e75f2b839`
|
||||
* Date/time: 2026-06-20 09:30 Europe/Berlin
|
||||
* Browser method used: Playwright
|
||||
* Auth/session notes: Admin `/admin` session was authenticated as Ahmed Darrazi. `/system/*` redirected to `/system/login`; system panel was not authenticated.
|
||||
* Test environment / workspace / environment names used: workspace `wp` (`id=3`), environment `YPTW2` (`id=4`, route key `b0091e5d-944f-4a34-bcd9-12cbfb7b75cf`).
|
||||
* Whether repo was dirty before audit: No. Initial `git status --short` returned no entries before audit artifacts were written.
|
||||
* Initial `git status --short`:
|
||||
|
||||
```text
|
||||
|
||||
```
|
||||
|
||||
* Final `git status --short`:
|
||||
|
||||
```text
|
||||
M apps/platform/app/Filament/Resources/RestoreRunResource.php
|
||||
M apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php
|
||||
M apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunDetailPresenter.php
|
||||
M apps/platform/resources/views/filament/forms/components/restore-run-safety-decision.blade.php
|
||||
M apps/platform/resources/views/filament/infolists/entries/restore-results.blade.php
|
||||
M apps/platform/tests/Browser/Spec333RestoreCreateUxFinalProductizationSmokeTest.php
|
||||
M apps/platform/tests/Browser/Spec335RestoreRunDetailProductizationSmokeTest.php
|
||||
M docs/ui-ux-enterprise-audit/design-coverage-matrix.md
|
||||
M docs/ui-ux-enterprise-audit/page-reports/ui-014-restore-runs.md
|
||||
M docs/ui-ux-enterprise-audit/route-inventory.md
|
||||
M docs/ui-ux-enterprise-audit/target-experience-briefs/restore-safety-workflow.md
|
||||
M docs/ui-ux-enterprise-audit/unresolved-pages.md
|
||||
?? apps/platform/app/Support/RestoreReadinessResolution/
|
||||
?? apps/platform/tests/Feature/Filament/Spec390RestoreReadinessGuidanceTest.php
|
||||
?? apps/platform/tests/Unit/Support/RestoreReadinessResolution/
|
||||
?? specs/390-restore-readiness-resolution-adapter-v1/artifacts/
|
||||
?? specs/390-restore-readiness-resolution-adapter-v1/contracts/
|
||||
?? specs/browser-productization-bug-audit/
|
||||
```
|
||||
|
||||
* Whether any files were modified: Yes. Audit screenshots/logs/report were written under `specs/browser-productization-bug-audit/`. The final worktree also contained non-audit RestoreRun/docs/spec changes that appeared during the audit and were not edited by this audit.
|
||||
* Confirmation that only allowed report/screenshot/log files were modified: No. I only intentionally wrote allowed audit files, but the final worktree contains non-allowed modified/untracked files outside the audit directory.
|
||||
* Tool/browser limitations: Integrated Browser connector failed during bootstrap with missing `sandboxPolicy` metadata, so Playwright was used per fallback rule. Playwright route-sweep output was truncated by tool output limits, so screenshots, console logs, network logs, DOM snapshots, and read-only DB/source checks are the evidence basis. Local config has `app.env=local`, `app.debug=true`, and `app.name=Laravel`, so Debugbar and debug error pages were visible in this audit environment.
|
||||
|
||||
## Executive Summary
|
||||
|
||||
* Total bugs found: 10
|
||||
* P0/P1/P2/P3 counts: P0=0, P1=6, P2=3, P3=1
|
||||
* Top 10 issues to fix first:
|
||||
1. `Operations` workspace hub times out and exposes a Laravel debug page.
|
||||
2. Primary `Open evidence basis` CTA points to superseded partial Evidence #30 while active complete Evidence #34 exists.
|
||||
3. Customer Review Workspace also anchors to stale/superseded Evidence #30.
|
||||
4. Download-with-limitations links are visible while output is PII-bearing, incomplete, and not customer-ready.
|
||||
5. Required Permissions page shows `Present 0` / no configured permissions despite 15 granted permission rows.
|
||||
6. Provider health is shown as `Healthy` while the same page says verification is stale and action required.
|
||||
7. Environment dashboard `Open customer workspace` CTA opens a Review Pack detail page.
|
||||
8. System login is branded `Laravel` and exposes Debugbar in local audit.
|
||||
9. Debugbar/source links and Vite client failures pollute the browser/runtime signal.
|
||||
10. Environment page title lacks a separator: `YPTW2Action needed - TenantPilot`.
|
||||
* Merge/customer-readiness recommendation: Not customer-ready. The Operations 500, evidence-anchor drift, provider readiness contradictions, and download-with-limitations affordances should block productization until fixed and re-smoked in browser.
|
||||
|
||||
## Route Coverage
|
||||
|
||||
| Area | Route | Page name | Status | Screenshot path | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| Admin | `/admin` | Workspace overview | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-admin-dashboard.png` | Redirected to `/admin/workspaces/3/overview`. |
|
||||
| Workspace | `/admin/workspaces/3/overview` | Workspace overview | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-workspace-overview.png` | Priority queue and recent ops visible. |
|
||||
| Workspace | `/admin/choose-workspace?choose=1` | Choose workspace | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-choose-workspace.png` | Workspace switcher reachable. |
|
||||
| Workspace | `/admin/choose-environment` | Choose environment | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-choose-environment.png` | Environment switcher reachable. |
|
||||
| Environment | `/admin/workspaces/3/environments` | Managed environments | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-managed-environments.png` | Environment list captured. |
|
||||
| Environment | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf` | Environment dashboard | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-environment-dashboard.png` | Multiple readiness contradictions. |
|
||||
| Environment | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/required-permissions` | Required permissions | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-required-permissions.png` | Permission counts contradict DB rows. |
|
||||
| Inventory | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/inventory` | Inventory items | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-inventory-items.png` | Route loaded. |
|
||||
| Inventory | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/policies` | Policies | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-policies.png` | Route loaded. |
|
||||
| Inventory | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/policy-versions` | Policy versions | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-policy-versions.png` | Route loaded. |
|
||||
| Inventory | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/inventory/inventory-coverage` | Inventory coverage | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-inventory-coverage.png` | Route loaded. |
|
||||
| Reporting | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/environment-reviews` | Environment reviews | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-environment-reviews.png` | Route loaded. |
|
||||
| Reporting | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/stored-reports` | Stored reports | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-stored-reports.png` | Route loaded. |
|
||||
| Reporting | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/review-packs` | Review packs | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-review-packs.png` | Route loaded. |
|
||||
| Reporting | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/review-packs/32` | View Review Pack | checked | `specs/browser-productization-bug-audit/screenshots/BUG-003-internal-pack-download-enabled-while-not-usable.png` | Download action visible despite limitations. |
|
||||
| Governance | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/findings` | Findings | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-findings.png` | First rows and first detail inspected. |
|
||||
| Governance | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/findings/254` | View Findings | checked | not captured separately | Detail showed technical IDs as admin-only evidence. |
|
||||
| Governance | `/admin/baseline-profiles` | Baseline profiles | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-baseline-profiles.png` | Route loaded. |
|
||||
| Governance | `/admin/baseline-snapshots` | Baseline snapshots | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-baseline-snapshots.png` | Route loaded. |
|
||||
| Governance | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/baseline-compare` | Baseline compare | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-baseline-compare.png` | Route loaded. |
|
||||
| Governance | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/evidence` | Evidence snapshots | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-evidence-snapshots.png` | Route loaded. |
|
||||
| Governance | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/evidence/30` | View Evidence Snapshot | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-evidence-snapshot-detail.png` | Superseded partial evidence. |
|
||||
| Governance | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/finding-exceptions` | Risk exceptions | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-risk-exceptions.png` | Route loaded. |
|
||||
| Backup/Restore | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/backup-schedules` | Backup schedules | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-backup-schedules.png` | Route loaded. |
|
||||
| Backup/Restore | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/backup-sets` | Backup sets | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-backup-sets.png` | First three rows inspected; destructive actions verified read-only in source. |
|
||||
| Backup/Restore | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/restore-runs` | Restore runs | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-restore-runs.png` | Preview row inspected. |
|
||||
| Backup/Restore | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/restore-runs/create` | Create Restore Run | checked | not captured separately | Wizard inspected without submitting. |
|
||||
| Directory | `/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/entra-groups` | Entra groups | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-entra-groups.png` | Route loaded. |
|
||||
| Workspace-wide | `/admin/finding-exceptions/queue?environment_id=4` | Finding exceptions queue | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-finding-exceptions-queue.png` | Route loaded. |
|
||||
| Workspace-wide | `/admin/reviews?environment_id=4` | Reviews | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-reviews-workspace-filtered.png` | Route loaded. |
|
||||
| Workspace-wide | `/admin/governance/inbox?environment_id=4` | Governance inbox | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-governance-inbox.png` | Route loaded. |
|
||||
| Workspace-wide | `/admin/governance/decisions?environment_id=4` | Decision register | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-decision-register.png` | Route loaded. |
|
||||
| Workspace-wide | `/admin/workspaces/3/operations?environment_id=4` | Operations | blocked | `specs/browser-productization-bug-audit/screenshots/BUG-001-operations-500-debug-page.png` | 500 / timeout / debug page. |
|
||||
| Workspace-wide | `/admin/alerts?environment_id=4` | Alerts | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-alerts.png` | Route loaded. |
|
||||
| Workspace-wide | `/admin/evidence/overview?environment_id=4` | Evidence overview | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-evidence-overview.png` | Route loaded. |
|
||||
| Workspace-wide | `/admin/audit-log?environment_id=4` | Audit log | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-audit-log.png` | Route loaded. |
|
||||
| Workspace-wide | `/admin/reviews/workspace?environment_id=4` | Customer Review Workspace | checked | `specs/browser-productization-bug-audit/screenshots/BUG-006-customer-review-download-and-stale-evidence.png` | Stale evidence and download-with-limitations. |
|
||||
| Workspace admin | `/admin/workspaces` | Workspaces | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-workspaces.png` | Route loaded. |
|
||||
| Workspace admin | `/admin/provider-connections?environment_id=4` | Provider connections | checked | `specs/browser-productization-bug-audit/screenshots/BUG-007-provider-health-healthy-while-verification-stale.png` | Provider verification contradiction. |
|
||||
| Workspace admin | `/admin/settings/workspace` | Workspace settings | checked | `specs/browser-productization-bug-audit/screenshots/ROUTE-workspace-settings.png` | Route loaded. |
|
||||
| System | `/system` | System dashboard | blocked | `specs/browser-productization-bug-audit/screenshots/BUG-008-system-login-default-laravel-branding.png` | Redirected to `/system/login`. |
|
||||
| System | `/system/directory/tenants` | System tenants | blocked | `specs/browser-productization-bug-audit/screenshots/ROUTE-system-tenants.png` | System auth required. |
|
||||
| System | `/system/directory/workspaces` | System workspaces | blocked | `specs/browser-productization-bug-audit/screenshots/ROUTE-system-workspaces.png` | System auth required. |
|
||||
| System | `/system/ops/runs` | System ops runs | blocked | `specs/browser-productization-bug-audit/screenshots/ROUTE-system-ops-runs.png` | System auth required. |
|
||||
| System | `/system/ops/failures` | System ops failures | blocked | `specs/browser-productization-bug-audit/screenshots/ROUTE-system-ops-failures.png` | System auth required. |
|
||||
| System | `/system/ops/stuck` | System ops stuck | blocked | `specs/browser-productization-bug-audit/screenshots/ROUTE-system-ops-stuck.png` | System auth required. |
|
||||
| System | `/system/ops/controls` | System ops controls | blocked | `specs/browser-productization-bug-audit/screenshots/ROUTE-system-ops-controls.png` | System auth required. |
|
||||
| System | `/system/ops/runbooks` | System ops runbooks | blocked | `specs/browser-productization-bug-audit/screenshots/ROUTE-system-ops-runbooks.png` | System auth required. |
|
||||
| System | `/system/security/access-logs` | System access logs | blocked | `specs/browser-productization-bug-audit/screenshots/ROUTE-system-access-logs.png` | System auth required. |
|
||||
|
||||
## Bugs
|
||||
|
||||
### BUG-001 — Operations index times out and exposes debug page
|
||||
|
||||
Severity: P1
|
||||
Area: Workspace-wide / Operations
|
||||
Route: `http://localhost/admin/workspaces/3/operations?environment_id=4`
|
||||
Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-001-operations-500-debug-page.png`
|
||||
Console/network errors: yes. Network log includes `GET http://localhost/admin/workspaces/3/operations?environment_id=4 => [500] Internal Server Error`; console includes Filament/Alpine reference errors and Vite client failures.
|
||||
|
||||
Actual:
|
||||
Opening the Operations route takes roughly 40 seconds and lands on a Laravel debug/error page. The browser reports 35 console errors and 33 warnings. Laravel Boost `last_error` shows `Maximum execution time of 30 seconds exceeded` at `Illuminate\Database\Eloquent\Concerns\HasAttributes.php:1577`.
|
||||
|
||||
Expected:
|
||||
The Operations hub should render a bounded, paginated operations list or a controlled error state. Operators should never see a raw Laravel debug page or stack trace in a customer-ready environment.
|
||||
|
||||
Steps to reproduce:
|
||||
|
||||
1. Authenticate in `/admin`.
|
||||
2. Open `http://localhost/admin/workspaces/3/operations?environment_id=4`.
|
||||
3. Wait for the request to complete.
|
||||
|
||||
Evidence:
|
||||
|
||||
* Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-001-operations-500-debug-page.png`
|
||||
* Console: `specs/browser-productization-bug-audit/logs/console-warnings-final.txt`
|
||||
* Network: `specs/browser-productization-bug-audit/logs/network-requests-final.txt`
|
||||
* Read-only log: local error at `2026-06-20 09:19:07`, max execution time exceeded.
|
||||
|
||||
Likely source:
|
||||
|
||||
* `apps/platform/app/Filament/Pages/Monitoring/Operations.php:610`
|
||||
* `apps/platform/app/Filament/Resources/OperationRunResource.php:134`
|
||||
|
||||
Suggested fix:
|
||||
Profile the Operations index render path with the environment filter applied. Bound expensive model attribute/accessor work in table columns/actions, avoid per-row heavy presenters, and replace local debug exposure with a controlled Filament error/empty state.
|
||||
|
||||
Product impact:
|
||||
Operations is a core operator workflow and the main drilldown for dashboard follow-up. A 500 blocks incident triage and can expose implementation details if debug mode leaks outside local.
|
||||
|
||||
### BUG-002 — Primary evidence CTA points to superseded evidence
|
||||
|
||||
Severity: P1
|
||||
Area: Environment dashboard / Evidence
|
||||
Route: `http://localhost/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf`
|
||||
Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-004-primary-evidence-cta-points-to-superseded-snapshot.png`
|
||||
Console/network errors: no current route error.
|
||||
|
||||
Actual:
|
||||
The hero recommendation says `Evidence basis is incomplete` and the primary CTA `Open evidence basis` links to Evidence Snapshot #30. Read-only DB shows Evidence #30 is `superseded` and `partial`, while Evidence #34 is `active` and `complete`. The same dashboard's readiness proof `Open evidence` action points to #34.
|
||||
|
||||
Expected:
|
||||
A primary evidence CTA should either open the active/current evidence basis or explicitly explain that it is intentionally opening the anchored historical evidence that blocks the released review. It should not silently point to stale/superseded evidence while another proof section points to current evidence.
|
||||
|
||||
Steps to reproduce:
|
||||
|
||||
1. Open the YPTW2 environment dashboard.
|
||||
2. Inspect the primary `Open evidence basis` CTA.
|
||||
3. Compare it with the `Readiness proof -> Evidence coverage -> Open evidence` action.
|
||||
|
||||
Evidence:
|
||||
|
||||
* Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-004-primary-evidence-cta-points-to-superseded-snapshot.png`
|
||||
* Read-only DB: Evidence #34 = `active/complete`, generated `2026-06-20 08:36:00`; Evidence #30 = `superseded/partial`, generated `2026-06-14 23:47:48`.
|
||||
|
||||
Likely source:
|
||||
|
||||
* `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php:327`
|
||||
* `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php:1568`
|
||||
* `apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php:441`
|
||||
|
||||
Suggested fix:
|
||||
Make the CTA source explicit: use latest active evidence for environment readiness, or label anchored released-review evidence as historical and show the current-evidence alternative next to it.
|
||||
|
||||
Product impact:
|
||||
Operators are sent to the wrong evidence object for the primary decision. That can cause stale governance conclusions and unnecessary remediation work.
|
||||
|
||||
### BUG-003 — Customer Review Workspace anchors to stale evidence
|
||||
|
||||
Severity: P1
|
||||
Area: Customer Review Workspace / Evidence
|
||||
Route: `http://localhost/admin/reviews/workspace?environment_id=4`
|
||||
Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-006-customer-review-download-and-stale-evidence.png`
|
||||
Console/network errors: no current route error.
|
||||
|
||||
Actual:
|
||||
Customer Review Workspace shows `Evidence snapshot Available Generated Jun 14, 2026 23:47` and links `View evidence snapshot` to Evidence #30. That evidence is superseded/partial, while the environment has active/complete Evidence #34 generated on Jun 20, 2026.
|
||||
|
||||
Expected:
|
||||
Customer-facing review workspace should clearly distinguish released-review anchored evidence from current environment evidence. If it intentionally uses historical evidence, it should say "released-review evidence" and warn that newer complete evidence exists.
|
||||
|
||||
Steps to reproduce:
|
||||
|
||||
1. Open `http://localhost/admin/reviews/workspace?environment_id=4`.
|
||||
2. Scroll to Supporting Reference.
|
||||
3. Inspect Evidence path and `View evidence snapshot`.
|
||||
|
||||
Evidence:
|
||||
|
||||
* Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-006-customer-review-download-and-stale-evidence.png`
|
||||
* Read-only DB evidence comparison from BUG-002.
|
||||
|
||||
Likely source:
|
||||
|
||||
* `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php:745`
|
||||
* `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php:768`
|
||||
|
||||
Suggested fix:
|
||||
Show both anchored release evidence and current evidence when they differ; update labels and warnings so customer review operators understand whether they are looking at historical or current proof.
|
||||
|
||||
Product impact:
|
||||
Customer-facing review workflows can appear stale or misleading even after evidence has been regenerated successfully.
|
||||
|
||||
### BUG-004 — Download-with-limitations is enabled for PII-bearing, not-ready output
|
||||
|
||||
Severity: P1
|
||||
Area: Customer Review / Review Packs
|
||||
Route: `http://localhost/admin/reviews/workspace?environment_id=4` and `http://localhost/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/review-packs/32`
|
||||
Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-003-internal-pack-download-enabled-while-not-usable.png`
|
||||
Console/network errors: no current route error.
|
||||
|
||||
Actual:
|
||||
Review Pack #32 says `Internal only`, `Result trust Not usable yet`, `Coverage Partially complete`, and `This package includes internal or PII-bearing detail`. Customer Review Workspace says `Requires review`, `PII Contains PII`, and `Customer sharing still depends on readiness blockers`, but still renders a direct `Download review pack with limitations` signed URL.
|
||||
|
||||
Expected:
|
||||
When output is not customer-ready and contains PII, direct download should be gated behind an explicit confirmation or moved behind a review/detail action. The UI should avoid presenting a direct download as a normal supporting action.
|
||||
|
||||
Steps to reproduce:
|
||||
|
||||
1. Open Customer Review Workspace with `environment_id=4`.
|
||||
2. Inspect `Supporting actions`.
|
||||
3. Open Review Pack #32 and inspect header actions and output guidance.
|
||||
|
||||
Evidence:
|
||||
|
||||
* Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-003-internal-pack-download-enabled-while-not-usable.png`
|
||||
* Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-006-customer-review-download-and-stale-evidence.png`
|
||||
|
||||
Likely source:
|
||||
|
||||
* `apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php:400`
|
||||
* `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php:2659`
|
||||
|
||||
Suggested fix:
|
||||
Require an explicit confirmation for download-with-limitations, make the primary path open review/redaction checks, and only enable direct customer-safe downloads when the output state is customer-safe ready.
|
||||
|
||||
Product impact:
|
||||
An MSP/operator could download and share an internal or PII-bearing package despite on-page warnings that it is not ready. This is a customer disclosure risk.
|
||||
|
||||
### BUG-005 — Required permissions detail shows zero present despite granted rows
|
||||
|
||||
Severity: P1
|
||||
Area: Provider permissions / Required permissions
|
||||
Route: `http://localhost/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf/required-permissions`
|
||||
Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-005-required-permissions-zero-present-despite-grants.png`
|
||||
Console/network errors: no current route error.
|
||||
|
||||
Actual:
|
||||
The page summary shows `Missing (app) 0`, `Missing (delegated) 0`, `Present 0`, `Errors 0`, says `No required permissions are configured yet`, and still recommends `Run provider verification` / `Open admin consent`. Read-only DB shows 15 `managed_environment_permissions` rows for environment 4, all `status=granted`, last checked `2026-05-14 20:42:51`.
|
||||
|
||||
Expected:
|
||||
The detail page should show the 15 granted permissions as present but stale, and the next action should be refresh/verification, not imply no configured permissions or missing consent.
|
||||
|
||||
Steps to reproduce:
|
||||
|
||||
1. Open Required permissions for YPTW2.
|
||||
2. Compare summary counts and issue text.
|
||||
3. Query `managed_environment_permissions` for environment 4 read-only.
|
||||
|
||||
Evidence:
|
||||
|
||||
* Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-005-required-permissions-zero-present-despite-grants.png`
|
||||
* DB: `SELECT status, COUNT(*) ...` returned 15 granted rows.
|
||||
|
||||
Likely source:
|
||||
|
||||
* `apps/platform/app/Services/Intune/ManagedEnvironmentRequiredPermissionsViewModelBuilder.php:249`
|
||||
* `apps/platform/app/Filament/Pages/EnvironmentRequiredPermissions.php:125`
|
||||
|
||||
Suggested fix:
|
||||
Ensure stored granted rows are included in the default view/counts when evidence is stale. Default filter can still focus on missing permissions, but the summary must not collapse present granted rows to zero or "not configured".
|
||||
|
||||
Product impact:
|
||||
Operators cannot tell whether permissions are absent, stale, or complete. This can drive unnecessary admin-consent workflows and undermine trust in readiness gates.
|
||||
|
||||
### BUG-006 — Provider health is marked Healthy while verification is stale
|
||||
|
||||
Severity: P1
|
||||
Area: Provider connections / Environment readiness
|
||||
Route: `http://localhost/admin/provider-connections?environment_id=4`
|
||||
Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-007-provider-health-healthy-while-verification-stale.png`
|
||||
Console/network errors: no current route error.
|
||||
|
||||
Actual:
|
||||
Provider Connections top guidance says `Action required / Provider verification required` because stored verification evidence is stale. The table row simultaneously shows `Verification Healthy` and `Provider capability Unknown`. The environment dashboard also shows `Provider permissions Needs attention` while `Provider Health Healthy` repeats that the verification snapshot is stale.
|
||||
|
||||
Expected:
|
||||
Provider health should not be green/healthy when the verification basis is stale and capabilities are unknown. It should read `Needs attention`, `Stale`, or `Verification required` consistently across dashboard and list.
|
||||
|
||||
Steps to reproduce:
|
||||
|
||||
1. Open Provider Connections with `environment_id=4`.
|
||||
2. Compare the top action-required banner with the table `Verification` and `Provider capability` columns.
|
||||
3. Open the environment dashboard and compare `Provider permissions` with `Provider Health`.
|
||||
|
||||
Evidence:
|
||||
|
||||
* Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-007-provider-health-healthy-while-verification-stale.png`
|
||||
* Screenshot: `specs/browser-productization-bug-audit/screenshots/ROUTE-environment-dashboard.png`
|
||||
|
||||
Likely source:
|
||||
|
||||
* `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php:1708`
|
||||
* `apps/platform/app/Filament/Resources/ProviderConnectionResource.php` (not line-investigated)
|
||||
|
||||
Suggested fix:
|
||||
Make provider health tone/status consume permission freshness and capability uncertainty. A stale verification snapshot should downgrade health even when the last stored grant set was complete.
|
||||
|
||||
Product impact:
|
||||
Provider readiness gates can look green while write/read capability checks are unknown or stale, causing unsafe operational decisions.
|
||||
|
||||
### BUG-007 — Open customer workspace CTA opens Review Pack detail
|
||||
|
||||
Severity: P2
|
||||
Area: Environment dashboard / Review output
|
||||
Route: `http://localhost/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf`
|
||||
Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-002-customer-workspace-cta-opens-review-pack.png`
|
||||
Console/network errors: no.
|
||||
|
||||
Actual:
|
||||
The dashboard link labelled `Open customer workspace` navigates to `.../review-packs/32`, whose heading is `View Review Pack`. It does not open Customer Review Workspace.
|
||||
|
||||
Expected:
|
||||
A link labelled `Open customer workspace` should open `Customer Review Workspace`, or the label should say `Open review pack`.
|
||||
|
||||
Steps to reproduce:
|
||||
|
||||
1. Open the YPTW2 environment dashboard.
|
||||
2. Click `Open customer workspace`.
|
||||
3. Observe the destination heading.
|
||||
|
||||
Evidence:
|
||||
|
||||
* Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-002-customer-workspace-cta-opens-review-pack.png`
|
||||
|
||||
Likely source:
|
||||
|
||||
* `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php:327`
|
||||
* `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php:2038`
|
||||
|
||||
Suggested fix:
|
||||
Align label and destination: use CustomerReviewWorkspace URL for this label, or change the CTA text when the action opens a review pack artifact.
|
||||
|
||||
Product impact:
|
||||
Operators lose orientation between customer workspace, review detail, and review pack artifact. This increases the chance of sharing or reviewing the wrong surface.
|
||||
|
||||
### BUG-008 — System login uses default Laravel branding
|
||||
|
||||
Severity: P2
|
||||
Area: System / Authentication
|
||||
Route: `http://localhost/system`
|
||||
Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-008-system-login-default-laravel-branding.png`
|
||||
Console/network errors: no current route error.
|
||||
|
||||
Actual:
|
||||
The system login page title is `Login - Laravel`, the card brand text is `Laravel`, and a Debugbar icon is visible in the lower-left corner. Read-only config shows `app.name=Laravel`.
|
||||
|
||||
Expected:
|
||||
System login should be TenantPilot-branded and make it clear this is the system/admin panel. Debugbar should not be visible outside local development and should not be part of customer-ready screenshots.
|
||||
|
||||
Steps to reproduce:
|
||||
|
||||
1. Open `http://localhost/system`.
|
||||
2. Observe redirect to `/system/login`.
|
||||
3. Inspect title and login card branding.
|
||||
|
||||
Evidence:
|
||||
|
||||
* Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-008-system-login-default-laravel-branding.png`
|
||||
* Config read-only: `app.name=Laravel`, `app.debug=true`, `app.env=local`.
|
||||
|
||||
Likely source:
|
||||
|
||||
* `config/app.php` / environment `APP_NAME` (not line-investigated)
|
||||
* System panel login branding configuration (not line-investigated)
|
||||
|
||||
Suggested fix:
|
||||
Set application/panel branding to TenantPilot and ensure Debugbar is disabled in staging/production validation environments.
|
||||
|
||||
Product impact:
|
||||
Default framework branding looks unfinished and makes system/admin scope less clear for platform admins.
|
||||
|
||||
### BUG-009 — Debugbar and asset failures pollute browser runtime
|
||||
|
||||
Severity: P2
|
||||
Area: Cross-cutting / Frontend runtime
|
||||
Route: Multiple admin and system routes
|
||||
Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-001-operations-500-debug-page.png`
|
||||
Console/network errors: yes. Console logs include `filamentSchema is not defined`, `filamentSchemaComponent is not defined`, `filamentTable is not defined`, `selectFormComponent is not defined`, and network failures for `http://localhost:5173/@vite/client`.
|
||||
|
||||
Actual:
|
||||
Debugbar links and `phpstorm://open?...` links appear in the DOM on admin/system pages. Network logs include repeated Debugbar requests and Vite client failures. The Operations route exposed a full debug error surface.
|
||||
|
||||
Expected:
|
||||
Browser validation for productization should run with compiled/stable assets and no Debugbar/source-link leakage. If local debug remains enabled, it should be recorded as a tooling limitation and not confused with customer UX.
|
||||
|
||||
Steps to reproduce:
|
||||
|
||||
1. Open several admin routes.
|
||||
2. Inspect console warnings and network requests.
|
||||
3. Inspect DOM links for `phpstorm://` and `_debugbar`.
|
||||
|
||||
Evidence:
|
||||
|
||||
* Console: `specs/browser-productization-bug-audit/logs/console-warnings-final.txt`
|
||||
* Network: `specs/browser-productization-bug-audit/logs/network-requests-final.txt`
|
||||
* Screenshot: `specs/browser-productization-bug-audit/screenshots/BUG-001-operations-500-debug-page.png`
|
||||
|
||||
Likely source:
|
||||
|
||||
* Local environment config/assets; exact source not investigated.
|
||||
|
||||
Suggested fix:
|
||||
Run audit/staging with `APP_DEBUG=false`, Debugbar disabled, and built Filament/Vite assets. Add a smoke check that fails on missing Filament JS globals or Vite client load failures.
|
||||
|
||||
Product impact:
|
||||
JS runtime failures can make filters/actions unreliable and debug surfaces can obscure real customer UX issues.
|
||||
|
||||
### BUG-010 — Environment page title has missing separator
|
||||
|
||||
Severity: P3
|
||||
Area: Environment dashboard / Browser metadata
|
||||
Route: `http://localhost/admin/workspaces/3/environments/b0091e5d-944f-4a34-bcd9-12cbfb7b75cf`
|
||||
Screenshot: `specs/browser-productization-bug-audit/screenshots/ROUTE-environment-dashboard.png`
|
||||
Console/network errors: no.
|
||||
|
||||
Actual:
|
||||
The browser title is `YPTW2Action needed - TenantPilot` with no space or separator between the environment name and status.
|
||||
|
||||
Expected:
|
||||
The page title should read something like `YPTW2 - Action needed - TenantPilot`.
|
||||
|
||||
Steps to reproduce:
|
||||
|
||||
1. Open the YPTW2 environment dashboard.
|
||||
2. Inspect browser title.
|
||||
|
||||
Evidence:
|
||||
|
||||
* Screenshot: `specs/browser-productization-bug-audit/screenshots/ROUTE-environment-dashboard.png`
|
||||
* Playwright page title: `YPTW2Action needed - TenantPilot`
|
||||
|
||||
Likely source:
|
||||
|
||||
* Environment dashboard page title composition, exact file not investigated.
|
||||
|
||||
Suggested fix:
|
||||
Add a separator between environment name and status in title composition.
|
||||
|
||||
Product impact:
|
||||
Minor polish issue, but it makes browser tabs/history look unprofessional.
|
||||
|
||||
## Cross-cutting Patterns
|
||||
|
||||
* Misleading readiness labels: Provider permissions, provider health, evidence basis, and customer output readiness use inconsistent states for the same underlying data.
|
||||
* Stale data displayed as current: Superseded Evidence #30 remains the primary/customer evidence path even though active Evidence #34 exists.
|
||||
* Workspace/environment scope confusion: Environment dashboard mixes Customer Workspace, Review Pack, Review Detail, and Evidence Detail labels without clearly naming the destination.
|
||||
* Customer-facing report/disclosure problems: Direct download links are visible while the page says the package contains PII and requires review.
|
||||
* Debugbar/stack trace leakage: Local debug settings exposed Debugbar, `phpstorm://` source links, and a Laravel error page during the audit.
|
||||
* Broken redirects/back/navigation flows: Operations links from dashboard, backup sets, customer workspace, findings, and notifications route to a hub that currently 500s.
|
||||
* Inconsistent badges/tones: `Healthy` appears next to stale/unknown provider capability states.
|
||||
* Raw IDs/GUIDs in UI: Admin finding detail shows provider GUIDs and fingerprints in technical sections; acceptable for admin detail, but these must remain hidden from customer output.
|
||||
|
||||
## Suggested Follow-up Specs
|
||||
|
||||
* Proposed spec number placeholder: `SPEC-OPS-001`
|
||||
Title: Stabilize operations hub rendering and frontend runtime.
|
||||
Bugs covered: BUG-001, BUG-009.
|
||||
Why this should be one spec: Operations is the common drilldown from many surfaces, and the JS/runtime/debug failure affects table actions and filters.
|
||||
Acceptance criteria summary: Operations index renders under environment filters in under 3 seconds; no debug page; no Filament JS missing-global console errors; route has controlled empty/error states.
|
||||
|
||||
* Proposed spec number placeholder: `SPEC-EVIDENCE-001`
|
||||
Title: Reconcile current vs anchored evidence across dashboards and customer review.
|
||||
Bugs covered: BUG-002, BUG-003.
|
||||
Why this should be one spec: Both bugs are evidence-anchor selection and labeling issues across environment and customer-review surfaces.
|
||||
Acceptance criteria summary: Active evidence and released-review anchored evidence are separately labelled; primary CTAs point to the intended evidence; stale/superseded evidence is never silently presented as current.
|
||||
|
||||
* Proposed spec number placeholder: `SPEC-OUTPUT-001`
|
||||
Title: Gate review-pack downloads by customer readiness and PII state.
|
||||
Bugs covered: BUG-004, BUG-007.
|
||||
Why this should be one spec: Download affordances and customer workspace/review-pack navigation are part of the same output handoff model.
|
||||
Acceptance criteria summary: Direct download only for customer-safe-ready output; limitation downloads require confirmation and explicit internal-only language; labels match destinations.
|
||||
|
||||
* Proposed spec number placeholder: `SPEC-PROVIDER-001`
|
||||
Title: Normalize provider permission and health freshness semantics.
|
||||
Bugs covered: BUG-005, BUG-006.
|
||||
Why this should be one spec: Permission counts, stale verification, provider connection health, and capability readiness must share one status taxonomy.
|
||||
Acceptance criteria summary: Granted stale permissions count as present-but-stale; health tone downgrades on stale verification; list, detail, and dashboard states match.
|
||||
|
||||
* Proposed spec number placeholder: `SPEC-SYSTEM-001`
|
||||
Title: Productize system panel login and debug configuration checks.
|
||||
Bugs covered: BUG-008, BUG-009, BUG-010.
|
||||
Why this should be one spec: Branding, debug settings, and metadata polish are cross-panel readiness concerns.
|
||||
Acceptance criteria summary: TenantPilot branding on system login; no Debugbar/source links in staging/productization; browser titles are formatted consistently.
|
||||
|
||||
## Appendix
|
||||
|
||||
* Screenshot index:
|
||||
* 47 `ROUTE-*` screenshots under `specs/browser-productization-bug-audit/screenshots/`
|
||||
* Bug screenshots: `BUG-001-operations-500-debug-page.png`, `BUG-002-customer-workspace-cta-opens-review-pack.png`, `BUG-003-internal-pack-download-enabled-while-not-usable.png`, `BUG-004-primary-evidence-cta-points-to-superseded-snapshot.png`, `BUG-005-required-permissions-zero-present-despite-grants.png`, `BUG-006-customer-review-download-and-stale-evidence.png`, `BUG-007-provider-health-healthy-while-verification-stale.png`, `BUG-008-system-login-default-laravel-branding.png`
|
||||
* Console error index:
|
||||
* `specs/browser-productization-bug-audit/logs/console-warnings.txt`
|
||||
* `specs/browser-productization-bug-audit/logs/console-warnings-final.txt`
|
||||
* Key entries: Filament/Alpine missing globals, Vite client failure, Operations debug-page stack traces.
|
||||
* Network error index:
|
||||
* `specs/browser-productization-bug-audit/logs/network-requests.txt`
|
||||
* `specs/browser-productization-bug-audit/logs/network-requests-final.txt`
|
||||
* Key entries: Operations 500, Vite client connection reset, Debugbar aborted requests, old Microsoft login favicon/SSO probe noise.
|
||||
* Blocked routes:
|
||||
* `/admin/workspaces/3/operations?environment_id=4` blocked by 500/timeout.
|
||||
* `/system/*` routes blocked by system auth; `/system/login` was inspected.
|
||||
* Not reachable routes:
|
||||
* Authenticated system panel internals were not reachable without a system login session.
|
||||
* Dangerous actions intentionally not executed:
|
||||
* Evidence: `Refresh evidence`, `Expire snapshot`.
|
||||
* Provider Connections: `Check connection`, `Inventory sync`, `Compliance snapshot`, `Enable dedicated override`, `Disable connection`.
|
||||
* Required Permissions: `Run provider verification`, external `Open admin consent`.
|
||||
* Review/customer output: `Download review pack with limitations`, `Download internal review pack`, `View internal report`, `Regenerate review pack`, `Acknowledge review`.
|
||||
* Findings: `Triage`, `Assign`, `Resolve`, `Close`, `Request exception`, all bulk variants.
|
||||
* Backup/Restore: archive/restore/force-delete and bulk archive actions, restore wizard submit/execute steps.
|
||||
* Read-only code/db inspections performed:
|
||||
* `git status --short`, `git branch --show-current`, `git rev-parse HEAD`.
|
||||
* Laravel routes via Boost `list_routes`.
|
||||
* Laravel config via Boost `get_config` for `app.name`, `app.env`, `app.debug`, `debugbar.enabled`.
|
||||
* Laravel logs via Boost `last_error` and `read_log_entries`.
|
||||
* Read-only DB queries for `evidence_snapshots`, `managed_environment_permissions`, `operation_runs`, `backup_sets`, `restore_runs`.
|
||||
* Source reads for Operations, EnvironmentDashboardSummaryBuilder, ReviewPackOutputResolutionGuidance, CustomerReviewWorkspace, RequiredPermissions, BackupSetResource.
|
||||
* Tool/browser limitations:
|
||||
* Integrated Browser control was unavailable due missing `sandboxPolicy` metadata, so Playwright was used.
|
||||
* Full route-sweep tool output was truncated; screenshots/logs and targeted DOM evaluations were used for durable evidence.
|
||||
* Local debug mode and Debugbar were enabled, so debug artifacts are recorded as both observed risk and local-environment limitation.
|
||||
1974
specs/browser-productization-bug-audit/logs/console-warnings.txt
Normal file
@ -0,0 +1,4 @@
|
||||
21. [POST] http://localhost/livewire-47b331c2/update => [200] OK
|
||||
22. [GET] http://localhost/_debugbar/open?op=get&id=01KVJ5PESV759A5B14GFX7HKX1 => [200] OK
|
||||
|
||||
Note: 20 static requests not shown, run with "static" option to see them.
|
||||
@ -0,0 +1,3 @@
|
||||
1. [GET] http://localhost/admin/workspaces/3/operations?environment_id=4 => [500] Internal Server Error
|
||||
2. [GET] http://localhost:5173/@vite/client => [FAILED] net::ERR_CONNECTION_RESET
|
||||
3. [GET] http://localhost:5173/@vite/client => [FAILED] net::ERR_CONNECTION_RESET
|
||||
|
After Width: | Height: | Size: 802 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 328 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 409 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 469 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 572 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 328 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 390 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 390 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 802 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 402 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 409 KiB |
|
After Width: | Height: | Size: 435 KiB |
|
After Width: | Height: | Size: 30 KiB |