feat: add restore readiness resolution adapter improvements (#461)
Automated PR created by Codex via Gitea API. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #461
@ -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,
|
||||
|
||||
@ -828,7 +828,8 @@ private function supportingSignals(
|
||||
$overview = is_array($requiredPermissions['overview'] ?? null)
|
||||
? $requiredPermissions['overview']
|
||||
: [];
|
||||
$providerPermissionsReady = $this->providerPermissionsTone($overview) === 'success';
|
||||
$providerPermissionsValue = $this->providerPermissionsValue($overview);
|
||||
$providerPermissionsTone = $this->providerPermissionsTone($overview);
|
||||
$operationCount = (int) ($activeOperationSummary['count'] ?? 0);
|
||||
$operationsAction = $operationCount > 0 && is_string($activeOperationSummary['secondaryActionUrl'] ?? null)
|
||||
? [
|
||||
@ -878,8 +879,8 @@ private function supportingSignals(
|
||||
$this->supportingSignal(
|
||||
key: 'provider_permissions',
|
||||
label: 'Provider permissions',
|
||||
value: $providerPermissionsReady ? 'Ready' : 'Missing',
|
||||
tone: $providerPermissionsReady ? 'success' : 'danger',
|
||||
value: $providerPermissionsValue,
|
||||
tone: $providerPermissionsTone,
|
||||
action: $this->requiredPermissionsAction($tenant, $user, $this->overviewText('action_open_required_permissions')),
|
||||
),
|
||||
$this->supportingSignal(
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -7,11 +7,11 @@
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Services\Intune\ManagedEnvironmentRequiredPermissionsViewModelBuilder;
|
||||
use App\Support\EnvironmentDashboard\EnvironmentDashboardSummaryBuilder;
|
||||
use App\Support\OperationCatalog;
|
||||
use App\Support\OperationRunLinks;
|
||||
use App\Support\OperationRunOutcome;
|
||||
use App\Support\OperationRunStatus;
|
||||
use App\Support\EnvironmentDashboard\EnvironmentDashboardSummaryBuilder;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
use function Pest\Laravel\mock;
|
||||
@ -278,6 +278,41 @@ function mockEnvironmentDashboardSummaryPermissions(array $overview = []): void
|
||||
->and($attentionOperations['Permission posture check']['icon'] ?? null)->toBe('heroicon-m-key');
|
||||
});
|
||||
|
||||
it('keeps stale complete provider permissions out of missing supporting signals', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
mockEnvironmentDashboardSummaryPermissions([
|
||||
'overall' => 'needs_attention',
|
||||
'counts' => [
|
||||
'missing_application' => 0,
|
||||
'missing_delegated' => 0,
|
||||
],
|
||||
'freshness' => [
|
||||
'is_stale' => true,
|
||||
'last_refreshed_at' => now()->subDays(31)->toIso8601String(),
|
||||
],
|
||||
]);
|
||||
|
||||
$summary = app(EnvironmentDashboardSummaryBuilder::class)
|
||||
->build($tenant, $user)
|
||||
->toArray();
|
||||
|
||||
$kpis = collect($summary['kpis'])->keyBy('key');
|
||||
$governanceStatus = collect($summary['governanceStatus'])->keyBy('key');
|
||||
$supportingSignals = collect($summary['supportingSignals'])->keyBy('key');
|
||||
$readinessDimensions = collect($summary['readinessDimensions'])->keyBy('key');
|
||||
|
||||
expect($kpis['missing_permissions']['value'] ?? null)->toBe(0)
|
||||
->and($governanceStatus['provider_permissions']['value'] ?? null)->toBe('Needs attention')
|
||||
->and($governanceStatus['provider_permissions']['tone'] ?? null)->toBe('warning')
|
||||
->and($governanceStatus['provider_permissions']['description'] ?? null)->toContain('Required permissions currently look complete.')
|
||||
->and($governanceStatus['provider_permissions']['description'] ?? null)->toContain('The verification snapshot is stale.')
|
||||
->and($supportingSignals['provider_permissions']['value'] ?? null)->toBe('Needs attention')
|
||||
->and($supportingSignals['provider_permissions']['tone'] ?? null)->toBe('warning')
|
||||
->and($readinessDimensions['provider_permissions']['status'] ?? null)->toBe('Needs attention')
|
||||
->and($readinessDimensions['provider_permissions']['tone'] ?? null)->toBe('warning');
|
||||
});
|
||||
|
||||
it('shows calm honest fallbacks when no urgent tenant follow-up is visible', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
|
||||
@ -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,92 @@
|
||||
# Requirements Quality Checklist: Restore Readiness Resolution Adapter v1
|
||||
|
||||
**Purpose**: Validate preparation artifacts before implementation starts.
|
||||
**Created**: 2026-06-20
|
||||
**Feature**: `specs/390-restore-readiness-resolution-adapter-v1/spec.md`
|
||||
|
||||
## Candidate Selection
|
||||
|
||||
- [x] Direct user-provided candidate source recorded.
|
||||
- [x] Automatic queue status checked and summarized.
|
||||
- [x] Deferred alternatives recorded.
|
||||
- [x] Completed related specs identified as context only.
|
||||
- [x] No completed historical spec was rewritten.
|
||||
- [x] Spec number 390 intentionally selected from user draft and adjacent completed context.
|
||||
|
||||
## Scope Quality
|
||||
|
||||
- [x] Feature goal is clear and operator-facing.
|
||||
- [x] Scope, Primary Routes, Data Ownership, and RBAC requirements are explicitly declared.
|
||||
- [x] Mode B is selected as the default implementation mode.
|
||||
- [x] Generic resolution framework is explicitly out of scope.
|
||||
- [x] Adapter registry is explicitly out of scope.
|
||||
- [x] Generic persistence is explicitly out of scope.
|
||||
- [x] Review-publication resolution persistence reuse is explicitly forbidden.
|
||||
- [x] Restore auto-execution from guidance actions is explicitly forbidden.
|
||||
- [x] New navigation, dashboard, global search, and notification-center surfaces are explicitly out of scope.
|
||||
|
||||
## Requirements Completeness
|
||||
|
||||
- [x] User stories cover blocked, ready, persisted, and unauthorized states.
|
||||
- [x] Functional requirements are testable.
|
||||
- [x] Non-functional requirements are testable.
|
||||
- [x] Edge cases include stale basis, unsaved wizard state, execution states, and legacy statuses.
|
||||
- [x] Success criteria are measurable.
|
||||
- [x] No unresolved blocking clarification is present.
|
||||
- [x] Assumptions are documented.
|
||||
|
||||
## Existing-System Alignment
|
||||
|
||||
- [x] RestoreRun remains the persisted restore record.
|
||||
- [x] RestoreSafetyResolver remains the source for checks/preview currentness concepts.
|
||||
- [x] Restore Preview remains the preview owner.
|
||||
- [x] OperationRun remains execution/evidence truth.
|
||||
- [x] Restore Wizard remains the preparation input owner.
|
||||
- [x] Existing tenant/workspace/environment scoping is preserved.
|
||||
- [x] Existing view/manage capability split is preserved.
|
||||
|
||||
## UI and Filament Safety
|
||||
|
||||
- [x] Existing Restore create/view surfaces are the only planned UI surfaces.
|
||||
- [x] Changed Restore create/view surfaces include UI/UX Surface Classification.
|
||||
- [x] Changed Restore create/view surfaces include page contract fields and state ownership.
|
||||
- [x] Durable UI/productization coverage update is required because UI impact is material.
|
||||
- [x] Decision-first copy requirements are included.
|
||||
- [x] Copy must state preparation actions do not execute Restore.
|
||||
- [x] Filament v5 / Livewire v4 compliance is recorded in plan.
|
||||
- [x] Laravel provider registration location is recorded in plan.
|
||||
- [x] Global-search hard rule is recorded in plan and tasks.
|
||||
- [x] Destructive-action confirmation rule is recorded in spec, plan, and tasks.
|
||||
- [x] Asset strategy and `filament:assets` deployment condition are recorded.
|
||||
- [x] Testing approach for Filament pages/actions is recorded.
|
||||
|
||||
## Data and Runtime Safety
|
||||
|
||||
- [x] No new migration is planned.
|
||||
- [x] No new environment variable is planned.
|
||||
- [x] No new queue worker is planned.
|
||||
- [x] No scheduled command is planned.
|
||||
- [x] No storage/volume change is planned.
|
||||
- [x] No Microsoft Graph render-time call is planned.
|
||||
- [x] Staging validation requirement is recorded.
|
||||
|
||||
## Testability
|
||||
|
||||
- [x] Unit test expectations cover readiness derivation.
|
||||
- [x] Feature/Filament test expectations cover authorization and UI behavior.
|
||||
- [x] Testing/lane/runtime impact includes lane classification, fixture cost, heavy-family decision, budget/trend expectation, and escalation outcome.
|
||||
- [x] Query-count budget is measurable and has a validation task.
|
||||
- [x] Stale guidance action protection is explicitly tested.
|
||||
- [x] Execution confirmation/safety gate preservation is explicitly tested.
|
||||
- [x] Browser smoke expectations are recorded for visible UI changes.
|
||||
- [x] Validation commands are included.
|
||||
|
||||
## Implementation Readiness
|
||||
|
||||
- [x] Tasks are dependency-ordered.
|
||||
- [x] Contract/spec-package artifacts are required before app code changes.
|
||||
- [x] Provider seam classification and OperationRun UX contract reuse are covered.
|
||||
- [x] Audit/evidence reuse verification is covered.
|
||||
- [x] Stop conditions are explicit.
|
||||
- [x] Delivery notes include required Filament output contract items.
|
||||
- [x] Tasks avoid marking implementation work complete during preparation.
|
||||
@ -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.
|
||||
329
specs/390-restore-readiness-resolution-adapter-v1/plan.md
Normal file
@ -0,0 +1,329 @@
|
||||
# Implementation Plan: Restore Readiness Resolution Adapter v1
|
||||
|
||||
**Branch**: `390-restore-readiness-resolution-adapter-v1`
|
||||
**Spec**: `specs/390-restore-readiness-resolution-adapter-v1/spec.md`
|
||||
**Created**: 2026-06-20
|
||||
**Mode**: Mode B - Restore-local guidance on existing Restore surfaces
|
||||
|
||||
## Summary
|
||||
|
||||
Implement a Restore-owned readiness adapter that derives decision-first guidance from existing Restore state and shows the operator the next safe preparation or inspection action. The adapter must reuse existing RestoreRun, RestoreSafetyResolver, Restore Preview, confirmation, OperationRun, tenant scoping, and Filament surfaces.
|
||||
|
||||
The v1 implementation must not create a generic resolution engine, adapter registry, new persistence table, global search surface, or automatic execution path. Persisted generic resolution cases are deferred because current repo truth shows only review-publication-specific resolution persistence.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: PHP 8.4.15
|
||||
**Framework**: Laravel 12.52.0
|
||||
**Admin UI**: Filament 5.2.1
|
||||
**Reactive UI**: Livewire 4.1.4
|
||||
**Testing**: Pest 4.3.1 / PHPUnit 12
|
||||
**Database**: PostgreSQL
|
||||
**Local dev**: Laravel Sail first
|
||||
**Package constraints**: Do not add dependencies without explicit approval.
|
||||
|
||||
Relevant existing code:
|
||||
|
||||
- `apps/platform/app/Models/RestoreRun.php`
|
||||
- `apps/platform/app/Support/RestoreSafety/RestoreSafetyResolver.php`
|
||||
- `apps/platform/app/Support/RestoreRunStatus.php`
|
||||
- `apps/platform/app/Filament/Resources/RestoreRunResource.php`
|
||||
- `apps/platform/app/Filament/Resources/RestoreRunResource/Pages/CreateRestoreRun.php`
|
||||
- `apps/platform/app/Filament/Resources/RestoreRunResource/Pages/ViewRestoreRun.php`
|
||||
- `apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php`
|
||||
- `apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunDetailPresenter.php`
|
||||
- `apps/platform/app/Support/Auth/Capabilities.php`
|
||||
|
||||
Relevant completed context specs:
|
||||
|
||||
- `specs/386-review-publication-resolution-workflow-v1/`
|
||||
- `specs/387-review-publication-resolution-decision-ux-v1/`
|
||||
- `specs/388-resolution-proof-currentness-contract-v1/`
|
||||
- `specs/389-governance-inbox-resolution-intake-v1/`
|
||||
|
||||
These specs inform the decision-first pattern only. Their completed artifacts and review-publication persistence must not be rewritten.
|
||||
|
||||
## Constitution Check
|
||||
|
||||
- **Workspace first**: Pass. Guidance is scoped to existing workspace/tenant/environment RestoreRun surfaces.
|
||||
- **RBAC first**: Pass with implementation requirement. View guidance must require view access; mutating preparation actions must require manage access server-side.
|
||||
- **Audit first**: Pass with implementation requirement. User-triggered preparation mutations must preserve existing operation/audit evidence paths.
|
||||
- **Restore safety**: Pass. Execution remains controlled by existing safety gates, confirmation, and OperationRun execution truth.
|
||||
- **Spec before code**: Pass. This plan and tasks define implementation boundaries before app code changes.
|
||||
- **Proportionality review**: Pass. New local state/action contracts are allowed; new persistence and generic frameworks are not.
|
||||
|
||||
## Filament v5 / Livewire v4 Compliance Plan
|
||||
|
||||
- Filament work must use Filament v5 APIs only.
|
||||
- Livewire v4.0+ is required and present.
|
||||
- Laravel 11+ panel providers must remain registered through `bootstrap/providers.php`; this feature does not require a new panel provider.
|
||||
- `RestoreRunResource` already has a View page, satisfying the global-search hard rule if it remains globally searchable. If implementation touches global search behavior, explicitly verify or disable search.
|
||||
- Destructive actions must use `Action::make(...)->action(...)` plus `->requiresConfirmation()`. This feature should not add destructive actions.
|
||||
- Navigation-only actions must use `->url(...)` and must not be described as confirmed modal actions unless Filament v5 docs are verified.
|
||||
- No new heavy global assets are planned. If assets are registered despite this plan, deploy must run `cd apps/platform && php artisan filament:assets`.
|
||||
- Test pages/widgets/actions as Livewire components, not static resource classes.
|
||||
|
||||
## UI / Productization Guardrails
|
||||
|
||||
The feature touches existing operational UI, not marketing or landing content.
|
||||
|
||||
Required UI behavior:
|
||||
|
||||
- Use compact decision-first copy that fits existing Filament panel density.
|
||||
- Keep Restore Wizard as the preparation input owner.
|
||||
- Keep Restore Preview as the preview owner.
|
||||
- Keep RestoreRun View as a status/evidence inspection surface, not a new workflow hub.
|
||||
- Keep OperationRun links as evidence links only.
|
||||
- Avoid a top-level dashboard, nav item, global search result, or notification center integration.
|
||||
- Avoid visible instructional walls of text.
|
||||
- Avoid implying that a preparation action executes or approves a restore.
|
||||
|
||||
UI coverage to update during implementation if the visible surface materially changes:
|
||||
|
||||
- Durable UI/productization coverage registry artifacts under `docs/ui-ux-enterprise-audit/` for the changed RestoreRun create/view surfaces.
|
||||
- Existing UI coverage/report for RestoreRun create/view where those artifacts exist.
|
||||
- `docs/ui-ux-enterprise-audit/target-experience-briefs/restore-safety-workflow.md` if existing documentation requires alignment, or an explicit checked target-brief no-change note inside the coverage update.
|
||||
- Browser smoke notes for blocked, stale, ready, and read-only states.
|
||||
|
||||
Do not claim `No UI surface impact` for this feature unless the spec is updated first with a checked rationale. The default implementation path is to update UI coverage artifacts because RestoreRun create/view behavior changes.
|
||||
|
||||
## Shared Pattern and System Fit
|
||||
|
||||
This feature may introduce a Restore-specific support namespace, for example:
|
||||
|
||||
- `apps/platform/app/Support/RestoreReadinessResolution/`
|
||||
|
||||
Allowed local contracts:
|
||||
|
||||
- Restore readiness summary value object.
|
||||
- Restore reason/action identifiers.
|
||||
- Restore next-action planner.
|
||||
- Restore guidance basis/fingerprint helper.
|
||||
- Restore presenter adapter that converts readiness summaries into Filament-safe display data.
|
||||
|
||||
Disallowed contracts:
|
||||
|
||||
- Generic `ActionResolution` model or table.
|
||||
- Generic adapter registry.
|
||||
- Generic status taxonomy shared across unrelated domains.
|
||||
- Review-publication resolution persistence reuse.
|
||||
- Provider-specific Graph execution logic in the readiness layer.
|
||||
|
||||
## Data Model / Storage Plan
|
||||
|
||||
No database migration is expected.
|
||||
|
||||
Implementation must use existing persisted data:
|
||||
|
||||
- `restore_runs.requested_items`
|
||||
- `restore_runs.preview`
|
||||
- `restore_runs.results`
|
||||
- `restore_runs.metadata`
|
||||
- `restore_runs.group_mapping`
|
||||
- `restore_runs.operation_run_id`
|
||||
- `backup_sets`
|
||||
- `operation_runs`
|
||||
|
||||
If implementation discovers that persistence is required to satisfy acceptance criteria, stop and update `spec.md`, `plan.md`, and `tasks.md` before creating any migration. The update must include a new proportionality review and rollback/forward plan.
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
OperationRun remains execution/evidence truth.
|
||||
|
||||
Implementation must:
|
||||
|
||||
- Reuse the central OperationRun Start UX Contract for any OperationRun-linked evidence or operation-entry affordance.
|
||||
- Reuse existing OperationRun links/presenters where available.
|
||||
- Show queued/running RestoreRun states as execution states, not preparation states.
|
||||
- Avoid creating OperationRun records from the readiness guidance layer.
|
||||
- Avoid queuing database notifications from the readiness guidance layer. Queued/terminal notifications remain owned by the existing lifecycle mechanism unless this spec is updated first.
|
||||
- Avoid duplicate operation status rendering that conflicts with existing OperationRun UX.
|
||||
- Treat this feature as deep-link/inspect-only for OperationRun. It must not create, queue, deduplicate, resume, block, or complete OperationRun records.
|
||||
|
||||
## Provider Boundary
|
||||
|
||||
The touched seam is platform-core presentation over existing RestoreRun, BackupSet, RestoreSafetyResolver, and OperationRun data. Provider-owned semantics remain inside existing Restore services, Graph contract registry, and provider integration seams.
|
||||
|
||||
The readiness adapter is provider-neutral within the current RestoreRun domain. It must not call Microsoft Graph directly and must not assume provider-specific restore capabilities beyond existing RestoreRun/BackupSet/preview data. Any provider-specific hotspot discovered during implementation must be resolved in-feature by keeping it behind existing provider-owned seams, or escalated as a follow-up spec if it would leak into platform-core contracts or vocabulary.
|
||||
|
||||
Provider expansion or provider-specific readiness plugins require a later spec.
|
||||
|
||||
## Security and Authorization Plan
|
||||
|
||||
Implementation must:
|
||||
|
||||
- Reuse existing workspace/tenant/environment scoping traits and policies used by RestoreRunResource.
|
||||
- Require tenant view capability for inspection.
|
||||
- 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.
|
||||
|
||||
## Testing Plan
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Add focused tests for:
|
||||
|
||||
- Readiness summary derivation for missing checks.
|
||||
- Readiness summary derivation for stale checks.
|
||||
- Readiness summary derivation for missing preview.
|
||||
- Readiness summary derivation for stale preview.
|
||||
- Ready state with current checks and preview.
|
||||
- Execution state for queued/running RestoreRun.
|
||||
- Historical state for completed/partial/failed/cancelled/aborted RestoreRun.
|
||||
- Deterministic next safe action ordering.
|
||||
- Basis/fingerprint mismatch handling.
|
||||
|
||||
### Feature / Filament Tests
|
||||
|
||||
Add or update tests for:
|
||||
|
||||
- RestoreRun create wizard guidance in blocked, stale, and ready states.
|
||||
- RestoreRun view guidance for persisted records.
|
||||
- Read-only user can inspect permitted readiness details but cannot invoke mutating preparation actions.
|
||||
- Manage user can invoke allowed preparation action through existing Restore action path.
|
||||
- Existing final execution still requires confirmation and safety gate inputs.
|
||||
- Global search hard rule remains satisfied or disabled if touched.
|
||||
|
||||
### Browser Smoke
|
||||
|
||||
Use the repository browser smoke pattern if the UI change is visible:
|
||||
|
||||
- Authorized manage user sees blocked guidance and next safe action.
|
||||
- Authorized manage user sees stale preview guidance.
|
||||
- Authorized manage user sees ready-for-confirmation guidance.
|
||||
- Read-only user sees inspection-only affordances.
|
||||
- No visible copy implies automatic execution.
|
||||
|
||||
### Test Governance / Runtime Impact
|
||||
|
||||
Implementation must keep test scope proportional:
|
||||
|
||||
- Unit lane proves Restore-local derivation and side-effect-free behavior.
|
||||
- Feature/Filament lane proves authorization, wizard/view rendering, stale-action rejection, and execution-gate preservation.
|
||||
- Browser lane is limited to feature-local visible workflow smoke for blocked, stale, ready, and read-only states.
|
||||
- Fixture/context setup must stay opt-in and use existing factories/helpers. Do not introduce broad provider/workspace/membership defaults.
|
||||
- No new heavy-governance test family is expected. If implementation creates one, stop and update the spec/plan/tasks first.
|
||||
- Query-count validation must compare the Restore create/view render against the pre-feature fixture baseline and stay within the spec budget.
|
||||
- Expected lane decision: `keep`. Escalate as `document-in-feature` only if measured runtime or fixture cost materially changes; use `follow-up-spec` for recurring structural cost.
|
||||
|
||||
### Validation Commands
|
||||
|
||||
Preferred full validation:
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail artisan test
|
||||
cd apps/platform && ./vendor/bin/sail pint --dirty
|
||||
```
|
||||
|
||||
Focused validation should be added to implementation notes once concrete test paths exist.
|
||||
|
||||
## Project Structure
|
||||
|
||||
Expected implementation paths:
|
||||
|
||||
```text
|
||||
apps/platform/app/Support/RestoreReadinessResolution/
|
||||
apps/platform/app/Filament/Resources/RestoreRunResource.php
|
||||
apps/platform/app/Filament/Resources/RestoreRunResource/Pages/CreateRestoreRun.php
|
||||
apps/platform/app/Filament/Resources/RestoreRunResource/Pages/ViewRestoreRun.php
|
||||
apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php
|
||||
apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunDetailPresenter.php
|
||||
apps/platform/tests/Unit/Support/RestoreReadinessResolution/
|
||||
apps/platform/tests/Feature/Filament/
|
||||
apps/platform/tests/Feature/Restore/
|
||||
```
|
||||
|
||||
Spec-package implementation support artifacts to create during implementation:
|
||||
|
||||
```text
|
||||
specs/390-restore-readiness-resolution-adapter-v1/artifacts/current-restore-flow-inventory.md
|
||||
specs/390-restore-readiness-resolution-adapter-v1/contracts/restore-readiness-state-matrix.md
|
||||
specs/390-restore-readiness-resolution-adapter-v1/contracts/restore-requirement-map.md
|
||||
specs/390-restore-readiness-resolution-adapter-v1/contracts/restore-ui-copy-contract.md
|
||||
```
|
||||
|
||||
No expected paths:
|
||||
|
||||
```text
|
||||
apps/platform/database/migrations/*restore_readiness*
|
||||
apps/platform/app/Models/*ResolutionCase*
|
||||
apps/platform/app/Models/*ResolutionStep*
|
||||
apps/platform/app/Support/ActionResolution/
|
||||
apps/platform/app/Filament/Resources/*Resolution*
|
||||
```
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 0 - Repo Verification
|
||||
|
||||
Verify current RestoreRun surfaces, policies, presenters, RestoreSafetyResolver methods, OperationRun link helpers, and test fixture patterns. Record findings in `artifacts/current-restore-flow-inventory.md`.
|
||||
|
||||
### Phase 1 - Local Contracts
|
||||
|
||||
Define Restore-local readiness states, reasons, next-action identifiers, and copy rules. Record the state matrix, requirement map, and UI copy contract in spec-package contract files before app code changes.
|
||||
|
||||
### Phase 2 - Readiness Derivation
|
||||
|
||||
Implement a side-effect-free readiness resolver/planner that consumes existing Restore state and safety resolver outputs. Unit test all state transitions and next-action priority.
|
||||
|
||||
### Phase 3 - Stale Basis Protection
|
||||
|
||||
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
|
||||
|
||||
Integrate guidance into the existing RestoreRun create wizard/presenter without replacing the wizard. Keep controls compact and use existing action semantics.
|
||||
|
||||
### Phase 5 - Persisted RestoreRun View Integration
|
||||
|
||||
Add local status/evidence guidance to the existing RestoreRun view page. Do not create persisted resolution cases.
|
||||
|
||||
### Phase 6 - Authorization and Evidence
|
||||
|
||||
Verify view/manage split, OperationRun link access, and no data leakage for inaccessible related records.
|
||||
|
||||
### Phase 7 - Validation
|
||||
|
||||
Run unit, feature, Filament, browser smoke, Pint, and static diff checks. Update coverage notes only where repo conventions require it.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Potential complexity | Status | Mitigation |
|
||||
| --- | --- | --- |
|
||||
| New state/reason family | Accepted | Keep Restore-local and documented in contracts. |
|
||||
| Generic resolution persistence | Rejected | Defer to later spec. |
|
||||
| Adapter registry | Rejected | Use direct Restore-local support classes. |
|
||||
| Extra navigation/global search | Rejected | Existing Restore surfaces only. |
|
||||
| Execution bypass risk | Rejected | Existing confirmation/safety gates remain mandatory. |
|
||||
| UI sprawl | Controlled | Decision-first compact guidance only. |
|
||||
|
||||
## Deployment Plan
|
||||
|
||||
Default deployment impact:
|
||||
|
||||
- No migration.
|
||||
- No environment variable.
|
||||
- No new queue worker.
|
||||
- No new scheduler.
|
||||
- No new storage volume.
|
||||
- No new global frontend asset.
|
||||
|
||||
Validation and rollout:
|
||||
|
||||
- Validate in local Sail.
|
||||
- Validate on Staging before Production because Restore is an Intune-critical workflow.
|
||||
- Production promotion only after confirming no-auto-execute behavior, authorization behavior, and stale guidance rejection.
|
||||
|
||||
## Stop Conditions
|
||||
|
||||
Stop implementation and update the spec before continuing if any of the following becomes necessary:
|
||||
|
||||
- A new database table or migration.
|
||||
- A generic resolution case abstraction.
|
||||
- Reuse of review-publication resolution persistence for Restore.
|
||||
- A new global navigation or dashboard surface.
|
||||
- A new provider-specific restore backend.
|
||||
- Direct Microsoft Graph calls from readiness derivation.
|
||||
- Any execution path that bypasses existing final confirmation.
|
||||
355
specs/390-restore-readiness-resolution-adapter-v1/spec.md
Normal file
@ -0,0 +1,355 @@
|
||||
# Feature Specification: Restore Readiness Resolution Adapter v1
|
||||
|
||||
**Feature Branch**: `390-restore-readiness-resolution-adapter-v1`
|
||||
**Created**: 2026-06-20
|
||||
**Status**: Ready for implementation planning
|
||||
**Input**: User-provided candidate draft at `/Users/ahmeddarrazi/Downloads/spec-390-restore-readiness-resolution-adapter-v1-improved.md`
|
||||
|
||||
## Candidate Selection Summary
|
||||
|
||||
Spec 390 is selected as a direct, user-provided manual candidate. It follows the completed resolution-readiness work in Specs 386 through 389 and applies the proven decision-first pattern to Restore preparation without turning it into a generic workflow framework.
|
||||
|
||||
The active automatic queue in `docs/product/spec-candidates.md` has no safe next-best-prep target remaining. Manual backlog alternatives were deferred because they either require explicit promotion or are broader than this bounded Restore-owned slice.
|
||||
|
||||
Completed-spec guardrail result:
|
||||
|
||||
- `specs/386-review-publication-resolution-workflow-v1/` is completed and remains historical context only.
|
||||
- `specs/387-review-publication-resolution-decision-ux-v1/` is completed and remains historical context only.
|
||||
- `specs/388-resolution-proof-currentness-contract-v1/` is completed and remains historical context only.
|
||||
- `specs/389-governance-inbox-resolution-intake-v1/` is completed and remains historical context only.
|
||||
- No existing `specs/390-restore-readiness-resolution-adapter-v1/` artifact existed before this preparation branch.
|
||||
|
||||
Current repo truth shows the persisted resolution tables are review-publication-specific (`review_publication_resolution_cases`, `review_publication_resolution_steps`) and are not an approved generic action-key storage foundation. Therefore this spec selects Mode B for v1: Restore-local readiness guidance on existing Restore surfaces. Persisted generic resolution cases for Restore are out of scope unless a later spec explicitly creates and approves a generic persistence foundation.
|
||||
|
||||
## Spec Candidate Check
|
||||
|
||||
| Dimension | Score | Rationale |
|
||||
| --- | ---: | --- |
|
||||
| Nutzen | 2 | Restore is a critical admin workflow where blocked preparation must be explainable and safe. |
|
||||
| Dringlichkeit | 2 | Existing Restore code already has readiness gates, but the user-facing path from blocked state to next safe action is fragmented. |
|
||||
| Scope | 2 | The v1 slice is bounded to existing RestoreRun create/view surfaces and local derived guidance. |
|
||||
| Komplexitaetslast | 1 | The draft introduces readiness states, reason families, and action planning, so implementation must stay Restore-local. |
|
||||
| Produktnaehe | 2 | The result directly improves restore operator workflow and safety. |
|
||||
| Wiederverwendung | 1 | It reuses existing RestoreSafetyResolver, RestoreRun, OperationRun, and Filament surfaces, but must not reuse review-specific persistence. |
|
||||
|
||||
**Decision**: Approved as a bounded Core Enterprise feature with Mode B selected for v1.
|
||||
|
||||
**Scope shrink applied during preparation**:
|
||||
|
||||
- No generic workflow engine.
|
||||
- No adapter registry.
|
||||
- No new global navigation or global search surface.
|
||||
- No automatic execution of Restore.
|
||||
- No new persistence tables by default.
|
||||
- No reuse of review-publication resolution persistence for Restore.
|
||||
- No replacement of the Restore Wizard, RestoreSafetyResolver, Restore Preview, validation gates, confirmation, or OperationRun execution truth.
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- Derive a Restore-owned readiness summary from existing RestoreRun, wizard state, backup set, preview, checks, and OperationRun evidence.
|
||||
- Show decision-first guidance when Restore cannot continue safely.
|
||||
- Present deterministic next safe actions such as run checks, regenerate preview, review group mappings, review backup selection, open evidence, or return to confirmation when ready.
|
||||
- Protect against stale guidance by tying guidance to the same basis/fingerprint concepts already used by Restore safety code.
|
||||
- Support in-memory wizard state without creating orphaned persisted resolution cases.
|
||||
- Support persisted RestoreRun records with local guidance on the existing view surface where the underlying record exists.
|
||||
- Keep all execution truth inside existing Restore execution gates, confirmation, OperationRun, and safety validation.
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- Generic resolution framework, adapter registry, or cross-domain workflow engine.
|
||||
- Persisted generic resolution cases or steps for Restore.
|
||||
- New restore-specific database tables.
|
||||
- New top-level navigation, global search result type, dashboard widget, or notification center intake.
|
||||
- Automatic Restore execution, queued execution from guidance actions, or bypass of confirmation.
|
||||
- Replacement of RestoreSafetyResolver, existing preview generation, checks, or execution services.
|
||||
- Microsoft Graph write calls from the readiness guidance layer.
|
||||
- Broad redesign of Restore Wizard layout beyond the guidance and action affordances needed by this feature.
|
||||
|
||||
## Spec Scope Fields
|
||||
|
||||
- **Scope**: Existing tenant/environment-scoped Restore preparation and RestoreRun inspection surfaces in the Filament admin panel.
|
||||
- **Primary Routes**:
|
||||
- `GET admin/workspaces/{workspace}/environments/{environment}/restore-runs`
|
||||
- `GET admin/workspaces/{workspace}/environments/{environment}/restore-runs/create`
|
||||
- `GET admin/workspaces/{workspace}/environments/{environment}/restore-runs/{record}`
|
||||
- **Route Names**:
|
||||
- `filament.admin.resources.workspaces.{workspace}.environments.{environment}.restore-runs.index`
|
||||
- `filament.admin.resources.workspaces.{workspace}.environments.{environment}.restore-runs.create`
|
||||
- `filament.admin.resources.workspaces.{workspace}.environments.{environment}.restore-runs.view`
|
||||
- **Data Ownership**: Existing `restore_runs`, `backup_sets`, and `operation_runs` remain the only persisted records involved. Restore readiness summary, reason, next action, and basis/fingerprint values are derived and non-persisted. Tenant-bound RestoreRun data remains scoped by workspace plus managed environment/tenant authorization; unsaved wizard state remains in-memory only.
|
||||
- **RBAC Requirements**: Workspace/tenant/environment non-members must receive deny-as-not-found behavior through existing page/resource authorization. Members without view capability must not inspect readiness. Members without manage capability must not mutate preparation state and server-side execution must fail with capability denial. UI visibility is not authorization.
|
||||
- **Canonical View Status**: This is not a new tenantless canonical view. Existing environment-scoped RestoreRun create/view surfaces remain the canonical operator surfaces for this feature.
|
||||
|
||||
## User Problem
|
||||
|
||||
Restore operators can reach blocked, stale, or incomplete preparation states where the platform prevents unsafe execution, but the next safe recovery action is not always obvious. This creates support risk and can lead to repeated previews, skipped checks, or confusion between "not ready" and "failed."
|
||||
|
||||
This feature makes the Restore preparation state understandable without weakening the existing safety gates.
|
||||
|
||||
## Primary Operator Copy Contract
|
||||
|
||||
The experience must use decision-first, safety-first language:
|
||||
|
||||
- "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."
|
||||
|
||||
The UI must avoid language that implies the readiness action executes the restore, approves a restore, or overrides a safety gate.
|
||||
|
||||
## Users and Permissions
|
||||
|
||||
| Actor | Capability expectation | Allowed by this spec |
|
||||
| --- | --- | --- |
|
||||
| Workspace member with tenant view access | Inspect Restore readiness and evidence for accessible tenant/environment records. | View guidance and evidence links only. |
|
||||
| Workspace member with tenant manage access | Perform existing Restore preparation actions. | Run checks, generate/regenerate preview, update wizard preparation inputs through existing Restore UI paths. |
|
||||
| User without workspace membership or environment access | No access. | Existing access checks continue to block the surface. |
|
||||
| User without manage access | No mutation. | Guidance actions that mutate preparation state are hidden or blocked by policy/server checks. |
|
||||
|
||||
UI visibility is not authorization. All mutating preparation actions must still enforce server-side access checks.
|
||||
|
||||
## User Scenarios and Tests
|
||||
|
||||
### User Story 1 - Blocked Restore Shows the Next Safe Action (Priority: P1)
|
||||
|
||||
As an authorized restore operator, I want a blocked Restore preparation state to explain why Restore cannot continue and which safe action should happen next, so that I can recover without guessing.
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
1. Given a Restore wizard state with missing or stale checks, when the operator reaches the preparation step, then the UI states that Restore cannot continue yet and shows "Run readiness checks" as the next safe action.
|
||||
2. Given a Restore wizard state with a stale preview basis, when the operator reaches confirmation, then the UI blocks execution guidance and shows "Regenerate preview" as the next safe action.
|
||||
3. Given a blocked state, when the operator sees the next safe action, then the copy states that the action will not execute the restore.
|
||||
|
||||
### User Story 2 - Current Readiness Allows Continuation Without Extra Friction (Priority: P1)
|
||||
|
||||
As an authorized restore operator, I want current checks and preview evidence to be recognized, so that I can continue to confirmation without unnecessary repeated work.
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
1. Given current checks and preview that match the selected Restore scope, when the operator reviews readiness, then the UI shows the Restore is ready for final confirmation.
|
||||
2. Given current evidence with an OperationRun link, when the operator inspects details, then the UI links to the existing evidence where allowed.
|
||||
3. Given current readiness, when the operator proceeds, then existing final confirmation and execution gates still apply.
|
||||
|
||||
### User Story 3 - Persisted RestoreRun Records Explain Their State (Priority: P2)
|
||||
|
||||
As an operator reviewing a saved RestoreRun, I want the view page to explain whether the run is draft, previewed, pending, running, completed, failed, cancelled, or stale, so that I can decide what to inspect next.
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
1. Given a draft or scoped RestoreRun with incomplete preparation, when an authorized user opens its view page, then the page shows local readiness guidance and next safe inspection/preparation action.
|
||||
2. Given a queued or running RestoreRun, when an authorized user opens its view page, then the page emphasizes existing OperationRun execution truth rather than preparation guidance.
|
||||
3. Given a completed, partial, failed, cancelled, or legacy-aborted RestoreRun, when an authorized user opens its view page, then the page links to existing result/evidence details and does not offer preparation actions that would imply mutation of that historical run.
|
||||
|
||||
### User Story 4 - Unauthorized Users Cannot Mutate Preparation State (Priority: P2)
|
||||
|
||||
As a tenant owner, I want Restore readiness actions to obey existing RBAC boundaries, so that read-only users cannot run preparation mutations.
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
1. Given a user with tenant view access but not tenant manage access, when the user sees readiness guidance, then mutating preparation actions are unavailable and server-side calls are rejected.
|
||||
2. Given a user without workspace or environment access, when the user tries to access Restore readiness surfaces directly, then existing authorization behavior is preserved.
|
||||
3. Given a user with manage access, when the user starts a preparation action, then the action logs or preserves existing audit/evidence behavior already associated with that Restore preparation action.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-390-001**: The system MUST produce a Restore readiness summary from existing Restore state, including selected backup set, selected scope/items, checks state, preview state, group mapping requirements, execution status, and available OperationRun evidence.
|
||||
- **FR-390-002**: The readiness summary MUST identify whether Restore is blocked, needs preparation, ready for final confirmation, executing, completed, failed, cancelled, or historical/non-actionable.
|
||||
- **FR-390-003**: The readiness summary MUST identify the primary reason Restore cannot continue when it is not ready.
|
||||
- **FR-390-004**: The readiness summary MUST identify one deterministic next safe action when a safe action exists.
|
||||
- **FR-390-005**: The next safe action MUST be preparation-only unless it delegates to the existing final confirmation path.
|
||||
- **FR-390-006**: The next safe action copy MUST state when the action will not execute the restore.
|
||||
- **FR-390-007**: The system MUST protect against stale action execution by comparing the action's basis/fingerprint with the current Restore basis before performing a mutating preparation action.
|
||||
- **FR-390-008**: The system MUST use existing RestoreSafetyResolver concepts for preview/check currentness where possible instead of creating a parallel source of truth.
|
||||
- **FR-390-009**: The system MUST keep Restore execution authorization, confirmation, validation, and queueing inside the existing Restore execution path.
|
||||
- **FR-390-010**: The system MUST NOT execute a Restore as part of a readiness guidance action.
|
||||
- **FR-390-011**: The system MUST NOT create review-publication resolution cases or steps for Restore.
|
||||
- **FR-390-012**: The system MUST NOT introduce generic resolution persistence in this v1 spec.
|
||||
- **FR-390-013**: The system MUST support in-memory wizard state by showing inline guidance only.
|
||||
- **FR-390-014**: The system MUST support persisted RestoreRun records by showing local guidance on existing Restore view surfaces where the record already exists.
|
||||
- **FR-390-015**: The system MUST not create orphaned persisted cases for unsaved wizard state.
|
||||
- **FR-390-016**: The system MUST keep the Restore Wizard as the owner of preparation input collection.
|
||||
- **FR-390-017**: The system MUST keep Restore Preview as the owner of preview content.
|
||||
- **FR-390-018**: The system MUST keep OperationRun as the owner of execution/evidence truth.
|
||||
- **FR-390-019**: The system MUST show evidence links only where the current user is authorized to access the linked record.
|
||||
- **FR-390-020**: The system MUST preserve existing tenant/workspace/environment scoping.
|
||||
- **FR-390-021**: Mutating readiness/preparation actions MUST require the same manage capability expected by existing Restore preparation actions.
|
||||
- **FR-390-022**: Read-only inspection MUST require the same view capability expected by existing RestoreRun view access.
|
||||
- **FR-390-023**: Any new destructive or high-impact Filament action introduced by implementation MUST execute through `Action::make(...)->action(...)`, enforce server-side authorization, and include `->requiresConfirmation()` when destructive.
|
||||
- **FR-390-024**: URL-only navigation actions MUST not be described as confirmed actions unless Filament v5 documentation is verified for that behavior.
|
||||
- **FR-390-025**: Global search behavior MUST remain valid for `RestoreRunResource`: either the resource has an Edit/View page if searchable, or global search is explicitly disabled.
|
||||
- **FR-390-026**: The implementation MUST provide meaningful empty or inactive states for any table, list, or repeated readiness item it adds.
|
||||
- **FR-390-027**: The implementation MUST not add heavy global frontend assets for this feature.
|
||||
- **FR-390-028**: The implementation MUST use existing Filament v5 and Livewire v4 patterns and avoid Filament v3/v4 APIs.
|
||||
- **FR-390-029**: The implementation MUST avoid new Microsoft Graph calls during page render or readiness display.
|
||||
- **FR-390-030**: The implementation MUST not assume a provider-specific restore backend beyond the existing RestoreRun data and service abstractions.
|
||||
- **FR-390-031**: The implementation MUST maintain auditability for user-triggered preparation mutations by reusing or preserving existing operation/audit trails where available.
|
||||
- **FR-390-032**: The implementation MUST include tests proving that stale guidance cannot mutate an out-of-date Restore preparation state.
|
||||
- **FR-390-033**: The implementation MUST include tests proving read-only users cannot trigger mutating preparation actions.
|
||||
- **FR-390-034**: The implementation MUST include tests proving execution still requires existing final confirmation and safety gates.
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- **NFR-390-001**: Readiness derivation MUST be deterministic for the same Restore state.
|
||||
- **NFR-390-002**: Readiness derivation MUST be side-effect-free.
|
||||
- **NFR-390-003**: Readiness display MUST not materially increase query count on Restore create/view pages.
|
||||
- **NFR-390-004**: Readiness copy MUST be concise enough to fit existing Filament panel surfaces on desktop and mobile.
|
||||
- **NFR-390-005**: The implementation MUST be testable without external Microsoft Graph calls.
|
||||
- **NFR-390-006**: The implementation MUST be deployable without new environment variables, queue workers, scheduled commands, or persistent storage by default.
|
||||
|
||||
## UI Surface Impact
|
||||
|
||||
| Surface | Impact |
|
||||
| --- | --- |
|
||||
| RestoreRun create wizard | Existing page changed. Adds decision-first readiness guidance and next safe action affordances in current wizard steps. |
|
||||
| RestoreRun view page | Existing page changed. Adds local readiness/status explanation for persisted runs where useful. |
|
||||
| RestoreRun list table | No required change. Do not add new bulk or row mutation unless implementation proves a narrow need and updates this spec first. |
|
||||
| Global search | No new result type. Validate existing RestoreRunResource hard rule if the implementation touches resource search behavior. |
|
||||
| Navigation | No new top-level navigation, cluster, widget, or dashboard surface. |
|
||||
| OperationRun links | Existing evidence links may be reused where authorization permits. |
|
||||
|
||||
## UI/UX Surface Classification
|
||||
|
||||
This feature materially changes reachable operator-facing surfaces. No `No UI surface impact` decision is claimed. Implementation must update the durable UI/productization coverage registry under `docs/ui-ux-enterprise-audit/` for the changed RestoreRun create/view surfaces, or stop and update this spec if repo coverage tooling proves a narrower checked decision is required.
|
||||
|
||||
| Surface | Page archetype | Surface class | Native/custom/shared | Primary persona | Primary operator question | Dominant next action | Dangerous actions | State ownership |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| RestoreRun create wizard | Create/Edit staged workflow | Preparation workflow surface | Native Surface using Filament wizard/presenter paths; no UI exception planned | Tenant operator/manager preparing a restore | "What prevents this restore from being safe to confirm, and what should I do next?" | Run checks, regenerate preview, review mappings, or continue to final confirmation when ready | Existing final Restore execution remains the dangerous action and stays behind existing confirmation/hard-confirm gates | Requested/draft/restorable state is owned by the Restore wizard and RestoreSafetyResolver-derived basis; readiness display is derived presentation only |
|
||||
| RestoreRun view page | View/Detail status surface | Detail/status inspection surface | Native Surface using existing RestoreRun detail presenter paths; no UI exception planned | Tenant operator/manager or read-only reviewer inspecting a saved restore | "What state is this RestoreRun in, what evidence exists, and is any safe preparation/inspection action available?" | Inspect evidence, open OperationRun, or return to the create/preparation flow only where existing state permits | No new dangerous action; historical states must not expose preparation mutations | Inspect/execution state is owned by RestoreRun and OperationRun; local guidance must not duplicate or override execution truth |
|
||||
|
||||
Page contract details:
|
||||
|
||||
- Default-visible information: readiness/status, primary reason, next safe action, mutation scope, and final-confirmation requirement.
|
||||
- Diagnostics-only information: raw basis/fingerprint details, raw preview/check payload fragments, and support evidence metadata.
|
||||
- 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 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, 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. |
|
||||
| Execute restore | Existing Restore execution path | Yes | Yes | Tenant manage plus existing gates | Existing confirmation, hard confirm, safety validation, and OperationRun execution truth remain mandatory. |
|
||||
|
||||
## Key Entities and Concepts
|
||||
|
||||
This spec prefers existing entities and local derived value objects over new persisted models.
|
||||
|
||||
- **RestoreRun**: Existing persisted restore record. Owns restore status, selected scope, preview/results metadata, group mapping, idempotency key, and OperationRun linkage.
|
||||
- **BackupSet**: Existing source for backup payload selection and restore scope.
|
||||
- **OperationRun**: Existing evidence/execution truth for queued/running/completed operational work.
|
||||
- **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 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
|
||||
|
||||
This feature introduces a local Restore-specific readiness summary/action contract and possibly a small Restore-specific state/reason family. It does not introduce a new persisted entity, generic workflow engine, cross-domain taxonomy, or storage abstraction.
|
||||
|
||||
**Constitution review result**:
|
||||
|
||||
- New persisted entity: No.
|
||||
- New database table: No.
|
||||
- New enum/status family: Yes, but only local Restore readiness/reason/action identifiers derived from existing Restore state.
|
||||
- New abstraction: Yes, bounded to Restore readiness derivation and presentation.
|
||||
- New taxonomy/framework: No. Generic action resolution remains out of scope.
|
||||
- Canonical user-facing view: No new canonical view. Existing Restore create/view surfaces remain the canonical surfaces.
|
||||
- Reuse before build: Required. RestoreSafetyResolver, RestoreRun, RestoreRun presenters, OperationRun links, existing policies/capabilities, and Filament action patterns must be reused where possible.
|
||||
|
||||
The proportional response is to create Restore-local contracts and tests, not persistence or a generic adapter system.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- RestoreRun has a legacy `aborted` status. Treat it as historical/non-actionable and preserve existing display compatibility.
|
||||
- RestoreRun has stale checks but current preview. The next safe action must prioritize checks before confirmation.
|
||||
- RestoreRun has current checks but stale preview. The next safe action must prioritize preview regeneration.
|
||||
- RestoreRun is queued or running. Guidance must defer to OperationRun execution truth and avoid preparation actions.
|
||||
- RestoreRun is completed, partial, failed, cancelled, or completed with errors. Guidance must explain result/evidence inspection and avoid preparation mutations.
|
||||
- Wizard state is unsaved. Guidance must remain inline and non-persisted.
|
||||
- User loses manage access after the page renders. Mutating action must fail server-side.
|
||||
- Restore scope changes after readiness action render. Action must fail stale basis validation and ask the operator to refresh/regenerate.
|
||||
- BackupSet or environment access is unavailable. Guidance must show safe blocked state without leaking inaccessible names or payload details.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- **SC-390-001**: In tests, a blocked Restore preparation state produces one clear reason and one deterministic next safe action.
|
||||
- **SC-390-002**: In tests, current checks and preview allow continuation to final confirmation while still requiring existing execution confirmation gates.
|
||||
- **SC-390-003**: In tests, stale basis data prevents mutating readiness/preparation actions.
|
||||
- **SC-390-004**: In tests, read-only users can inspect allowed readiness information but cannot trigger mutating preparation actions.
|
||||
- **SC-390-005**: Browser smoke coverage verifies blocked, stale, and ready Restore wizard states with no copy implying automatic execution.
|
||||
- **SC-390-006**: No new migration, environment variable, queue, scheduled command, global asset registration, or navigation surface is required for v1.
|
||||
|
||||
## Testing Expectations
|
||||
|
||||
Implementation must include:
|
||||
|
||||
- Unit tests for Restore readiness derivation from representative Restore states.
|
||||
- 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.
|
||||
- Browser smoke coverage for at least blocked, stale, and ready states if UI changes are visible in the panel.
|
||||
|
||||
## 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, 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.
|
||||
- **Runtime budget/baseline/trend impact**: No material suite-cost increase is expected. If browser or fixture expansion materially changes runtime, document the measured impact in the implementation PR and escalate as `document-in-feature`; recurring/structural cost must become `follow-up-spec`.
|
||||
- **Query budget**: Restore readiness display must not add more than two database queries to the default Restore create or view render compared with the pre-feature fixture baseline. Authorized evidence links may add relationship loading only when visible and must be eager-loaded or documented.
|
||||
- **Escalation outcome**: `keep`, provided implementation stays Restore-local and avoids generic persistence, hidden heavy fixtures, and broad browser families.
|
||||
|
||||
Preferred local validation commands:
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail artisan test
|
||||
cd apps/platform && ./vendor/bin/sail pint --dirty
|
||||
```
|
||||
|
||||
Focused commands may be used during implementation once concrete test paths exist.
|
||||
|
||||
## Deployment and Runtime Impact
|
||||
|
||||
- Database migrations: Not expected for v1.
|
||||
- Environment variables: Not expected.
|
||||
- Queue workers: No new workers expected.
|
||||
- Scheduled commands: Not expected.
|
||||
- Storage/volumes: Not expected.
|
||||
- Filament assets: No new heavy assets expected. If registered assets are added despite this plan, deployment must include `cd apps/platform && php artisan filament:assets`.
|
||||
- Staging validation: Required because Restore is an Intune-critical flow, even though v1 is guidance/preparation-only.
|
||||
- Production promotion: Validate on Staging first, including authorization and no-auto-execute behavior.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Existing RestoreRun create/view pages remain the primary surfaces.
|
||||
- Existing RestoreSafetyResolver and RestoreRun presenter classes provide enough state to derive readiness without new persistence.
|
||||
- Existing tenant manage/view capabilities remain the correct authorization boundary for Restore preparation and inspection.
|
||||
- Mode B is sufficient for v1 because current resolution persistence is review-publication-specific.
|
||||
- Any future generic resolution persistence requires a separate spec and proportionality review.
|
||||
|
||||
## Open Questions
|
||||
|
||||
No blocking product questions remain for preparation. Implementation must verify exact class names, route names, and test fixture helpers before editing application code.
|
||||
|
||||
## Follow-Up Candidates
|
||||
|
||||
- Persisted generic resolution case foundation, if a future cross-domain action-resolution model is explicitly approved.
|
||||
- Governance Inbox Restore readiness intake, after Restore-local guidance proves stable.
|
||||
- Provider-specific Restore adapter expansion, after the local Restore contract is validated.
|
||||
- Restore preview item-level remediation guidance, if operators need row-level recovery actions.
|
||||
156
specs/390-restore-readiness-resolution-adapter-v1/tasks.md
Normal file
@ -0,0 +1,156 @@
|
||||
# Tasks: Restore Readiness Resolution Adapter v1
|
||||
|
||||
**Input**: `specs/390-restore-readiness-resolution-adapter-v1/spec.md`, `specs/390-restore-readiness-resolution-adapter-v1/plan.md`
|
||||
**Branch**: `390-restore-readiness-resolution-adapter-v1`
|
||||
**Mode**: Mode B - Restore-local guidance only
|
||||
|
||||
## Execution Rules
|
||||
|
||||
- Do not implement a generic resolution framework.
|
||||
- Do not add a database migration unless `spec.md` and `plan.md` are updated first.
|
||||
- Do not reuse review-publication resolution persistence for Restore.
|
||||
- Do not add top-level navigation, global search surfaces, dashboards, or notification-center intake.
|
||||
- Do not execute Restore from a readiness guidance action.
|
||||
- Keep Restore execution behind existing confirmation, validation, and OperationRun paths.
|
||||
- Keep all tasks unchecked until implementation work is actually completed.
|
||||
|
||||
## Phase 1: Repo Verification and Spec-Package Contracts
|
||||
|
||||
- [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
|
||||
|
||||
- [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
|
||||
|
||||
- [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
|
||||
|
||||
- [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
|
||||
|
||||
- [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
|
||||
|
||||
- [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
|
||||
|
||||
- [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
|
||||
|
||||
- [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
|
||||
|
||||
- [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
|
||||
|
||||
- [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
|
||||
|
||||
- Phase 1 must complete before app code changes.
|
||||
- Phase 2 and Phase 3 tests should be written before or alongside Phase 4 support implementation.
|
||||
- Phase 4 must complete before UI integration in Phases 5 and 6.
|
||||
- Phase 5 and Phase 6 can proceed independently after Phase 4.
|
||||
- Phase 8 browser smoke depends on visible UI integration.
|
||||
- Phase 9 must complete before delivery.
|
||||
|
||||
## Parallel Work Candidates
|
||||
|
||||
- T012 through T021 can be split by readiness state after contracts exist.
|
||||
- T022 through T028 can be split by authorization/stale-action concern after fixture patterns are verified.
|
||||
- T049 through T055 can proceed in parallel with T039 through T048 after the support resolver is stable.
|
||||
- T062 through T066 can be written in parallel with UI integration once page/presenter seams are known.
|
||||
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 |
BIN
tenantpilot-environment-overview-playwright.png
Normal file
|
After Width: | Height: | Size: 349 KiB |
BIN
tenantpilot-evidence-basis-playwright.png
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
tenantpilot-login-playwright.png
Normal file
|
After Width: | Height: | Size: 25 KiB |