feat: migrate decision page contracts to productized flow
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m31s
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m31s
This commit is contained in:
parent
a5b7300ca9
commit
9e57fa3991
@ -462,16 +462,15 @@ private function decisionCard(bool $hasWarnings, bool $hasCoverageWarnings, bool
|
||||
private function decisionStatus(string $state, int $findingsCount, bool $hasWarnings): string
|
||||
{
|
||||
return match ($state) {
|
||||
'no_assignment' => 'Baseline not assigned',
|
||||
'no_snapshot' => 'Baseline snapshot required',
|
||||
'invalid_scope' => 'Baseline scope requires review',
|
||||
'comparing' => 'Compare in progress',
|
||||
'failed' => 'Compare failed',
|
||||
'idle' => 'Compare run required',
|
||||
'no_assignment' => 'Not configured',
|
||||
'no_snapshot', 'invalid_scope' => 'Blocked',
|
||||
'comparing' => 'Running',
|
||||
'failed' => 'Failed',
|
||||
'idle' => 'Needs attention',
|
||||
'ready' => $findingsCount > 0
|
||||
? 'Drift findings available'
|
||||
: ($hasWarnings ? 'Decision output needs review' : 'No drift detected'),
|
||||
default => 'Compare unavailable',
|
||||
? 'Needs attention'
|
||||
: ($hasWarnings ? 'Needs attention' : 'Ready'),
|
||||
default => 'Unknown',
|
||||
};
|
||||
}
|
||||
|
||||
@ -492,7 +491,7 @@ private function decisionReason(string $state, int $findingsCount): string
|
||||
'ready' => $findingsCount > 0
|
||||
? 'Baseline comparison found governance-relevant differences. Drift requires review before a decision is recorded.'
|
||||
: 'Current environment state matches the assigned baseline within available compare coverage.',
|
||||
default => 'Compare state is derived from the latest baseline assignment, snapshot, and operation proof.',
|
||||
default => 'Compare state is derived from the latest baseline assignment, snapshot, and compare result.',
|
||||
};
|
||||
}
|
||||
|
||||
@ -511,11 +510,11 @@ private function decisionImpact(string $state, int $findingsCount, bool $hasCove
|
||||
}
|
||||
|
||||
if ($state === 'failed') {
|
||||
return 'Drift findings cannot be trusted until the failure is resolved. Review operation proof before retrying.';
|
||||
return 'Drift findings cannot be trusted until the failure is resolved. Retry compare when the inputs are ready, or use technical details for audit review.';
|
||||
}
|
||||
|
||||
if ($state === 'comparing') {
|
||||
return 'Drift findings are not final yet. Wait for operation proof before acting on drift or evidence state.';
|
||||
return 'Drift findings are not final yet. Wait for the compare result before acting on drift or evidence state.';
|
||||
}
|
||||
|
||||
if ($state === 'idle') {
|
||||
@ -578,21 +577,26 @@ private function primaryDecisionAction(string $state, int $findingsCount): array
|
||||
];
|
||||
}
|
||||
|
||||
if ($state === 'comparing' && $this->getRunUrl() !== null) {
|
||||
if ($state === 'comparing') {
|
||||
return [
|
||||
'actionLabel' => 'View operation progress',
|
||||
'actionUrl' => $this->getRunUrl(),
|
||||
'actionDisabled' => false,
|
||||
'helperText' => null,
|
||||
'actionLabel' => 'Comparison running',
|
||||
'actionUrl' => null,
|
||||
'actionDisabled' => true,
|
||||
'helperText' => 'Refresh will update this decision when the compare run finishes.',
|
||||
];
|
||||
}
|
||||
|
||||
if ($state === 'failed' && $this->getRunUrl() !== null) {
|
||||
if ($state === 'failed') {
|
||||
$canRunCompare = $this->canRunCompareAction();
|
||||
|
||||
return [
|
||||
'actionLabel' => 'Review compare failure',
|
||||
'actionUrl' => $this->getRunUrl(),
|
||||
'actionDisabled' => false,
|
||||
'helperText' => null,
|
||||
'actionLabel' => $canRunCompare ? 'Retry comparison' : 'Compare unavailable',
|
||||
'actionUrl' => null,
|
||||
'actionDisabled' => ! $canRunCompare,
|
||||
'actionName' => 'compareNow',
|
||||
'helperText' => $canRunCompare
|
||||
? 'Retry compare after reviewing the failure summary.'
|
||||
: 'You are not authorized to retry baseline compare from this environment.',
|
||||
];
|
||||
}
|
||||
|
||||
@ -605,20 +609,20 @@ private function primaryDecisionAction(string $state, int $findingsCount): array
|
||||
];
|
||||
}
|
||||
|
||||
if ($state === 'ready' && $this->getRunUrl() !== null) {
|
||||
if ($state === 'ready') {
|
||||
return [
|
||||
'actionLabel' => 'Review evidence',
|
||||
'actionUrl' => $this->getRunUrl(),
|
||||
'actionDisabled' => false,
|
||||
'helperText' => null,
|
||||
'actionLabel' => 'No action required',
|
||||
'actionUrl' => null,
|
||||
'actionDisabled' => true,
|
||||
'helperText' => 'Technical evidence remains available in the collapsed audit details.',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'actionLabel' => 'Review compare state',
|
||||
'actionUrl' => $this->getRunUrl(),
|
||||
'actionDisabled' => $this->getRunUrl() === null,
|
||||
'helperText' => $this->getRunUrl() === null ? 'No operation proof link is currently available.' : null,
|
||||
'actionUrl' => null,
|
||||
'actionDisabled' => true,
|
||||
'helperText' => 'No primary operator action is available for this compare state.',
|
||||
];
|
||||
}
|
||||
|
||||
@ -696,11 +700,11 @@ private function proofPanelItems(bool $hasWarnings, bool $hasCoverageWarnings, b
|
||||
],
|
||||
[
|
||||
'key' => 'operation_run_proof',
|
||||
'label' => 'OperationRun proof',
|
||||
'label' => 'Operation evidence',
|
||||
'value' => $operationProofAvailable ? 'Available' : 'Unavailable',
|
||||
'tone' => $operationProofAvailable ? 'success' : 'gray',
|
||||
'description' => $operationProofAvailable ? 'Compare proof is linked to an OperationRun.' : 'No compare OperationRun proof is available yet.',
|
||||
'actionLabel' => $operationProofAvailable ? 'Open operation proof' : null,
|
||||
'description' => $operationProofAvailable ? 'Technical run details are available for audit review.' : 'No compare operation details are available yet.',
|
||||
'actionLabel' => $operationProofAvailable ? 'Open operation details' : null,
|
||||
'actionUrl' => $operationProofAvailable ? $this->getRunUrl() : null,
|
||||
],
|
||||
[
|
||||
@ -826,7 +830,6 @@ private function availableCompareInputs(bool $hasWarnings, bool $hasCoverageWarn
|
||||
$baselineSnapshotState = $this->state === 'no_snapshot'
|
||||
? 'Missing'
|
||||
: ($this->snapshotId !== null ? 'Available' : 'Unavailable');
|
||||
$operationProofState = $this->operationRunId !== null && $this->getRunUrl() !== null ? 'Available' : 'Unavailable';
|
||||
$driftFindingsState = $this->state === 'ready' ? 'Available' : 'Unavailable';
|
||||
$evidencePathState = $this->evidenceInputState($hasWarnings);
|
||||
|
||||
@ -853,14 +856,6 @@ private function availableCompareInputs(bool $hasWarnings, bool $hasCoverageWarn
|
||||
'tone' => $this->flowTone($environmentSnapshotState),
|
||||
'description' => $this->environmentSnapshotDescription($environmentSnapshotState),
|
||||
],
|
||||
[
|
||||
'label' => 'OperationRun proof',
|
||||
'state' => $operationProofState,
|
||||
'tone' => $this->flowTone($operationProofState),
|
||||
'description' => $operationProofState === 'Available'
|
||||
? 'A compare OperationRun proof link is available.'
|
||||
: 'No compare OperationRun proof is available yet.',
|
||||
],
|
||||
[
|
||||
'label' => 'Drift findings',
|
||||
'state' => $driftFindingsState,
|
||||
@ -877,7 +872,6 @@ private function availableCompareInputs(bool $hasWarnings, bool $hasCoverageWarn
|
||||
}
|
||||
|
||||
$environmentSnapshotState = $this->hasEnvironmentSnapshot() ? 'Available' : 'Unavailable';
|
||||
$operationProofState = $this->operationRunId !== null && $this->getRunUrl() !== null ? 'Available' : 'Unavailable';
|
||||
|
||||
return [
|
||||
[
|
||||
@ -888,14 +882,6 @@ private function availableCompareInputs(bool $hasWarnings, bool $hasCoverageWarn
|
||||
? 'Current environment evidence is present.'
|
||||
: 'No repo-backed environment snapshot is available yet.',
|
||||
],
|
||||
[
|
||||
'label' => 'Operation proof',
|
||||
'state' => $operationProofState,
|
||||
'tone' => $operationProofState === 'Available' ? 'success' : 'gray',
|
||||
'description' => $operationProofState === 'Available'
|
||||
? 'A compare operation proof link is available.'
|
||||
: 'No compare operation proof is available yet.',
|
||||
],
|
||||
[
|
||||
'label' => 'Baseline snapshot',
|
||||
'state' => 'Unavailable',
|
||||
@ -1010,7 +996,7 @@ private function evidencePathSummary(bool $hasCoverageWarnings, bool $hasEvidenc
|
||||
}
|
||||
|
||||
if ($this->operationRunId !== null) {
|
||||
return 'Evidence unavailable - Operation proof available';
|
||||
return 'Compare evidence available in technical details';
|
||||
}
|
||||
|
||||
return 'Evidence unavailable';
|
||||
@ -1155,7 +1141,7 @@ private function resolveFindingsColorClass(bool $hasWarnings): string
|
||||
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
||||
{
|
||||
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly)
|
||||
->satisfy(ActionSurfaceSlot::ListHeader, 'Header action: Compare Now (confirmation modal, capability-gated).')
|
||||
->exempt(ActionSurfaceSlot::ListHeader, 'Compare Now is mounted from the decision card only, so the header does not render a duplicate primary action.')
|
||||
->exempt(ActionSurfaceSlot::InspectAffordance, 'This is a tenant-scoped landing page, not a record inspect surface.')
|
||||
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'This page does not render table rows with secondary actions.')
|
||||
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'This page has no bulk actions.')
|
||||
@ -1178,12 +1164,10 @@ protected function getHeaderActions(): array
|
||||
->url($navigationContext->backLinkUrl);
|
||||
}
|
||||
|
||||
$actions[] = $this->compareNowAction();
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
private function compareNowAction(): Action
|
||||
protected function compareNowAction(): Action
|
||||
{
|
||||
$isFullContent = false;
|
||||
|
||||
|
||||
@ -352,10 +352,12 @@ public static function getWizardSteps(): array
|
||||
->schema([
|
||||
Forms\Components\ViewField::make('restore_safety_decision')
|
||||
->hiddenLabel()
|
||||
->columnSpanFull()
|
||||
->view('filament.forms.components.restore-run-safety-decision')
|
||||
->viewData(fn (Get $get): array => static::restoreWizardViewData($get, currentStep: 1)),
|
||||
Forms\Components\Select::make('backup_set_id')
|
||||
->label('Backup set')
|
||||
->columnSpanFull()
|
||||
->options(fn () => static::restoreBackupSetOptions())
|
||||
->helperText(fn (Get $get): string => static::restoreBackupSetHelperText($get('backup_set_id')))
|
||||
->reactive()
|
||||
@ -388,6 +390,7 @@ public static function getWizardSteps(): array
|
||||
->required(),
|
||||
Forms\Components\ViewField::make('restore_backup_quality_summary')
|
||||
->hiddenLabel()
|
||||
->columnSpanFull()
|
||||
->view('filament.forms.components.restore-run-backup-quality-summary')
|
||||
->viewData(fn (Get $get): array => [
|
||||
...static::restoreWizardViewData($get, currentStep: 1),
|
||||
@ -395,6 +398,7 @@ public static function getWizardSteps(): array
|
||||
]),
|
||||
Forms\Components\ViewField::make('restore_safety_evidence')
|
||||
->hiddenLabel()
|
||||
->columnSpanFull()
|
||||
->view('filament.forms.components.restore-run-safety-evidence')
|
||||
->viewData(fn (Get $get): array => static::restoreWizardViewData($get, currentStep: 1, compactFlow: true)),
|
||||
]),
|
||||
|
||||
@ -240,6 +240,7 @@ public static function contract(
|
||||
executionReadiness: is_array($executionReadiness) ? $executionReadiness : [],
|
||||
)
|
||||
->toArray();
|
||||
$readinessGuidance = self::canonicalRestoreReadinessGuidance($readinessGuidance);
|
||||
|
||||
$decisionCard = self::restoreWizardDecisionCard(
|
||||
backupSet: $backupSet,
|
||||
@ -1011,11 +1012,14 @@ private static function restoreWizardDecisionCard(
|
||||
: 'review_scope';
|
||||
|
||||
$status = match (true) {
|
||||
! ($backupSet instanceof BackupSet) => 'Source required',
|
||||
! $hasUsableSource => 'Source not usable',
|
||||
! $checksAreCurrent || ! $previewIsCurrent => 'Source selected',
|
||||
$safetyState !== null => RestoreSafetyCopy::safetyStateLabel($safetyState),
|
||||
default => 'Unavailable',
|
||||
! ($backupSet instanceof BackupSet) => 'Not configured',
|
||||
! $hasUsableSource => 'Blocked',
|
||||
! $checksAreCurrent || ! $previewIsCurrent => 'Needs attention',
|
||||
$safetyState === 'ready' => 'Ready',
|
||||
$safetyState === 'ready_with_caution' => 'Needs attention',
|
||||
$safetyState === 'blocked' => 'Blocked',
|
||||
$safetyState === 'risky' => 'Needs attention',
|
||||
default => 'Unknown',
|
||||
};
|
||||
|
||||
$reason = match (true) {
|
||||
@ -1045,16 +1049,16 @@ private static function restoreWizardDecisionCard(
|
||||
|
||||
$tone = match (true) {
|
||||
! ($backupSet instanceof BackupSet) => 'gray',
|
||||
! $hasUsableSource => 'warning',
|
||||
! $hasUsableSource => 'danger',
|
||||
$backupQuality instanceof BackupQualitySummary && $backupQuality->hasDegradations() => 'warning',
|
||||
! $checksAreCurrent || ! $previewIsCurrent => 'success',
|
||||
! $checksAreCurrent || ! $previewIsCurrent => 'warning',
|
||||
($safetyAssessment['state'] ?? null) === 'ready' => 'success',
|
||||
($safetyAssessment['state'] ?? null) === 'ready_with_caution' => 'warning',
|
||||
default => 'danger',
|
||||
};
|
||||
|
||||
return [
|
||||
'title' => 'Restore Safety',
|
||||
'title' => 'Is this restore safe to continue?',
|
||||
'statusLabel' => 'Status',
|
||||
'reasonLabel' => 'Reason',
|
||||
'impactLabel' => 'Impact',
|
||||
@ -1068,6 +1072,30 @@ private static function restoreWizardDecisionCard(
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function canonicalRestoreReadinessGuidance(array $readinessGuidance): array
|
||||
{
|
||||
$state = is_string($readinessGuidance['state'] ?? null)
|
||||
? $readinessGuidance['state']
|
||||
: null;
|
||||
|
||||
$readinessGuidance['stateLabel'] = match ($state) {
|
||||
'ready_for_confirmation',
|
||||
'completed' => 'Ready',
|
||||
'needs_preparation' => 'Needs attention',
|
||||
'blocked' => 'Blocked',
|
||||
'executing' => 'Running',
|
||||
'failed',
|
||||
'cancelled' => 'Failed',
|
||||
'historical' => 'Historical',
|
||||
default => 'Unknown',
|
||||
};
|
||||
|
||||
return $readinessGuidance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
@ -1076,7 +1104,7 @@ private static function restoreWizardBackupQualityCard(?BackupQualitySummary $ba
|
||||
if (! $backupQuality instanceof BackupQualitySummary) {
|
||||
return [
|
||||
'available' => false,
|
||||
'status' => 'Select a backup set to inspect input quality.',
|
||||
'status' => 'Not configured',
|
||||
'summary' => 'Backup quality hints describe input strength only.',
|
||||
'nextAction' => 'Select a backup set to inspect item counts and degradations.',
|
||||
'positiveClaimBoundary' => 'Input quality signals do not prove that execution is safe or that recovery is verified.',
|
||||
@ -1085,10 +1113,10 @@ private static function restoreWizardBackupQualityCard(?BackupQualitySummary $ba
|
||||
}
|
||||
|
||||
$status = match (true) {
|
||||
$backupQuality->totalItems === 0 => 'No captured items',
|
||||
! $hasUsableSource => 'Degraded input',
|
||||
$backupQuality->degradedItemCount > 0 => 'Degraded input',
|
||||
default => 'Available',
|
||||
$backupQuality->totalItems === 0 => 'Blocked',
|
||||
! $hasUsableSource => 'Needs attention',
|
||||
$backupQuality->degradedItemCount > 0 => 'Needs attention',
|
||||
default => 'Ready',
|
||||
};
|
||||
|
||||
$summary = match (true) {
|
||||
|
||||
@ -81,25 +81,23 @@
|
||||
</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>
|
||||
<details data-testid="restore-run-confirm-readiness-detail" class="rounded-lg border border-gray-200 bg-white px-4 py-3 text-sm shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer font-medium text-gray-950 dark:text-white">
|
||||
View readiness context
|
||||
</summary>
|
||||
|
||||
<div class="mt-3 space-y-2">
|
||||
<p class="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="text-xs font-semibold text-gray-700 dark:text-gray-200">
|
||||
Suggested next step: {{ $decisionCard['nextAction'] ?? ($readinessGuidance['nextActionLabel'] ?? 'Review the current restore state.') }}
|
||||
</p>
|
||||
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $readinessGuidance['actionSafetyCopy'] ?? 'This guidance does not execute the restore.' }}
|
||||
</p>
|
||||
</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>
|
||||
</details>
|
||||
|
||||
<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.
|
||||
|
||||
@ -57,6 +57,25 @@
|
||||
$blockingCount = (int) ($validationState['blockingCount'] ?? 0);
|
||||
$warningCount = (int) ($validationState['warningCount'] ?? 0);
|
||||
$nextGate = (string) ($wizardGateState['next_gate_label'] ?? ($previewState['nextGateLabel'] ?? ($processFlowState['nextGate'] ?? 'Unavailable')));
|
||||
$visibleChangedDiffs = array_slice($changedDiffs, 0, 8);
|
||||
$hiddenChangedCount = max(0, count($changedDiffs) - count($visibleChangedDiffs));
|
||||
$visibleUnchangedDiffs = array_slice($unchangedDiffs, 0, 8);
|
||||
$hiddenUnchangedCount = max(0, count($unchangedDiffs) - count($visibleUnchangedDiffs));
|
||||
$visibleAllReviewedDiffs = array_slice($allReviewedDiffs, 0, 8);
|
||||
$hiddenAllReviewedCount = max(0, count($allReviewedDiffs) - count($visibleAllReviewedDiffs));
|
||||
$previewMetricCards = [
|
||||
['label' => 'Policies changed', 'value' => (string) $reportedChangedCount],
|
||||
['label' => 'Requires review', 'value' => (string) $requiresReviewCount],
|
||||
['label' => 'Validation blockers', 'value' => (string) $blockingCount],
|
||||
['label' => 'Next gate', 'value' => $nextGate],
|
||||
];
|
||||
$secondaryMetricCards = [
|
||||
['label' => 'Policies reviewed', 'value' => $reviewedCount.' '.\Illuminate\Support\Str::plural('policy', $reviewedCount).' reviewed'],
|
||||
['label' => 'Policies unchanged', 'value' => (string) $unchangedCount],
|
||||
['label' => 'Assignments changed', 'value' => (string) $assignmentsChanged],
|
||||
['label' => 'Scope tags changed', 'value' => (string) $scopeTagsChanged],
|
||||
['label' => 'Validation warnings', 'value' => (string) $warningCount],
|
||||
];
|
||||
|
||||
$gateBadgeTone = static function (string $status): string {
|
||||
return match ($status) {
|
||||
@ -244,62 +263,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Policies reviewed</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $reviewedCount }} {{ \Illuminate\Support\Str::plural('policy', $reviewedCount) }} reviewed
|
||||
<div data-testid="restore-run-preview-primary-metrics" class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@foreach ($previewMetricCards as $metric)
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $metric['label'] }}</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $metric['value'] }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Policies changed</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $reportedChangedCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Policies unchanged</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $unchangedCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Requires review</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $requiresReviewCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Assignments changed</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $assignmentsChanged }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Scope tags changed</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $scopeTagsChanged }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Validation blockers</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $blockingCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Validation warnings</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $warningCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Next gate</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $nextGate }}
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<details data-testid="restore-run-preview-secondary-metrics" class="rounded-lg border border-gray-200 bg-white px-4 py-3 text-sm shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer font-semibold text-gray-950 dark:text-white">
|
||||
View secondary preview counts
|
||||
</summary>
|
||||
|
||||
<div class="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@foreach ($secondaryMetricCards as $metric)
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $metric['label'] }}</div>
|
||||
<div class="mt-1 text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $metric['value'] }}
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
@ -347,7 +337,7 @@
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
<div data-testid="restore-run-preview-review-cards" data-preview-group="changed" class="space-y-3 md:hidden">
|
||||
@foreach ($changedDiffs as $entry)
|
||||
@foreach ($visibleChangedDiffs as $entry)
|
||||
@php
|
||||
$entry = is_array($entry) ? $entry : [];
|
||||
$diffSummary = $policyDiffSummary($entry);
|
||||
@ -433,7 +423,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
@foreach ($changedDiffs as $entry)
|
||||
@foreach ($visibleChangedDiffs as $entry)
|
||||
@php
|
||||
$entry = is_array($entry) ? $entry : [];
|
||||
$diffSummary = $policyDiffSummary($entry);
|
||||
@ -485,6 +475,12 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if ($hiddenChangedCount > 0)
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300">
|
||||
{{ $hiddenChangedCount }} additional {{ \Illuminate\Support\Str::plural('changed policy', $hiddenChangedCount) }} kept in detailed review.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@elseif ($policiesTotal > 0)
|
||||
<div class="space-y-2">
|
||||
@ -503,17 +499,13 @@
|
||||
@endif
|
||||
|
||||
@if ($unchangedDiffs !== [])
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
No changes detected
|
||||
</div>
|
||||
<x-filament::badge color="success" size="sm">
|
||||
{{ $unchangedCount }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
<details data-testid="restore-run-preview-unchanged-disclosure" class="rounded-lg border border-gray-200 bg-white px-4 py-3 text-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer font-semibold text-gray-950 dark:text-white">
|
||||
View unchanged policies ({{ $unchangedCount }})
|
||||
</summary>
|
||||
|
||||
<div data-testid="restore-run-preview-review-cards" data-preview-group="unchanged" class="space-y-3 md:hidden">
|
||||
@foreach ($unchangedDiffs as $entry)
|
||||
@foreach ($visibleUnchangedDiffs as $entry)
|
||||
@php
|
||||
$entry = is_array($entry) ? $entry : [];
|
||||
$diffSummary = $policyDiffSummary($entry);
|
||||
@ -597,7 +589,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
@foreach ($unchangedDiffs as $entry)
|
||||
@foreach ($visibleUnchangedDiffs as $entry)
|
||||
@php
|
||||
$entry = is_array($entry) ? $entry : [];
|
||||
$diffSummary = $policyDiffSummary($entry);
|
||||
@ -647,7 +639,13 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($hiddenUnchangedCount > 0)
|
||||
<div class="mt-3 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300">
|
||||
{{ $hiddenUnchangedCount }} additional unchanged {{ \Illuminate\Support\Str::plural('policy', $hiddenUnchangedCount) }} kept in detailed review.
|
||||
</div>
|
||||
@endif
|
||||
</details>
|
||||
@elseif ($policiesTotal > 0)
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
@ -667,10 +665,10 @@
|
||||
@if ($allReviewedDiffs !== [])
|
||||
<details class="rounded-lg border border-gray-200 bg-white px-4 py-3 text-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer font-semibold text-gray-950 dark:text-white">
|
||||
All reviewed items
|
||||
View all reviewed items ({{ $reviewedCount }})
|
||||
</summary>
|
||||
<div data-testid="restore-run-preview-review-cards" data-preview-group="all" class="mt-3 space-y-3 md:hidden">
|
||||
@foreach ($allReviewedDiffs as $entry)
|
||||
@foreach ($visibleAllReviewedDiffs as $entry)
|
||||
@php
|
||||
$entry = is_array($entry) ? $entry : [];
|
||||
$isUnchanged = in_array($entry, $unchangedDiffs, true);
|
||||
@ -743,7 +741,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
@foreach ($allReviewedDiffs as $entry)
|
||||
@foreach ($visibleAllReviewedDiffs as $entry)
|
||||
@php
|
||||
$entry = is_array($entry) ? $entry : [];
|
||||
$isUnchanged = in_array($entry, $unchangedDiffs, true);
|
||||
@ -796,11 +794,17 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if ($hiddenAllReviewedCount > 0)
|
||||
<div class="mt-3 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300">
|
||||
{{ $hiddenAllReviewedCount }} additional reviewed {{ \Illuminate\Support\Str::plural('item', $hiddenAllReviewedCount) }} kept in detailed review.
|
||||
</div>
|
||||
@endif
|
||||
</details>
|
||||
@elseif ($policiesTotal > 0)
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
All reviewed items
|
||||
Reviewed item detail
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300">
|
||||
The preview summary has no item rows to show.
|
||||
@ -811,43 +815,15 @@
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::section>
|
||||
<div data-testid="restore-run-preview-evidence" class="space-y-4">
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-base font-semibold text-gray-950 dark:text-white">
|
||||
Preview evidence
|
||||
</h2>
|
||||
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
Gate status and restore proof remain available after preview without competing with preview review.
|
||||
</p>
|
||||
</div>
|
||||
<details data-testid="restore-run-preview-evidence" class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
View safety gates and restore proof
|
||||
</summary>
|
||||
|
||||
<dl class="grid gap-2 sm:grid-cols-3 lg:min-w-[34rem]">
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">Gates</dt>
|
||||
<dd class="mt-0.5 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $processFlowState['gatesComplete'] ?? 0 }}/{{ $processFlowState['gatesTotal'] ?? count($processFlowSteps) }} complete
|
||||
</dd>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">Next gate</dt>
|
||||
<dd class="mt-0.5 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $nextGate }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">Execution</dt>
|
||||
<dd class="mt-0.5 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $wizardGateState['execution_label'] ?? ($processFlowState['executionLabel'] ?? 'Unavailable') }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<details data-testid="restore-run-preview-evidence-details" class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
View safety gates and restore proof
|
||||
</summary>
|
||||
<div class="mt-4 space-y-4">
|
||||
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
Gate status and restore proof remain available after preview without competing with preview review.
|
||||
</p>
|
||||
|
||||
<div class="mt-4 grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(18rem,0.8fr)]">
|
||||
<div class="space-y-3">
|
||||
@ -928,8 +904,8 @@ class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-8
|
||||
</details>
|
||||
</aside>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
|
||||
@ -104,63 +104,46 @@
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<x-filament::section>
|
||||
<div data-testid="restore-run-decision-card" class="grid gap-5 xl:grid-cols-[minmax(0,1fr)_20rem]">
|
||||
<div class="min-w-0 space-y-4">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<h2 class="text-base font-semibold text-gray-950 dark:text-white">
|
||||
{{ $decisionCard['title'] ?? 'Restore Safety' }}
|
||||
</h2>
|
||||
<span class="inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-semibold {{ $statusBadgeClasses((string) ($decisionCard['tone'] ?? 'gray')) }}">
|
||||
{{ $decisionCard['status'] ?? 'Unavailable' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<dl class="grid gap-3 md:grid-cols-3">
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $decisionCard['statusLabel'] ?? 'Status' }}</dt>
|
||||
<dd class="mt-1 text-sm font-medium text-gray-950 dark:text-white">{{ $decisionCard['status'] ?? 'Unavailable' }}</dd>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $decisionCard['reasonLabel'] ?? 'Reason' }}</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 dark:text-gray-200">{{ $decisionCard['reason'] ?? 'Restore safety reason is unavailable.' }}</dd>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $decisionCard['impactLabel'] ?? 'Impact' }}</dt>
|
||||
<dd class="mt-1 text-sm text-gray-700 dark:text-gray-200">{{ $decisionCard['impact'] ?? 'Restore impact is unavailable.' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div data-testid="restore-run-decision-card" class="min-w-0 space-y-4">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<h2 class="text-base font-semibold text-gray-950 dark:text-white">
|
||||
{{ $decisionCard['title'] ?? 'Restore Safety' }}
|
||||
</h2>
|
||||
<span class="inline-flex max-w-full items-center rounded-full border px-2.5 py-1 text-left text-xs font-semibold leading-5 whitespace-normal break-words {{ $statusBadgeClasses((string) ($decisionCard['tone'] ?? 'gray')) }}">
|
||||
{{ $decisionCard['status'] ?? 'Unavailable' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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="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">
|
||||
{{ $decisionCard['helperText'] }}
|
||||
</p>
|
||||
@endif
|
||||
<div class="rounded-lg border border-primary-200 bg-primary-50/70 px-4 py-3 shadow-sm dark:border-primary-800 dark:bg-primary-950/20">
|
||||
<div class="text-xs font-semibold text-primary-700 dark:text-primary-300">
|
||||
{{ $decisionCard['nextActionLabel'] ?? 'Primary next action' }}
|
||||
</div>
|
||||
</aside>
|
||||
<div data-testid="restore-run-decision-next-action" class="mt-1 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $decisionCard['nextAction'] ?? ($readinessGuidance['nextActionLabel'] ?? 'Review the current restore state.') }}
|
||||
</div>
|
||||
|
||||
<dl class="mt-3 space-y-2 border-t border-primary-200 pt-3 dark:border-primary-800">
|
||||
<div class="grid gap-1 sm:grid-cols-[6rem_minmax(0,1fr)] sm:gap-3">
|
||||
<dt class="text-xs font-semibold text-primary-700 dark:text-primary-300">{{ $decisionCard['reasonLabel'] ?? 'Reason' }}</dt>
|
||||
<dd class="text-xs leading-5 text-gray-700 dark:text-gray-200">{{ $decisionCard['reason'] ?? 'Restore safety reason is unavailable.' }}</dd>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-1 sm:grid-cols-[6rem_minmax(0,1fr)] sm:gap-3">
|
||||
<dt class="text-xs font-semibold text-primary-700 dark:text-primary-300">{{ $decisionCard['impactLabel'] ?? 'Impact' }}</dt>
|
||||
<dd class="text-xs leading-5 text-gray-700 dark:text-gray-200">{{ $decisionCard['impact'] ?? 'Restore impact is unavailable.' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<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-1 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $decisionCard['helperText'] }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</x-dynamic-component>
|
||||
|
||||
@ -44,8 +44,12 @@
|
||||
|
||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||
<x-filament::section>
|
||||
<div data-testid="restore-run-safety-evidence" class="space-y-4">
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<details data-testid="restore-run-safety-evidence" class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $evidenceDetailsLabel }}
|
||||
</summary>
|
||||
|
||||
<div class="mt-4 space-y-4">
|
||||
<div class="space-y-1">
|
||||
<h2 class="text-base font-semibold text-gray-950 dark:text-white">
|
||||
{{ $evidenceTitle }}
|
||||
@ -55,33 +59,6 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<dl class="grid gap-2 sm:grid-cols-3 lg:min-w-[34rem]">
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">Gates</dt>
|
||||
<dd class="mt-0.5 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $processFlow['gatesComplete'] ?? 0 }}/{{ $processFlow['gatesTotal'] ?? count($steps) }} complete
|
||||
</dd>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">Next gate</dt>
|
||||
<dd class="mt-0.5 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $wizardGate['next_gate_label'] ?? ($processFlow['nextGate'] ?? 'Unavailable') }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">Execution</dt>
|
||||
<dd class="mt-0.5 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $wizardGate['execution_label'] ?? ($processFlow['executionLabel'] ?? 'Unavailable') }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<details class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $evidenceDetailsLabel }}
|
||||
</summary>
|
||||
|
||||
<div class="mt-4 grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(18rem,0.8fr)]">
|
||||
<div class="space-y-3">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
@ -100,13 +77,13 @@
|
||||
data-step-status="{{ $status }}"
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-800 dark:bg-gray-950/50"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0 space-y-1 sm:flex-1">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-gray-200 bg-white text-xs font-semibold text-gray-600 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300">
|
||||
{{ $step['step'] ?? '•' }}
|
||||
</span>
|
||||
<span class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
<span class="min-w-0 text-sm font-semibold text-gray-950 break-words dark:text-white">
|
||||
{{ $step['label'] ?? 'Gate' }}
|
||||
</span>
|
||||
</div>
|
||||
@ -115,9 +92,11 @@ class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-8
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<x-filament::badge :color="$badgeTone($status)" size="sm">
|
||||
{{ $badgeLabel($status) }}
|
||||
</x-filament::badge>
|
||||
<div class="self-start">
|
||||
<x-filament::badge :color="$badgeTone($status)" size="sm">
|
||||
{{ $badgeLabel($status) }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
@ -135,8 +114,8 @@ class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-8
|
||||
data-proof-label="{{ $item['label'] ?? 'Proof item' }}"
|
||||
class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-800 dark:bg-gray-950/50"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="min-w-0 space-y-1 sm:flex-1">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $item['label'] ?? 'Proof item' }}
|
||||
</div>
|
||||
@ -144,7 +123,7 @@ class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-8
|
||||
{{ $item['description'] ?? '' }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="inline-flex shrink-0 items-center rounded-full border px-2 py-0.5 text-xs font-semibold {{ $proofBadgeClasses((string) ($item['tone'] ?? 'gray')) }}">
|
||||
<span class="inline-flex max-w-full shrink-0 items-center self-start rounded-full border px-2 py-0.5 text-left text-xs font-semibold leading-5 whitespace-normal break-words {{ $proofBadgeClasses((string) ($item['tone'] ?? 'gray')) }}">
|
||||
{{ $item['value'] ?? 'Unavailable' }}
|
||||
</span>
|
||||
</div>
|
||||
@ -161,7 +140,7 @@ class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-8
|
||||
</details>
|
||||
</aside>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</x-filament::section>
|
||||
</x-dynamic-component>
|
||||
|
||||
@ -79,6 +79,28 @@
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="rounded-lg border border-primary-200 bg-primary-50/70 px-4 py-3 shadow-sm dark:border-primary-800 dark:bg-primary-950/20">
|
||||
<div class="text-xs font-semibold text-primary-700 dark:text-primary-300">{{ $decisionCard['nextActionLabel'] ?? 'Next action' }}</div>
|
||||
|
||||
@if (filled($decisionCard['actionUrl'] ?? null))
|
||||
<x-filament::button class="mt-2 w-full justify-center sm:w-auto" tag="a" :href="$decisionCard['actionUrl']" size="sm">
|
||||
{{ $decisionCard['actionLabel'] ?? 'Review compare state' }}
|
||||
</x-filament::button>
|
||||
@elseif (($decisionCard['actionName'] ?? null) === 'compareNow' && ! (bool) ($decisionCard['actionDisabled'] ?? true))
|
||||
<x-filament::button data-testid="baseline-compare-primary-action" class="mt-2 w-full justify-center sm:w-auto" type="button" wire:click="mountAction('compareNow')" size="sm">
|
||||
{{ $decisionCard['actionLabel'] ?? 'Compare now' }}
|
||||
</x-filament::button>
|
||||
@else
|
||||
<x-filament::button class="mt-2 w-full justify-center sm:w-auto" color="gray" size="sm" disabled>
|
||||
{{ $decisionCard['actionLabel'] ?? 'Review compare state' }}
|
||||
</x-filament::button>
|
||||
@endif
|
||||
|
||||
@if (filled($decisionCard['helperText'] ?? null))
|
||||
<p class="mt-2 text-xs leading-5 text-gray-600 dark:text-gray-300">{{ $decisionCard['helperText'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (! empty($decisionSummaryItems))
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
@foreach ($decisionSummaryItems as $item)
|
||||
@ -95,60 +117,44 @@
|
||||
<aside data-testid="baseline-compare-proof-panel" class="min-w-0 space-y-3">
|
||||
<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['evidenceLabel'] ?? 'Evidence path' }}</div>
|
||||
<p class="mt-1 text-sm leading-6 text-gray-700 dark:text-gray-200">{{ $decisionCard['evidence'] ?? 'Operation proof unavailable' }}</p>
|
||||
|
||||
<div class="mt-4 border-t border-gray-200 pt-4 dark:border-gray-800">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $decisionCard['nextActionLabel'] ?? 'Next action' }}</div>
|
||||
|
||||
@if (filled($decisionCard['actionUrl'] ?? null))
|
||||
<x-filament::button class="mt-2 w-full justify-center" tag="a" :href="$decisionCard['actionUrl']" size="sm">
|
||||
{{ $decisionCard['actionLabel'] ?? 'Review compare state' }}
|
||||
</x-filament::button>
|
||||
@elseif (($decisionCard['actionName'] ?? null) === 'compareNow' && ! (bool) ($decisionCard['actionDisabled'] ?? true))
|
||||
<x-filament::button class="mt-2 w-full justify-center" type="button" wire:click="mountAction('compareNow')" size="sm">
|
||||
{{ $decisionCard['actionLabel'] ?? 'Compare now' }}
|
||||
</x-filament::button>
|
||||
@else
|
||||
<x-filament::button class="mt-2 w-full justify-center" color="gray" size="sm" disabled>
|
||||
{{ $decisionCard['actionLabel'] ?? 'Review compare state' }}
|
||||
</x-filament::button>
|
||||
@endif
|
||||
|
||||
@if (filled($decisionCard['helperText'] ?? null))
|
||||
<p class="mt-2 text-xs leading-5 text-gray-500 dark:text-gray-400">{{ $decisionCard['helperText'] }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<p class="mt-1 text-sm leading-6 text-gray-700 dark:text-gray-200">{{ $decisionCard['evidence'] ?? 'Evidence unavailable' }}</p>
|
||||
</div>
|
||||
|
||||
@foreach ($proofPanelItems as $proof)
|
||||
<div data-testid="baseline-compare-proof-item" data-proof-key="{{ $proof['key'] }}" class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ $proof['label'] }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span data-testid="baseline-compare-status-badge" class="{{ $baselineCompareStatusBadgeClasses($proof['tone'] ?? 'gray') }}">
|
||||
{{ $proof['value'] }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">{{ $proof['description'] }}</p>
|
||||
</div>
|
||||
|
||||
@if (filled($proof['actionLabel'] ?? null) && filled($proof['actionUrl'] ?? null))
|
||||
<x-filament::button class="mt-3" tag="a" :href="$proof['actionUrl']" size="sm" color="gray">
|
||||
{{ $proof['actionLabel'] }}
|
||||
</x-filament::button>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<details data-testid="baseline-compare-diagnostics" class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<details data-testid="baseline-compare-technical-details" class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $diagnosticsDisclosure['label'] ?? 'Diagnostics - Collapsed' }}
|
||||
Technical details and audit links
|
||||
</summary>
|
||||
<p class="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ $diagnosticsDisclosure['summary'] ?? 'Support diagnostics stay closed until needed.' }}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
@foreach ($proofPanelItems as $proof)
|
||||
<div data-testid="baseline-compare-proof-item" data-proof-key="{{ $proof['key'] }}" class="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ $proof['label'] }}</div>
|
||||
<div>
|
||||
<span data-testid="baseline-compare-status-badge" class="{{ $baselineCompareStatusBadgeClasses($proof['tone'] ?? 'gray') }}">
|
||||
{{ $proof['value'] }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">{{ $proof['description'] }}</p>
|
||||
</div>
|
||||
|
||||
@if (filled($proof['actionLabel'] ?? null) && filled($proof['actionUrl'] ?? null))
|
||||
<x-filament::button class="mt-3" tag="a" :href="$proof['actionUrl']" size="sm" color="gray">
|
||||
{{ $proof['actionLabel'] }}
|
||||
</x-filament::button>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<details data-testid="baseline-compare-diagnostics" class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ $diagnosticsDisclosure['label'] ?? 'Diagnostics - Collapsed' }}
|
||||
</summary>
|
||||
<p class="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ $diagnosticsDisclosure['summary'] ?? 'Support diagnostics stay closed until needed.' }}
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
</details>
|
||||
</aside>
|
||||
</div>
|
||||
@ -175,84 +181,96 @@
|
||||
|
||||
@if (! empty($compareReadinessFlow))
|
||||
<x-filament::section>
|
||||
<div class="space-y-5">
|
||||
@include('filament.components.product-process-flow-horizontal', [
|
||||
'title' => 'Compare readiness flow',
|
||||
'subtitle' => 'Baseline comparison needs an assigned baseline, linked snapshots, a compare run, and a decision output.',
|
||||
'ariaLabel' => 'Compare readiness pipeline',
|
||||
'steps' => $compareReadinessFlow,
|
||||
'flowTestId' => 'baseline-compare-readiness-flow',
|
||||
'stepTestId' => 'baseline-compare-readiness-step',
|
||||
'connectorTestId' => 'baseline-compare-readiness-connector',
|
||||
'badgeTestId' => 'baseline-compare-status-badge',
|
||||
'statusBadgeClasses' => $baselineCompareStatusBadgeClasses,
|
||||
])
|
||||
<details data-testid="baseline-compare-readiness-disclosure" class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
View compare readiness details
|
||||
</summary>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_18rem]">
|
||||
<div data-testid="baseline-compare-available-inputs" class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Available inputs
|
||||
</div>
|
||||
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
Repo-backed inputs only.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 space-y-5">
|
||||
@include('filament.components.product-process-flow-horizontal', [
|
||||
'title' => 'Compare readiness flow',
|
||||
'subtitle' => 'Baseline comparison needs an assigned baseline, linked snapshots, a compare run, and a decision output.',
|
||||
'ariaLabel' => 'Compare readiness pipeline',
|
||||
'steps' => $compareReadinessFlow,
|
||||
'flowTestId' => 'baseline-compare-readiness-flow',
|
||||
'stepTestId' => 'baseline-compare-readiness-step',
|
||||
'connectorTestId' => 'baseline-compare-readiness-connector',
|
||||
'badgeTestId' => 'baseline-compare-status-badge',
|
||||
'statusBadgeClasses' => $baselineCompareStatusBadgeClasses,
|
||||
])
|
||||
|
||||
<div class="mt-3 overflow-hidden rounded-md border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900/80">
|
||||
@foreach ($availableCompareInputs as $input)
|
||||
<div data-testid="baseline-compare-available-input" data-input-label="{{ $input['label'] }}" class="grid min-w-0 gap-2 border-b border-gray-200 px-3 py-2.5 last:border-b-0 dark:border-gray-800 sm:grid-cols-[minmax(8rem,1fr)_8rem_minmax(0,1.7fr)] sm:items-start">
|
||||
<div class="min-w-0 text-sm font-medium leading-5 text-gray-950 dark:text-white">
|
||||
<span class="break-words">
|
||||
{{ $input['label'] }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span data-testid="baseline-compare-status-badge" class="{{ $baselineCompareStatusBadgeClasses($input['tone'] ?? 'gray') }}">
|
||||
{{ $input['state'] }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="min-w-0 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $input['description'] }}
|
||||
</p>
|
||||
<div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_18rem]">
|
||||
<div data-testid="baseline-compare-available-inputs" class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-800 dark:bg-gray-950/50">
|
||||
<div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Available inputs
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (! empty($assignmentUnlocks))
|
||||
<div data-testid="baseline-compare-assignment-unlocks" class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
What this unlocks after assignment
|
||||
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
Repo-backed inputs only.
|
||||
</p>
|
||||
</div>
|
||||
<ul class="mt-3 space-y-2">
|
||||
@foreach ($assignmentUnlocks as $unlock)
|
||||
<li class="flex gap-2 text-xs leading-5 text-gray-600 dark:text-gray-300">
|
||||
<x-heroicon-m-arrow-right-circle class="mt-0.5 h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500" />
|
||||
<span>{{ $unlock }}</span>
|
||||
</li>
|
||||
|
||||
<div class="mt-3 overflow-hidden rounded-md border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900/80">
|
||||
@foreach ($availableCompareInputs as $input)
|
||||
<div data-testid="baseline-compare-available-input" data-input-label="{{ $input['label'] }}" class="grid min-w-0 gap-2 border-b border-gray-200 px-3 py-2.5 last:border-b-0 dark:border-gray-800 sm:grid-cols-[minmax(8rem,1fr)_8rem_minmax(0,1.7fr)] sm:items-start">
|
||||
<div class="min-w-0 text-sm font-medium leading-5 text-gray-950 dark:text-white">
|
||||
<span class="break-words">
|
||||
{{ $input['label'] }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span data-testid="baseline-compare-status-badge" class="{{ $baselineCompareStatusBadgeClasses($input['tone'] ?? 'gray') }}">
|
||||
{{ $input['state'] }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="min-w-0 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $input['description'] }}
|
||||
</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (! empty($assignmentUnlocks))
|
||||
<div data-testid="baseline-compare-assignment-unlocks" class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
What this unlocks after assignment
|
||||
</div>
|
||||
<ul class="mt-3 space-y-2">
|
||||
@foreach ($assignmentUnlocks as $unlock)
|
||||
<li class="flex gap-2 text-xs leading-5 text-gray-600 dark:text-gray-300">
|
||||
<x-heroicon-m-arrow-right-circle class="mt-0.5 h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500" />
|
||||
<span>{{ $unlock }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@if (filled($openCompareMatrixUrl ?? null))
|
||||
<x-filament::section>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Launch the compare matrix with the currently known baseline profile and any carried subject focus from this environment landing.
|
||||
</div>
|
||||
<details data-testid="baseline-compare-matrix-disclosure" class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
View compare matrix path
|
||||
</summary>
|
||||
|
||||
<x-filament::button tag="a" :href="$openCompareMatrixUrl" color="gray" icon="heroicon-o-squares-2x2">
|
||||
Open compare matrix
|
||||
</x-filament::button>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Launch the compare matrix with the currently known baseline profile and any carried subject focus from this environment landing.
|
||||
</div>
|
||||
|
||||
<x-filament::button tag="a" :href="$openCompareMatrixUrl" color="gray" icon="heroicon-o-squares-2x2">
|
||||
Open compare matrix
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</details>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@ -300,50 +318,55 @@
|
||||
|
||||
@if ($showCompareExplanation)
|
||||
<x-filament::section>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-wrap items-start gap-2">
|
||||
@if ($summary)
|
||||
<x-filament::badge :color="$summary['tone'] ?? 'gray'" size="sm">
|
||||
{{ $summaryLabel }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
<details data-testid="baseline-compare-explanation-disclosure" class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
View compare explanation details
|
||||
</summary>
|
||||
|
||||
@if ($evaluationSpec && $evaluationSpec->label !== 'Unknown')
|
||||
<x-filament::badge :color="$evaluationSpec->color" :icon="$evaluationSpec->icon" size="sm">
|
||||
{{ $evaluationSpec->label }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
<div class="mt-4 space-y-4">
|
||||
<div class="flex flex-wrap items-start gap-2">
|
||||
@if ($summary)
|
||||
<x-filament::badge :color="$summary['tone'] ?? 'gray'" size="sm">
|
||||
{{ $summaryLabel }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
|
||||
@if ($trustSpec && $trustSpec->label !== 'Unknown')
|
||||
<x-filament::badge :color="$trustSpec->color" :icon="$trustSpec->icon" size="sm">
|
||||
{{ $trustSpec->label }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
@if ($evaluationSpec && $evaluationSpec->label !== 'Unknown')
|
||||
<x-filament::badge :color="$evaluationSpec->color" :icon="$evaluationSpec->icon" size="sm">
|
||||
{{ $evaluationSpec->label }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="text-lg font-semibold text-gray-950 dark:text-white">
|
||||
{{ $summary['headline'] ?? ($explanation['headline'] ?? 'Compare explanation') }}
|
||||
@if ($trustSpec && $trustSpec->label !== 'Unknown')
|
||||
<x-filament::badge :color="$trustSpec->color" :icon="$trustSpec->icon" size="sm">
|
||||
{{ $trustSpec->label }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (filled($summary['supportingMessage'] ?? null))
|
||||
<p class="text-sm text-gray-700 dark:text-gray-200">
|
||||
{{ $summary['supportingMessage'] }}
|
||||
</p>
|
||||
@endif
|
||||
<div class="space-y-2">
|
||||
<div class="text-lg font-semibold text-gray-950 dark:text-white">
|
||||
{{ $summary['headline'] ?? ($explanation['headline'] ?? 'Compare explanation') }}
|
||||
</div>
|
||||
|
||||
@if (filled($explanation['reliabilityStatement'] ?? null))
|
||||
<p class="text-sm text-gray-700 dark:text-gray-200">
|
||||
{{ $explanation['reliabilityStatement'] }}
|
||||
</p>
|
||||
@endif
|
||||
@if (filled($summary['supportingMessage'] ?? null))
|
||||
<p class="text-sm text-gray-700 dark:text-gray-200">
|
||||
{{ $summary['supportingMessage'] }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
@if (filled(data_get($explanation, 'dominantCause.explanation')))
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ data_get($explanation, 'dominantCause.explanation') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@if (filled($explanation['reliabilityStatement'] ?? null))
|
||||
<p class="text-sm text-gray-700 dark:text-gray-200">
|
||||
{{ $explanation['reliabilityStatement'] }}
|
||||
</p>
|
||||
@endif
|
||||
|
||||
@if (filled(data_get($explanation, 'dominantCause.explanation')))
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ data_get($explanation, 'dominantCause.explanation') }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($reasonSemantics !== null)
|
||||
<dl class="grid gap-3 md:grid-cols-2">
|
||||
@ -414,17 +437,23 @@
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@if ($hasRbacRoleDefinitionSummary)
|
||||
<x-filament::section :heading="__('baseline-compare.rbac_summary_title')">
|
||||
<x-slot name="description">
|
||||
{{ __('baseline-compare.rbac_summary_description') }}
|
||||
</x-slot>
|
||||
<x-filament::section>
|
||||
<details data-testid="baseline-compare-rbac-summary-disclosure" class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ __('baseline-compare.rbac_summary_title') }}
|
||||
</summary>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<p class="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ __('baseline-compare.rbac_summary_description') }}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 flex flex-wrap items-center gap-3">
|
||||
<x-filament::badge color="gray">
|
||||
{{ __('baseline-compare.rbac_summary_compared') }}: {{ (int) ($rbacRoleDefinitionSummary['total_compared'] ?? 0) }}
|
||||
</x-filament::badge>
|
||||
@ -441,6 +470,7 @@
|
||||
{{ __('baseline-compare.rbac_summary_unexpected') }}: {{ (int) ($rbacRoleDefinitionSummary['unexpected'] ?? 0) }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
</details>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@ -471,20 +501,6 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@if ($this->getRunUrl())
|
||||
<div class="mt-2">
|
||||
<x-filament::button
|
||||
:href="$this->getRunUrl()"
|
||||
tag="a"
|
||||
color="warning"
|
||||
outlined
|
||||
icon="heroicon-o-queue-list"
|
||||
size="sm"
|
||||
>
|
||||
{{ __('baseline-compare.button_view_run') }}
|
||||
</x-filament::button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -502,20 +518,6 @@
|
||||
<div class="text-sm text-danger-700 dark:text-danger-300">
|
||||
{{ $failureReason ?? __('baseline-compare.failed_body_default') }}
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-3">
|
||||
@if ($this->getRunUrl())
|
||||
<x-filament::button
|
||||
:href="$this->getRunUrl()"
|
||||
tag="a"
|
||||
color="danger"
|
||||
outlined
|
||||
icon="heroicon-o-queue-list"
|
||||
size="sm"
|
||||
>
|
||||
{{ __('baseline-compare.button_view_failed_run') }}
|
||||
</x-filament::button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -543,16 +545,24 @@
|
||||
@endif
|
||||
|
||||
@if ($hasEvidenceGapDetailSection)
|
||||
<x-filament::section :heading="__('baseline-compare.evidence_gap_details_heading')">
|
||||
<x-slot name="description">
|
||||
{{ __('baseline-compare.evidence_gap_details_description') }}
|
||||
</x-slot>
|
||||
<x-filament::section>
|
||||
<details data-testid="baseline-compare-evidence-gap-detail-disclosure" class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ __('baseline-compare.evidence_gap_details_heading') }}
|
||||
</summary>
|
||||
|
||||
@include('filament.infolists.entries.evidence-gap-subjects', [
|
||||
'summary' => $evidenceGapSummary,
|
||||
'buckets' => $evidenceGapBuckets ?? [],
|
||||
'searchId' => 'tenant-baseline-compare-gap-search',
|
||||
])
|
||||
<p class="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ __('baseline-compare.evidence_gap_details_description') }}
|
||||
</p>
|
||||
|
||||
<div class="mt-4">
|
||||
@include('filament.infolists.entries.evidence-gap-subjects', [
|
||||
'summary' => $evidenceGapSummary,
|
||||
'buckets' => $evidenceGapBuckets ?? [],
|
||||
'searchId' => 'tenant-baseline-compare-gap-search',
|
||||
])
|
||||
</div>
|
||||
</details>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@ -587,45 +597,25 @@
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
@if ($this->getFindingsUrl())
|
||||
<x-filament::button
|
||||
:href="$this->getFindingsUrl()"
|
||||
tag="a"
|
||||
color="gray"
|
||||
icon="heroicon-o-eye"
|
||||
size="sm"
|
||||
>
|
||||
{{ __('baseline-compare.button_view_findings') }}
|
||||
</x-filament::button>
|
||||
@endif
|
||||
|
||||
@if ($this->getRunUrl())
|
||||
<x-filament::button
|
||||
:href="$this->getRunUrl()"
|
||||
tag="a"
|
||||
color="gray"
|
||||
outlined
|
||||
icon="heroicon-o-queue-list"
|
||||
size="sm"
|
||||
>
|
||||
{{ __('baseline-compare.button_review_last_run') }}
|
||||
</x-filament::button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
@if ($hasEvidenceGapDiagnostics)
|
||||
<x-filament::section :heading="__('baseline-compare.evidence_gap_diagnostics_heading')">
|
||||
<x-slot name="description">
|
||||
{{ __('baseline-compare.evidence_gap_diagnostics_description') }}
|
||||
</x-slot>
|
||||
<x-filament::section>
|
||||
<details data-testid="baseline-compare-evidence-gap-diagnostics-disclosure" class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/80">
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ __('baseline-compare.evidence_gap_diagnostics_heading') }}
|
||||
</summary>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-600 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300">
|
||||
Diagnostics are available for support review and stay outside the default operator view.
|
||||
</div>
|
||||
<p class="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ __('baseline-compare.evidence_gap_diagnostics_description') }}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-600 dark:border-gray-800 dark:bg-gray-950/50 dark:text-gray-300">
|
||||
Diagnostics are available for support review and stay outside the default operator view.
|
||||
</div>
|
||||
</details>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
</x-filament::page>
|
||||
|
||||
@ -90,27 +90,14 @@
|
||||
|
||||
visit(ManagedEnvironmentLinks::baselineCompareUrl($environment))
|
||||
->waitForText('Which baseline drift requires action?')
|
||||
->assertSee('Baseline not assigned')
|
||||
->assertSee('Not configured')
|
||||
->assertSee('Baseline drift cannot be used for governance decisions until a baseline assignment exists.')
|
||||
->assertSee('Evidence unavailable')
|
||||
->assertSee('Open baseline profiles to assign a baseline to this environment.')
|
||||
->assertSee('Compare readiness flow')
|
||||
->assertSee('Baseline comparison needs an assigned baseline, linked snapshots, a compare run, and a decision output.')
|
||||
->assertSee('Baseline assigned')
|
||||
->assertSee('Missing')
|
||||
->assertSee('Baseline snapshot')
|
||||
->assertSee('Environment snapshot')
|
||||
->assertSee('Environment snapshot evidence is present.')
|
||||
->assertSee('Compare run')
|
||||
->assertSee('Decision output')
|
||||
->assertSee('Available inputs')
|
||||
->assertSee('Operation proof')
|
||||
->assertSee('Unavailable because no baseline assigned.')
|
||||
->assertSee('What this unlocks after assignment')
|
||||
->assertSee('Actionable drift categories')
|
||||
->assertSee('Evidence-backed compare')
|
||||
->assertSee('Governance decision path')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertSee('View compare readiness details')
|
||||
->assertSee('Technical details and audit links')
|
||||
->assertScript('document.querySelector("[data-testid=\"baseline-compare-readiness-disclosure\"]")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"baseline-compare-technical-details\"]")?.open === false', true)
|
||||
->assertSee('Spec330 Tenant Display Name')
|
||||
->assertDontSee('This environment does not have an assigned baseline yet.')
|
||||
->assertDontSee('No coverage warning is currently reported for the latest compare.')
|
||||
|
||||
@ -274,7 +274,7 @@ function spec332ScreenshotsMetadataOnlyFixture(ManagedEnvironment $tenant): Back
|
||||
|
||||
spec332ScreenshotsSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Source not usable');
|
||||
$page->waitForText('Select another backup set.');
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->screenshot(true, spec332RestoreWizardScreenshot('step-1-backup-selected'));
|
||||
spec332CopyBrowserScreenshot('step-1-backup-selected', 'step-1-backup-selected.png');
|
||||
@ -329,7 +329,7 @@ function spec332ScreenshotsMetadataOnlyFixture(ManagedEnvironment $tenant): Back
|
||||
|
||||
spec332ScreenshotsSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Source not usable');
|
||||
$page->waitForText('Select another backup set.');
|
||||
spec332ScreenshotsWizardNext($page);
|
||||
$page->waitForText('Define Restore Scope');
|
||||
spec332ScreenshotsWizardNext($page);
|
||||
@ -358,7 +358,7 @@ function spec332ScreenshotsMetadataOnlyFixture(ManagedEnvironment $tenant): Back
|
||||
|
||||
spec332ScreenshotsSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Source selected');
|
||||
$page->waitForText('Needs attention');
|
||||
spec332ScreenshotsWizardNext($page);
|
||||
$page->waitForText('Define Restore Scope');
|
||||
spec332ScreenshotsWizardNext($page);
|
||||
|
||||
@ -261,15 +261,14 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
spec332BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Source selected')
|
||||
$page->waitForText('Needs attention')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->assertSee('Restore Safety')
|
||||
->assertSee('Is this restore safe to continue?')
|
||||
->assertSee('Backup quality summary')
|
||||
->assertSee('Safety evidence')
|
||||
->assertSee('View safety gates and proof')
|
||||
->assertSee('View quality caveat and detail')
|
||||
->assertSee('Continue to scope.')
|
||||
->assertSee('Review group mappings')
|
||||
->assertSee('Validate impact.')
|
||||
->assertDontSee('Technical startability')
|
||||
->assertDontSee('write-gate')
|
||||
@ -288,7 +287,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
JS, true)
|
||||
->assertScript(spec332BrowserElementIsVisibleScript('restore-run-process-flow-full'), false)
|
||||
->assertScript(spec332BrowserElementIsVisibleScript('restore-run-process-flow-compact'), false)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-safety-evidence\"] details")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-safety-evidence\"]")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-backup-quality-summary\"] details")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-diagnostics-disclosure\"]")?.open === false', true)
|
||||
->assertSee('A usable source backup is selected.');
|
||||
@ -305,12 +304,10 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
spec332BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Source selected');
|
||||
$page->waitForText('Needs attention');
|
||||
spec332BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('3/7 complete')
|
||||
->assertSee('Restore evidence')
|
||||
->assertSee('3/7 complete')
|
||||
$page->waitForText('View validation gates and restore proof')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertDontSee('Technical startability')
|
||||
->assertDontSee('write-gate')
|
||||
@ -350,7 +347,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
->assertSee('Validation blocked')
|
||||
->assertSee('0 of 1 mappings resolved')
|
||||
->assertSee('Resolve target mappings')
|
||||
->assertSee('Restore evidence')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertSee('Resolve required mappings before validation can run.')
|
||||
->assertDontSee('Paste the target Entra ID group Object ID (GUID).')
|
||||
@ -531,7 +528,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
spec332BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Source not usable');
|
||||
$page->waitForText('Select another backup set.');
|
||||
spec332BrowserWizardNext($page);
|
||||
$page->waitForText('Define Restore Scope');
|
||||
spec332BrowserWizardNext($page);
|
||||
@ -548,7 +545,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
->assertSee('Validation decision')
|
||||
->assertSee('Select another backup set.')
|
||||
->assertSee('Snapshot completeness')
|
||||
->assertSee('Validation evidence')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertSee('Safety & Conflict Checks');
|
||||
});
|
||||
|
||||
@ -563,7 +560,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
spec332BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Source selected');
|
||||
$page->waitForText('Needs attention');
|
||||
spec332BrowserWizardNext($page);
|
||||
$page->waitForText('Define Restore Scope');
|
||||
spec332BrowserWizardNext($page);
|
||||
@ -573,7 +570,6 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
->click('Run checks')
|
||||
->waitForText('Validation passed')
|
||||
->assertSee('Validation decision')
|
||||
->assertSee('Validation evidence')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertSee('Checks are current for this scope. Rerun only after scope or mapping changes.')
|
||||
->assertDontSee('Run checks after defining scope and mapping missing groups.')
|
||||
@ -597,7 +593,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
->assertDontSee('Restore safety status')
|
||||
->assertDontSee('Hide safety gates')
|
||||
->assertDontSee('tenant-wide recovery is proven')
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-preview-evidence-details\"]")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-preview-evidence\"]")?.open === false', true)
|
||||
->assertScript(spec332BrowserElementIsVisibleScript('restore-run-process-flow-compact'), false)
|
||||
->assertScript(spec332BrowserElementIsVisibleScript('restore-run-process-flow-full'), false)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-diagnostics-disclosure\"]")?.open === false', true);
|
||||
@ -614,7 +610,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
|
||||
spec332BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Source selected');
|
||||
$page->waitForText('Needs attention');
|
||||
spec332BrowserWizardNext($page);
|
||||
$page->waitForText('Define Restore Scope');
|
||||
spec332BrowserWizardNext($page);
|
||||
@ -623,7 +619,7 @@ function spec332BrowserMetadataOnlyFixture(ManagedEnvironment $tenant): array
|
||||
$page->waitForText('Run checks')
|
||||
->click('Run checks')
|
||||
->waitForText('Validation passed')
|
||||
->assertSee('Validation evidence')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-safe-checks-details\"]")?.open === false', true);
|
||||
|
||||
spec332BrowserWizardNext($page);
|
||||
|
||||
@ -343,7 +343,7 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
|
||||
$page->waitForText('A usable source backup is selected.')
|
||||
->assertSee('Backup quality summary')
|
||||
->assertSee('Available')
|
||||
->assertSee('Ready')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
@ -390,10 +390,11 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $backupSet);
|
||||
|
||||
$page->waitForText('Source selected')
|
||||
->assertSee('Restore needs preparation.')
|
||||
->assertSee('Review group mappings')
|
||||
->assertSee('Safety evidence')
|
||||
$page->waitForText('Needs attention')
|
||||
->assertDontSee('Restore needs preparation.')
|
||||
->assertDontSee('Restore readiness')
|
||||
->assertSee('Primary next action')
|
||||
->assertSee('Continue to scope and resolve required mappings.')
|
||||
->assertSee('View safety gates and proof')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
@ -401,10 +402,9 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
$stepOneDensity = $page->script(<<<'JS'
|
||||
(() => {
|
||||
const evidence = document.querySelector('[data-testid="restore-run-safety-evidence"]');
|
||||
const evidenceDetails = evidence?.querySelector('details');
|
||||
const qualityDetails = document.querySelector('[data-testid="restore-run-backup-quality-summary"] details');
|
||||
return {
|
||||
evidenceDetailsOpen: Boolean(evidenceDetails?.open),
|
||||
evidenceDetailsOpen: Boolean(evidence?.open),
|
||||
qualityDetailsOpen: Boolean(qualityDetails?.open),
|
||||
evidenceHeight: Math.round(evidence?.getBoundingClientRect().height ?? 0),
|
||||
qualityHeight: Math.round(document.querySelector('[data-testid="restore-run-backup-quality-summary"]')?.getBoundingClientRect().height ?? 0),
|
||||
@ -427,7 +427,7 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
->assertSee('1 mapping required')
|
||||
->assertSee('Validation blocked')
|
||||
->assertSee('Resolve target mappings')
|
||||
->assertSee('Restore evidence')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertDontSee('Resolve mappings');
|
||||
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
@ -458,7 +458,7 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $backupSetWithCache);
|
||||
$page->waitForText('Review group mappings');
|
||||
$page->waitForText('Continue to scope and resolve required mappings.');
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Resolve target mappings');
|
||||
spec333BrowserOpenGroupPicker($page);
|
||||
@ -479,7 +479,7 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $backupSetWithoutCache);
|
||||
$page->waitForText('Review group mappings');
|
||||
$page->waitForText('Continue to scope and resolve required mappings.');
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Resolve target mappings');
|
||||
spec333BrowserOpenGroupPicker($page);
|
||||
@ -502,7 +502,7 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $blockedBackupSet);
|
||||
$page->waitForText('Source selected');
|
||||
$page->waitForText('Needs attention');
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Scope summary');
|
||||
spec333BrowserWizardNext($page);
|
||||
@ -528,7 +528,7 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $passedBackupSet);
|
||||
$page->waitForText('Source selected');
|
||||
$page->waitForText('Needs attention');
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Scope summary');
|
||||
spec333BrowserWizardNext($page);
|
||||
@ -540,7 +540,6 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
->assertSee('Prerequisites available')
|
||||
->assertSee('Generate a preview for the current scope before confirmation.')
|
||||
->assertSee('Checks are current for this scope. Rerun only after scope or mapping changes.')
|
||||
->assertSee('Validation evidence')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertDontSee('Run checks after defining scope and mapping missing groups.')
|
||||
->assertDontSee('Safety checks completed');
|
||||
@ -616,7 +615,7 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $backupSet);
|
||||
$page->waitForText('Source selected');
|
||||
$page->waitForText('Needs attention');
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Scope summary');
|
||||
spec333BrowserWizardNext($page);
|
||||
@ -643,11 +642,9 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
->assertSee('Policy diff')
|
||||
->assertSee('Assignments')
|
||||
->assertSee('Scope tags')
|
||||
->assertSee('No changes detected')
|
||||
->assertSee('All reviewed items')
|
||||
->assertDontSee('Restore safety status')
|
||||
->assertDontSee('What the preview proves')
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-preview-evidence-details\"]")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-preview-evidence\"]")?.open === false', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
@ -694,8 +691,11 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
spec333BrowserWizardNext($page);
|
||||
|
||||
$page->waitForText('Confirm & Execute')
|
||||
->assertSee('Ready for final confirmation.')
|
||||
->assertSee('The restore still requires final confirmation before execution.')
|
||||
->assertDontSee('Ready for final confirmation.')
|
||||
->assertSee('Ready')
|
||||
->assertSee('View readiness context')
|
||||
->assertDontSee('The restore still requires final confirmation before execution.')
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-confirm-readiness-detail\"]")?.open === false', true)
|
||||
->assertSee('Preview-only run ready')
|
||||
->assertSee('Create preview-only run')
|
||||
->assertSee('Create a preview-only restore run.')
|
||||
@ -718,7 +718,7 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
->waitForText('Select Backup Set');
|
||||
|
||||
spec333BrowserSelectBackupSet($page, $backupSet);
|
||||
$page->waitForText('Source selected');
|
||||
$page->waitForText('Needs attention');
|
||||
spec333BrowserWizardNext($page);
|
||||
$page->waitForText('Scope summary');
|
||||
spec333BrowserWizardNext($page);
|
||||
@ -743,9 +743,8 @@ function spec333BrowserWizardHeaderMetrics($page): array
|
||||
$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.')
|
||||
$page->waitForText('Preview details')
|
||||
->assertSee('Regenerate preview')
|
||||
->assertDontSee('Execution blocked');
|
||||
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
@ -755,9 +754,12 @@ 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')
|
||||
->assertDontSee("Restore can't continue yet.")
|
||||
->assertSee('Blocked')
|
||||
->assertSee('View readiness context')
|
||||
->assertDontSee('Execution prerequisites are blocked even though preparation evidence is available.')
|
||||
->assertScript('document.querySelector("[data-testid=\"restore-run-confirm-readiness-detail\"]")?.open === false', true)
|
||||
->assertDontSee('Review validation blockers')
|
||||
->assertSee('Confirmation summary')
|
||||
->assertSee('Execution prerequisites blocked')
|
||||
->assertSee('Create preview-only run')
|
||||
|
||||
@ -146,12 +146,14 @@ function spec336EnvironmentFor(User $user, ManagedEnvironment $baseEnvironment,
|
||||
|
||||
$page = visit(ManagedEnvironmentLinks::baselineCompareUrl($noBaselineEnvironment))
|
||||
->waitForText('Which baseline drift requires action?')
|
||||
->assertSee('Baseline not assigned')
|
||||
->assertSee('Not configured')
|
||||
->assertSee('Open baseline profiles')
|
||||
->assertSee('Compare readiness flow')
|
||||
->assertSee('Available inputs')
|
||||
->assertSee('View compare readiness details')
|
||||
->assertSee('Technical details and audit links')
|
||||
->assertScript('document.querySelectorAll("[data-testid=\"baseline-compare-readiness-step\"]").length === 5', true)
|
||||
->assertScript('document.querySelector("[data-step-label=\"Baseline assigned\"]")?.dataset.stepState === "Missing"', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"baseline-compare-readiness-disclosure\"]")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"baseline-compare-technical-details\"]")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"baseline-compare-diagnostics\"]")?.open === false', true)
|
||||
->assertDontSee('raw diff')
|
||||
->assertDontSee('raw payload')
|
||||
@ -161,9 +163,9 @@ function spec336EnvironmentFor(User $user, ManagedEnvironment $baseEnvironment,
|
||||
spec336CopyBrowserScreenshot('01-no-baseline-assigned');
|
||||
|
||||
$page = visit(ManagedEnvironmentLinks::baselineCompareUrl($snapshotMissingEnvironment))
|
||||
->waitForText('Baseline snapshot required')
|
||||
->waitForText('Open baseline profile')
|
||||
->assertSee('Open baseline profile')
|
||||
->assertSee('No usable baseline snapshot input is linked.')
|
||||
->assertSee('Blocked')
|
||||
->assertScript('document.querySelector("[data-step-label=\"Baseline snapshot\"]")?.dataset.stepState === "Missing"', true)
|
||||
->assertScript('document.querySelector("[data-step-label=\"Baseline snapshot\"]")?.dataset.stepCurrentBlocker === "true"', true)
|
||||
->assertDontSee('raw diff')
|
||||
@ -173,8 +175,10 @@ function spec336EnvironmentFor(User $user, ManagedEnvironment $baseEnvironment,
|
||||
spec336CopyBrowserScreenshot('02-baseline-snapshot-required');
|
||||
|
||||
$page = visit(ManagedEnvironmentLinks::baselineCompareUrl($compareRequiredEnvironment))
|
||||
->waitForText('Compare run required')
|
||||
->waitForText('Compare now')
|
||||
->assertSee('Needs attention')
|
||||
->assertSee('Compare now')
|
||||
->assertScript('Array.from(document.querySelectorAll("button, a")).filter((element) => element.offsetParent !== null && element.innerText.trim() === "Compare now").length === 1', true)
|
||||
->assertScript('document.querySelector("[data-step-label=\"Compare run\"]")?.dataset.stepState === "Required"', true)
|
||||
->assertScript('document.querySelector("[data-step-label=\"Compare run\"]")?.dataset.stepCurrentBlocker === "true"', true)
|
||||
->assertScript('document.body.innerText.includes("Total Findings") === false', true)
|
||||
@ -184,9 +188,11 @@ function spec336EnvironmentFor(User $user, ManagedEnvironment $baseEnvironment,
|
||||
spec336CopyBrowserScreenshot('03-compare-run-required');
|
||||
|
||||
$page = visit(ManagedEnvironmentLinks::baselineCompareUrl($compareAvailableEnvironment))
|
||||
->waitForText('Drift findings available')
|
||||
->waitForText('Review drift findings')
|
||||
->assertSee('Needs attention')
|
||||
->assertSee('Review drift findings')
|
||||
->assertSee('OperationRun proof')
|
||||
->assertDontSee('OperationRun proof')
|
||||
->assertScript('document.querySelector("[data-testid=\"baseline-compare-technical-details\"]")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-step-label=\"Compare run\"]")?.dataset.stepState === "Available"', true)
|
||||
->assertScript('document.querySelector("[data-step-label=\"Decision output\"]")?.dataset.stepState === "Available"', true)
|
||||
->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-status-badge\"]")).every((badge) => !badge.innerText.includes("...") && getComputedStyle(badge).overflow !== "hidden" && getComputedStyle(badge).textOverflow !== "ellipsis")', true)
|
||||
@ -199,7 +205,8 @@ function spec336EnvironmentFor(User $user, ManagedEnvironment $baseEnvironment,
|
||||
|
||||
$page = visit(ManagedEnvironmentLinks::baselineCompareUrl($evidenceUnavailableEnvironment))
|
||||
->waitForText('Evidence unavailable - Evidence gaps need review')
|
||||
->assertSee('Compare result exists, but evidence output is not available.')
|
||||
->assertSee('View compare explanation details')
|
||||
->assertScript('document.querySelector("[data-testid=\"baseline-compare-explanation-disclosure\"]")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-step-label=\"Decision output\"]")?.dataset.stepState === "Needs review"', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"baseline-compare-diagnostics\"]")?.open === false', true)
|
||||
->assertDontSee('raw diff')
|
||||
|
||||
@ -67,7 +67,6 @@
|
||||
]);
|
||||
|
||||
baselineCompareLandingLivewire($tenant)
|
||||
->assertActionVisible('compareNow')
|
||||
->assertActionDisabled('compareNow')
|
||||
->assertDontSee('Monitoring landing')
|
||||
->assertDontSee('Navigation lane')
|
||||
|
||||
@ -104,7 +104,7 @@
|
||||
->assertSeeText('View safety gates and restore proof')
|
||||
->assertSeeText('Preview details')
|
||||
->assertSeeText('No changes detected')
|
||||
->assertSeeText('All reviewed items')
|
||||
->assertSeeText('View all reviewed items')
|
||||
->assertSeeText('BitLocker Require')
|
||||
->assertSeeText('No policy changes')
|
||||
->assertSeeText('Policy diff')
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
->fillForm([
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
])
|
||||
->assertSee('Degraded input')
|
||||
->assertSee('Needs attention')
|
||||
->assertSee('Recovery candidate')
|
||||
->assertSee('Degraded items')
|
||||
->assertSee('Input quality signals do not prove that execution is safe or that recovery is verified.');
|
||||
|
||||
@ -150,14 +150,13 @@
|
||||
|
||||
$component = baselineCompareLandingLivewire($environment)
|
||||
->assertSee('Which baseline drift requires action?')
|
||||
->assertSee('Baseline not assigned')
|
||||
->assertSee('Not configured')
|
||||
->assertSee('Baseline drift cannot be used for governance decisions until a baseline assignment exists.')
|
||||
->assertSee('Evidence unavailable')
|
||||
->assertSee('Open baseline profiles to assign a baseline to this environment.')
|
||||
->assertSee('Evidence path')
|
||||
->assertSee('Next action')
|
||||
->assertSee('Compare readiness flow')
|
||||
->assertSee('Baseline comparison needs an assigned baseline, linked snapshots, a compare run, and a decision output.')
|
||||
->assertSee('View compare readiness details')
|
||||
->assertSee('Baseline assigned')
|
||||
->assertSee('Missing')
|
||||
->assertSee('No baseline is assigned.')
|
||||
@ -170,20 +169,18 @@
|
||||
->assertSee('Decision output')
|
||||
->assertSee('No decision output.')
|
||||
->assertSee('Available inputs')
|
||||
->assertSee('Operation proof')
|
||||
->assertSee('Unavailable because no baseline assigned.')
|
||||
->assertSee('What this unlocks after assignment')
|
||||
->assertSee('Actionable drift categories')
|
||||
->assertSee('Evidence-backed compare')
|
||||
->assertSee('Governance decision path')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertSee('Technical details and audit links')
|
||||
->assertDontSee('No Baseline Assigned')
|
||||
->assertDontSee('This environment does not have an assigned baseline yet.')
|
||||
->assertDontSee('No coverage warning is currently reported for the latest compare.')
|
||||
->assertDontSee('Readiness overview')
|
||||
->assertDontSee('Recent baseline activity')
|
||||
->assertDontSee('0% Ready')
|
||||
->assertDontSee('Assigned baseline')
|
||||
->assertDontSee('Drift impact')
|
||||
->assertDontSee('Evidence gap details')
|
||||
->assertDontSee('Search recorded gap subjects')
|
||||
@ -204,7 +201,7 @@
|
||||
->and(substr_count($content, 'data-testid="baseline-compare-readiness-flow"'))->toBe(1)
|
||||
->and(substr_count($content, 'data-testid="baseline-compare-readiness-step"'))->toBe(5)
|
||||
->and(substr_count($content, 'data-testid="baseline-compare-readiness-connector"'))->toBe(4)
|
||||
->and(substr_count($content, 'data-testid="baseline-compare-available-input"'))->toBe(3)
|
||||
->and(substr_count($content, 'data-testid="baseline-compare-available-input"'))->toBe(2)
|
||||
->and(substr_count($content, 'data-testid="baseline-compare-assignment-unlocks"'))->toBe(1)
|
||||
->and($content)->toContain('aria-label="Compare readiness pipeline"')
|
||||
->and($content)->toContain('data-connector-label="Baseline assigned to Baseline snapshot"')
|
||||
@ -215,7 +212,6 @@
|
||||
->and($content)->toMatch('/data-step-label="Environment snapshot"[\s\S]*?>\s*Available\s*</')
|
||||
->and($content)->toMatch('/data-step-label="Compare run"[\s\S]*?>\s*Unavailable\s*</')
|
||||
->and($content)->toMatch('/data-input-label="Environment snapshot"[\s\S]*?>\s*Available\s*</')
|
||||
->and($content)->toMatch('/data-input-label="Operation proof"[\s\S]*?>\s*Unavailable\s*</')
|
||||
->and($visibleLabelCount('Assigned baseline'))->toBe(0)
|
||||
->and($visibleLabelCount('Compare trust'))->toBe(0)
|
||||
->and($visibleLabelCount('Drift impact'))->toBe(0)
|
||||
@ -268,7 +264,7 @@
|
||||
->assertSee('Drift requires review')
|
||||
->assertSee('Evidence path')
|
||||
->assertSee('Evidence gaps need review')
|
||||
->assertSee('Open operation proof')
|
||||
->assertSee('Open operation details')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertDontSee('raw payload should stay hidden')
|
||||
->assertDontSee('provider response should stay hidden')
|
||||
|
||||
@ -231,7 +231,7 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
->fillForm([
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
])
|
||||
->assertSee('Restore Safety')
|
||||
->assertSee('Is this restore safe to continue?')
|
||||
->assertSee('Backup quality summary')
|
||||
->assertSee('Safety evidence')
|
||||
->assertSee('View safety gates and proof')
|
||||
@ -241,7 +241,6 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertSee('Input quality signals do not prove that execution is safe or that recovery is verified.')
|
||||
->assertSee('A usable source backup is selected for this restore draft.')
|
||||
->assertSee('Continue to scope.')
|
||||
->assertSee('Validate impact.')
|
||||
->assertSee('This create flow does not prove recoverability before execution and post-run evidence exist.')
|
||||
->assertDontSee('Technical startability')
|
||||
@ -281,7 +280,6 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
->goToNextWizardStep()
|
||||
->assertWizardCurrentStep(2)
|
||||
->assertSee('Restore evidence')
|
||||
->assertSee('3/7 gates complete')
|
||||
->assertSee('View validation gates and restore proof')
|
||||
->assertSee('Requested by')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
@ -558,7 +556,7 @@ function spec332CurrentPreviewData(BackupSet $backupSet, BackupItem $backupItem)
|
||||
expect($html)
|
||||
->toContain('data-testid="restore-run-preview-evidence"');
|
||||
|
||||
expect(preg_match('/<details[^>]*data-testid="restore-run-preview-evidence-details"[^>]*>/', $html, $matches))->toBe(1);
|
||||
expect(preg_match('/<details[^>]*data-testid="restore-run-preview-evidence"[^>]*>/', $html, $matches))->toBe(1);
|
||||
expect($matches[0])->not->toContain(' open');
|
||||
});
|
||||
|
||||
|
||||
@ -154,29 +154,29 @@ function spec333RestoreCreateCurrentData(BackupSet $backupSet, BackupItem $backu
|
||||
$emptyBackup = spec333RestoreCreateEmptyBackup($tenant);
|
||||
|
||||
spec333RestoreCreateComponent($user, $tenant)
|
||||
->assertSee('Source required')
|
||||
->assertSee('Not configured')
|
||||
->assertSee('Select backup set.')
|
||||
->assertSee('Restore safety cannot be judged until a source backup is selected.');
|
||||
|
||||
spec333RestoreCreateComponent($user, $tenant)
|
||||
->fillForm(['backup_set_id' => (int) $emptyBackup->getKey()])
|
||||
->assertSee('Source not usable')
|
||||
->assertSee('Blocked')
|
||||
->assertSee('Select another backup set.')
|
||||
->assertDontSee('Source unavailable');
|
||||
|
||||
spec333RestoreCreateComponent($user, $tenant)
|
||||
->fillForm(['backup_set_id' => (int) $usableBackup->getKey()])
|
||||
->assertSee('Source selected')
|
||||
->assertSee('Continue to scope.')
|
||||
->assertSee('Needs attention')
|
||||
->assertSee('A usable source backup is selected for this restore draft.')
|
||||
->assertSee('A usable source backup is selected.')
|
||||
->assertSee('Available')
|
||||
->assertSee('Ready')
|
||||
->assertSee('No degradations were detected across 1 captured item.')
|
||||
->assertSee('Items captured')
|
||||
->assertSee('View quality caveat and detail')
|
||||
->assertSee('Safety evidence')
|
||||
->assertSee('View safety gates and proof')
|
||||
->assertDontSee('Backup quality summary Unavailable')
|
||||
->assertDontSee('Backup quality summary Unknown')
|
||||
->assertDontSee('Backup quality summary Available')
|
||||
->assertDontSee('Operation proof is complete')
|
||||
->assertDontSee('recovery verified');
|
||||
});
|
||||
@ -298,8 +298,8 @@ function spec333RestoreCreateCurrentData(BackupSet $backupSet, BackupItem $backu
|
||||
->assertSee('Validation warnings')
|
||||
->assertSee('Needs attention')
|
||||
->assertSee('Changes detected')
|
||||
->assertSee('No changes detected')
|
||||
->assertSee('All reviewed items')
|
||||
->assertSee('View unchanged policies')
|
||||
->assertSee('View all reviewed items')
|
||||
->assertSee('Spec333 Restore Policy')
|
||||
->assertSee('Spec333 Unchanged Policy')
|
||||
->assertSee('Restore action')
|
||||
|
||||
@ -22,12 +22,11 @@
|
||||
|
||||
$component = baselineCompareLandingLivewire($environment)
|
||||
->assertSee('Which baseline drift requires action?')
|
||||
->assertSee('Baseline not assigned')
|
||||
->assertSee('Not configured')
|
||||
->assertSee('This environment does not have an assigned baseline.')
|
||||
->assertSee('Open baseline profiles')
|
||||
->assertSee('Compare readiness flow')
|
||||
->assertSee('Available inputs')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertSee('View compare readiness details')
|
||||
->assertSee('Technical details and audit links')
|
||||
->assertDontSee('raw diff')
|
||||
->assertDontSee('raw payload')
|
||||
->assertDontSee('provider response')
|
||||
@ -42,7 +41,9 @@
|
||||
spec336AssertFlowStep($content, 'Decision output', 'Unavailable', false);
|
||||
|
||||
expect(substr_count($content, 'data-testid="baseline-compare-readiness-step"'))->toBe(5)
|
||||
->and(substr_count($content, 'data-testid="baseline-compare-available-input"'))->toBe(3)
|
||||
->and(substr_count($content, 'data-testid="baseline-compare-available-input"'))->toBe(2)
|
||||
->and($content)->not->toContain('data-testid="baseline-compare-readiness-disclosure" open')
|
||||
->and($content)->not->toContain('data-testid="baseline-compare-technical-details" open')
|
||||
->and($content)->not->toContain('data-testid="baseline-compare-diagnostics" open');
|
||||
});
|
||||
|
||||
@ -62,7 +63,7 @@
|
||||
setAdminPanelContext($environment);
|
||||
|
||||
$component = baselineCompareLandingLivewire($environment)
|
||||
->assertSee('Baseline snapshot required')
|
||||
->assertSee('Blocked')
|
||||
->assertSee('A baseline is assigned, but no usable baseline snapshot is available.')
|
||||
->assertSee('Compare cannot run until baseline snapshot input exists.')
|
||||
->assertSee('Open baseline profile')
|
||||
@ -92,7 +93,7 @@
|
||||
setAdminPanelContext($environment);
|
||||
|
||||
$component = baselineCompareLandingLivewire($environment)
|
||||
->assertSee('Compare run required')
|
||||
->assertSee('Needs attention')
|
||||
->assertSee('Required inputs exist, but no compare run has been created for the current state.')
|
||||
->assertSee('Compare now')
|
||||
->assertActionEnabled('compareNow')
|
||||
@ -104,7 +105,7 @@
|
||||
spec336AssertFlowStep($content, 'Baseline snapshot', 'Available', false);
|
||||
spec336AssertFlowStep($content, 'Compare run', 'Required', true);
|
||||
spec336AssertFlowStep($content, 'Decision output', 'Required', false);
|
||||
spec336AssertInputState($content, 'OperationRun proof', 'Unavailable');
|
||||
expect(substr_count($content, 'data-testid="baseline-compare-primary-action"'))->toBe(1);
|
||||
|
||||
baselineCompareLandingLivewire($environment, user: $readonly)
|
||||
->assertSee('Compare unavailable')
|
||||
@ -129,14 +130,13 @@
|
||||
setAdminPanelContext($environment);
|
||||
|
||||
$runningComponent = baselineCompareLandingLivewire($environment)
|
||||
->assertSee('Compare in progress')
|
||||
->assertSee('View operation progress')
|
||||
->assertSee('Running')
|
||||
->assertSee('Comparison running')
|
||||
->assertSet('operationRunId', (int) $runningRun->getKey());
|
||||
|
||||
$runningContent = $runningComponent->html();
|
||||
spec336AssertFlowStep($runningContent, 'Compare run', 'In progress', true);
|
||||
spec336AssertFlowStep($runningContent, 'Decision output', 'Unavailable', false);
|
||||
spec336AssertInputState($runningContent, 'OperationRun proof', 'Available');
|
||||
|
||||
$failedRun = seedBaselineCompareRun(
|
||||
$environment,
|
||||
@ -149,14 +149,13 @@
|
||||
);
|
||||
|
||||
$failedComponent = baselineCompareLandingLivewire($environment)
|
||||
->assertSee('Compare failed')
|
||||
->assertSee('Review compare failure')
|
||||
->assertSee('Failed')
|
||||
->assertSee('Retry comparison')
|
||||
->assertSet('operationRunId', (int) $failedRun->getKey());
|
||||
|
||||
$failedContent = $failedComponent->html();
|
||||
spec336AssertFlowStep($failedContent, 'Compare run', 'Failed', true);
|
||||
spec336AssertFlowStep($failedContent, 'Decision output', 'Unavailable', false);
|
||||
spec336AssertInputState($failedContent, 'OperationRun proof', 'Available');
|
||||
});
|
||||
|
||||
it('renders drift findings and zero-drift outcomes without broad health or evidence claims', function (): void {
|
||||
@ -188,10 +187,10 @@
|
||||
setAdminPanelContext($environment);
|
||||
|
||||
$driftComponent = baselineCompareLandingLivewire($environment)
|
||||
->assertSee('Drift findings available')
|
||||
->assertSee('Needs attention')
|
||||
->assertSee('Drift requires review before a decision is recorded.')
|
||||
->assertSee('Review drift findings')
|
||||
->assertSee('Evidence unavailable - Operation proof available')
|
||||
->assertSee('Compare evidence available in technical details')
|
||||
->assertDontSee('environment is healthy')
|
||||
->assertDontSee('customer-safe')
|
||||
->assertDontSee('compliant');
|
||||
@ -216,15 +215,18 @@
|
||||
setAdminPanelContext($quietEnvironment);
|
||||
|
||||
$quietComponent = baselineCompareLandingLivewire($quietEnvironment)
|
||||
->assertSee('No drift detected')
|
||||
->assertSee('Ready')
|
||||
->assertSee('No governance action is required from this compare result within available compare coverage.')
|
||||
->assertSee('Review evidence')
|
||||
->assertSee('Evidence unavailable - Operation proof available')
|
||||
->assertSee('No action required')
|
||||
->assertSee('Compare evidence available in technical details')
|
||||
->assertDontSee('environment is healthy')
|
||||
->assertDontSee('customer-safe')
|
||||
->assertDontSee('compliant');
|
||||
|
||||
spec336AssertFlowStep($quietComponent->html(), 'Decision output', 'Available', false);
|
||||
$quietContent = $quietComponent->html();
|
||||
|
||||
spec336AssertFlowStep($quietContent, 'Decision output', 'Available', false);
|
||||
expect($quietContent)->not->toContain('data-testid="baseline-compare-primary-action"');
|
||||
});
|
||||
|
||||
it('keeps compare result, OperationRun proof, and evidence unavailable states separated', function (): void {
|
||||
@ -262,19 +264,19 @@
|
||||
setAdminPanelContext($environment);
|
||||
|
||||
$component = baselineCompareLandingLivewire($environment)
|
||||
->assertSee('Drift findings available')
|
||||
->assertSee('OperationRun proof')
|
||||
->assertSee('Needs attention')
|
||||
->assertSee('Operation evidence')
|
||||
->assertSee('Evidence unavailable - Evidence gaps need review')
|
||||
->assertSee('Compare result exists, but evidence output is not available. Evidence gaps need review.')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertSee('Technical details and audit links')
|
||||
->assertDontSee('customer-safe')
|
||||
->assertDontSee('raw diff');
|
||||
|
||||
$content = $component->html();
|
||||
spec336AssertFlowStep($content, 'Compare run', 'Available', false);
|
||||
spec336AssertFlowStep($content, 'Decision output', 'Needs review', true);
|
||||
spec336AssertInputState($content, 'OperationRun proof', 'Available');
|
||||
spec336AssertInputState($content, 'Evidence path', 'Needs review');
|
||||
expect($content)->not->toContain('data-testid="baseline-compare-technical-details" open');
|
||||
expect($content)->not->toContain('data-testid="baseline-compare-diagnostics" open');
|
||||
});
|
||||
|
||||
@ -323,7 +325,7 @@
|
||||
setAdminPanelContext($environment);
|
||||
|
||||
$component = baselineCompareLandingLivewire($environment)
|
||||
->assertSee('Baseline scope requires review')
|
||||
->assertSee('Blocked')
|
||||
->assertSee('A baseline is assigned, but its scope cannot be used safely for compare.')
|
||||
->assertSee('Open baseline profile')
|
||||
->assertActionDisabled('compareNow');
|
||||
|
||||
@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Resources\RestoreRunResource;
|
||||
use App\Filament\Resources\RestoreRunResource\Pages\CreateRestoreRun;
|
||||
use App\Models\BackupItem;
|
||||
use App\Models\BackupSet;
|
||||
use App\Models\Finding;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\Policy;
|
||||
use App\Support\Baselines\BaselineCompareReasonCode;
|
||||
use App\Support\OperationRunType;
|
||||
use App\Support\RestoreSafety\RestoreSafetyResolver;
|
||||
use Livewire\Livewire;
|
||||
|
||||
it('renders baseline compare idle state with only the decision-card compare action visible', function (): void {
|
||||
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
||||
seedActiveBaselineForTenant($environment);
|
||||
createInventorySyncOperationRunWithCoverage($environment, ['deviceConfiguration' => 'succeeded']);
|
||||
|
||||
$this->actingAs($user);
|
||||
setAdminPanelContext($environment);
|
||||
|
||||
$component = baselineCompareLandingLivewire($environment)
|
||||
->assertSee('Which baseline drift requires action?')
|
||||
->assertSee('Needs attention')
|
||||
->assertSee('Compare now')
|
||||
->assertActionEnabled('compareNow')
|
||||
->assertDontSee('OperationRun proof')
|
||||
->assertDontSee('raw payload')
|
||||
->assertDontSee('provider response');
|
||||
|
||||
expect(substr_count($component->html(), 'data-testid="baseline-compare-primary-action"'))->toBe(1);
|
||||
});
|
||||
|
||||
it('renders baseline compare as one decision with technical proof collapsed', function (): void {
|
||||
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
||||
[$profile, $snapshot] = seedActiveBaselineForTenant($environment);
|
||||
|
||||
$run = seedBaselineCompareRun($environment, $profile, $snapshot, [
|
||||
'reason_code' => BaselineCompareReasonCode::OverdueFindingsRemain->value,
|
||||
'coverage' => [
|
||||
'effective_types' => ['deviceConfiguration'],
|
||||
'covered_types' => ['deviceConfiguration'],
|
||||
'uncovered_types' => [],
|
||||
'proof' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
Finding::factory()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
||||
'scope_key' => 'baseline_profile:'.$profile->getKey(),
|
||||
'severity' => Finding::SEVERITY_HIGH,
|
||||
'status' => Finding::STATUS_NEW,
|
||||
'source' => OperationRunType::BaselineCompare->value,
|
||||
'baseline_operation_run_id' => (int) $run->getKey(),
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
setAdminPanelContext($environment);
|
||||
|
||||
$component = baselineCompareLandingLivewire($environment)
|
||||
->assertSee('Which baseline drift requires action?')
|
||||
->assertSee('Needs attention')
|
||||
->assertSee('Review drift findings')
|
||||
->assertSee('Technical details and audit links')
|
||||
->assertSee('View compare readiness details')
|
||||
->assertDontSee('OperationRun proof')
|
||||
->assertDontSee('raw diff')
|
||||
->assertDontSee('raw payload')
|
||||
->assertDontSee('provider response');
|
||||
|
||||
$html = $component->html();
|
||||
|
||||
expect($html)
|
||||
->not->toContain('data-testid="baseline-compare-technical-details" open')
|
||||
->not->toContain('data-testid="baseline-compare-readiness-disclosure" open')
|
||||
->not->toContain('data-testid="baseline-compare-diagnostics" open');
|
||||
});
|
||||
|
||||
it('renders restore source decision without duplicate readiness status vocabulary', function (): void {
|
||||
[$user, $tenant, $backupSet] = spec398RestorePreviewFixture();
|
||||
|
||||
setAdminPanelContext($tenant);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(CreateRestoreRun::class)
|
||||
->fillForm(['backup_set_id' => (int) $backupSet->getKey()])
|
||||
->assertSee('Is this restore safe to continue?')
|
||||
->assertSee('Needs attention')
|
||||
->assertSee('Primary next action')
|
||||
->assertSee('Continue to scope.')
|
||||
->assertSee('Backup quality summary')
|
||||
->assertSee('Ready')
|
||||
->assertSee('View safety gates and proof')
|
||||
->assertDontSee('Restore readiness')
|
||||
->assertDontSee('Restore needs preparation.')
|
||||
->assertDontSee('Ready for final confirmation.')
|
||||
->assertDontSee("Restore can't continue yet.")
|
||||
->assertDontSee('Degraded input')
|
||||
->assertDontSee('Operation proof is complete');
|
||||
});
|
||||
|
||||
it('renders restore preview with capped rows and secondary proof collapsed', function (): void {
|
||||
[$user, $tenant, $backupSet, $backupItem] = spec398RestorePreviewFixture();
|
||||
$data = spec398RestorePreviewData($backupSet, $backupItem);
|
||||
|
||||
setAdminPanelContext($tenant);
|
||||
|
||||
$component = Livewire::actingAs($user)
|
||||
->test(CreateRestoreRun::class)
|
||||
->set('data', $data)
|
||||
->goToWizardStep(4)
|
||||
->assertWizardCurrentStep(4)
|
||||
->assertSee('Preview evidence')
|
||||
->assertSee('Policies changed')
|
||||
->assertSee('Requires review')
|
||||
->assertSee('Validation blockers')
|
||||
->assertSee('Next gate')
|
||||
->assertSee('View secondary preview counts')
|
||||
->assertSee('View safety gates and restore proof')
|
||||
->assertSee('Spec398 Changed Policy 8')
|
||||
->assertDontSee('Spec398 Changed Policy 9')
|
||||
->assertDontSee('spec398 raw restore payload')
|
||||
->assertDontSee('provider response payload');
|
||||
|
||||
$html = $component->html();
|
||||
|
||||
expect($html)
|
||||
->toContain('data-testid="restore-run-preview-primary-metrics"')
|
||||
->toContain('data-testid="restore-run-preview-secondary-metrics"')
|
||||
->toContain('4 additional changed policies kept in detailed review.')
|
||||
->not->toContain('data-testid="restore-run-preview-secondary-metrics" open')
|
||||
->not->toContain('data-testid="restore-run-preview-evidence" open');
|
||||
});
|
||||
|
||||
function spec398RestorePreviewFixture(): array
|
||||
{
|
||||
$tenant = ManagedEnvironment::factory()->create([
|
||||
'rbac_status' => 'ok',
|
||||
'rbac_last_checked_at' => now(),
|
||||
]);
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
ensureDefaultProviderConnection($tenant, 'microsoft');
|
||||
|
||||
$policy = Policy::create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'external_id' => 'spec398-policy',
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'display_name' => 'Spec398 Restore Policy',
|
||||
'platform' => 'windows',
|
||||
]);
|
||||
|
||||
$backupSet = BackupSet::factory()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'name' => 'Spec398 Restore Backup',
|
||||
'status' => 'completed',
|
||||
'item_count' => 1,
|
||||
]);
|
||||
|
||||
$backupItem = BackupItem::factory()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'policy_id' => (int) $policy->getKey(),
|
||||
'policy_identifier' => $policy->external_id,
|
||||
'policy_type' => $policy->policy_type,
|
||||
'platform' => $policy->platform,
|
||||
'payload' => [
|
||||
'id' => $policy->external_id,
|
||||
'displayName' => $policy->display_name,
|
||||
'settings' => ['encryption' => 'required'],
|
||||
],
|
||||
'metadata' => [
|
||||
'displayName' => $policy->display_name,
|
||||
],
|
||||
'assignments' => [],
|
||||
]);
|
||||
|
||||
return [$user, $tenant, $backupSet, $backupItem];
|
||||
}
|
||||
|
||||
function spec398RestorePreviewData(BackupSet $backupSet, BackupItem $backupItem): array
|
||||
{
|
||||
/** @var RestoreSafetyResolver $resolver */
|
||||
$resolver = app(RestoreSafetyResolver::class);
|
||||
|
||||
$previewDiffs = collect(range(1, 12))
|
||||
->map(fn (int $index): array => [
|
||||
'policy_identifier' => 'spec398-policy-'.$index,
|
||||
'display_name' => 'Spec398 Changed Policy '.$index,
|
||||
'policy_type' => 'deviceConfiguration',
|
||||
'platform' => 'windows',
|
||||
'action' => 'update',
|
||||
'assignments_changed' => $index === 1,
|
||||
'scope_tags_changed' => false,
|
||||
'review_reason' => 'Spec398 preview change requires review.',
|
||||
'review_action_label' => 'Review change',
|
||||
'diff' => [
|
||||
'summary' => ['added' => 0, 'removed' => 0, 'changed' => 1],
|
||||
'changed' => ['setting' => ['before' => 'off', 'after' => 'on']],
|
||||
'added' => [],
|
||||
'removed' => [],
|
||||
],
|
||||
])
|
||||
->all();
|
||||
|
||||
$data = [
|
||||
'backup_set_id' => (int) $backupSet->getKey(),
|
||||
'scope_mode' => 'selected',
|
||||
'backup_item_ids' => [(int) $backupItem->getKey()],
|
||||
'is_dry_run' => true,
|
||||
'group_mapping' => [],
|
||||
'check_summary' => ['blocking' => 0, 'warning' => 1, 'safe' => 2],
|
||||
'check_results' => [
|
||||
['code' => 'warning', 'severity' => 'warning', 'message' => 'Review assignment changes before execution.'],
|
||||
['code' => 'safe', 'severity' => 'safe', 'message' => 'Scope fingerprint is current.'],
|
||||
],
|
||||
'checks_ran_at' => now('UTC')->toIso8601String(),
|
||||
'preview_summary' => [
|
||||
'generated_at' => now('UTC')->toIso8601String(),
|
||||
'policies_total' => 12,
|
||||
'policies_changed' => 12,
|
||||
'assignments_changed' => 1,
|
||||
'scope_tags_changed' => 0,
|
||||
'raw_payload_marker' => 'spec398 raw restore payload',
|
||||
'provider_response' => 'provider response payload',
|
||||
],
|
||||
'preview_diffs' => $previewDiffs,
|
||||
'preview_ran_at' => now('UTC')->toIso8601String(),
|
||||
];
|
||||
|
||||
$data['check_basis'] = $resolver->checksBasisFromData($data);
|
||||
$data['preview_basis'] = $resolver->previewBasisFromData($data);
|
||||
|
||||
return RestoreRunResource::synchronizeRestoreSafetyDraft($data);
|
||||
}
|
||||
@ -67,7 +67,7 @@
|
||||
expect($firstSummary)
|
||||
->toBeString()
|
||||
->toContain('does not contain a usable captured item yet')
|
||||
->and(data_get($first, 'decisionCard.status'))->toBe('Source not usable')
|
||||
->and(data_get($first, 'decisionCard.status'))->toBe('Blocked')
|
||||
->and(data_get($first, 'decisionCard.nextAction'))->toBe('Select another backup set.');
|
||||
|
||||
$backupItem->update([
|
||||
@ -95,12 +95,12 @@
|
||||
->toBeString()
|
||||
->toContain('A usable source backup is selected for this restore draft.')
|
||||
->not->toContain('does not contain a usable captured item yet')
|
||||
->and(data_get($second, 'decisionCard.status'))->toBe('Source selected')
|
||||
->and(data_get($second, 'decisionCard.status'))->toBe('Needs attention')
|
||||
->and(data_get($second, 'decisionCard.reason'))->toBe('A usable source backup is selected.')
|
||||
->and(data_get($second, 'decisionCard.impact'))->toBe('Scope, validation, and preview must still prove this draft before confirmation or real execution.')
|
||||
->and(data_get($second, 'decisionCard.tone'))->toBe('success')
|
||||
->and(data_get($second, 'decisionCard.tone'))->toBe('warning')
|
||||
->and(data_get($second, 'decisionCard.nextAction'))->toBe('Continue to scope.')
|
||||
->and(data_get($second, 'backupQualityCard.status'))->toBe('Available')
|
||||
->and(data_get($second, 'backupQualityCard.status'))->toBe('Ready')
|
||||
->and(data_get($second, 'backupQualityCard.tone'))->toBe('success')
|
||||
->and(data_get($second, 'backupQualityCard.summary'))->toBe('No degradations were detected across 1 captured item.');
|
||||
});
|
||||
@ -157,7 +157,7 @@
|
||||
expect(data_get($first, 'processFlow.steps.0.summary'))
|
||||
->toBeString()
|
||||
->toContain('does not contain a usable captured item yet')
|
||||
->and(data_get($first, 'decisionCard.status'))->toBe('Source not usable');
|
||||
->and(data_get($first, 'decisionCard.status'))->toBe('Blocked');
|
||||
|
||||
$usableBackup = BackupSet::factory()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
@ -201,5 +201,5 @@
|
||||
->toBeString()
|
||||
->toContain('A usable source backup is selected for this restore draft.')
|
||||
->not->toContain('does not contain a usable captured item yet')
|
||||
->and(data_get($second, 'decisionCard.status'))->toBe('Source selected');
|
||||
->and(data_get($second, 'decisionCard.status'))->toBe('Needs attention');
|
||||
});
|
||||
|
||||
@ -40,9 +40,9 @@ ## 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` | Partial - Spec 390 adds Restore-local readiness guidance and Spec 397 verifies completed restore-run detail receipt reduction; list/create/failure/conflict coverage remains separate. |
|
||||
| Restore Safety Workflow | UI-053, UI-054 | `target-experience-briefs/restore-safety-workflow.md` | Partial - Spec 398 verifies Restore Create / Preview / Readiness decision-page reduction with focused browser proof; Spec 397 verifies completed restore-run detail receipt reduction; list/failure/conflict coverage remains 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 |
|
||||
| Baseline Compare / Drift | UI-061 | `target-experience-briefs/baseline-compare-drift.md` | Partial - Spec 398 verifies the Baseline Compare decision-page reduction with focused browser proof; compare matrix/product hierarchy remains separate. |
|
||||
|
||||
## Coverage By Area
|
||||
|
||||
|
||||
@ -61,14 +61,14 @@ # 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 | view browser-verified; create route exists | restore capability | Backup / Restore | Operations / Monitoring | Strategic Surface | repo-verified | - | [report](../../specs/397-receipt-page-reduction/implementation-report.md) | Spec 397 textual browser proof verifies completed Restore Run detail as a reduced receipt with OperationRun proof collapsed; broader create/failure/conflict workflow coverage remains follow-up. |
|
||||
| UI-054 | `/admin/workspaces/{workspace}/environments/{environment}/restore-runs/create` and `/view` | resource | Restore Run Create/View | Backup / restore | environment record/workflow | create/view browser-verified | restore capability | Backup / Restore | Operations / Monitoring | Strategic Surface | browser-verified | [create](../../specs/333-restore-create-ux-final-productization/artifacts/screenshots/08-step-4-preview-generated.png) | [report](../../specs/398-decision-page-contract-migration/implementation-report.md) | Spec 398 focused browser proof verifies Restore Create / Preview / Readiness as a compact decision surface with proof/readiness details collapsed and preview rows capped; Spec 397 still covers completed restore-run receipt reduction. |
|
||||
| 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. |
|
||||
| UI-058 | `/admin/baseline-profiles/{record}/compare-matrix` | page | Baseline Compare Matrix | Governance | workspace analysis | route exists | baseline capability | Drift / Diff | Evidence / Audit | Strategic Surface | repo-verified | - | - | Matrix/product hierarchy review needed. |
|
||||
| UI-059 | `/admin/baseline-snapshots` | resource | Baseline Snapshots | Evidence / audit | workspace analysis | route exists | workspace member | Evidence / Audit | Drift / Diff | Domain Pattern Surface | repo-verified | - | - | Workspace-owned evidence library. |
|
||||
| UI-060 | `/admin/baseline-snapshots/{record}` | resource | Baseline Snapshot Detail | Evidence / audit | workspace record | reachable | workspace + record entitlement | Evidence / Audit | Drift / Diff | Domain Pattern Surface | browser-verified | - | [report](../../specs/397-receipt-page-reduction/implementation-report.md) | Spec 397 textual browser proof verifies baseline receipt hierarchy and caps governed-subject detail before internal technical sections. |
|
||||
| UI-061 | `/admin/workspaces/{workspace}/environments/{environment}/baseline-compare` | page | Baseline Compare | Governance | environment-bound | browser blocked/404 in fixture | workspace + environment entitlement and baseline state | Drift / Diff | Operations / Monitoring | Strategic Surface | repo-verified | [blocked](screenshots/desktop/ui-015-baseline-compare-blocked-404.png) | [report](page-reports/ui-015-baseline-compare.md) | Route exists in route list; smoke fixture could not render it. |
|
||||
| UI-061 | `/admin/workspaces/{workspace}/environments/{environment}/baseline-compare` | page | Baseline Compare | Governance | environment-bound | browser-verified | workspace + environment entitlement and baseline state | Drift / Diff | Operations / Monitoring | Strategic Surface | browser-verified | [desktop](../../specs/336-baseline-compare-product-process-flow-alignment/artifacts/screenshots/04-compare-result-available.png) | [report](../../specs/398-decision-page-contract-migration/implementation-report.md) | Spec 398 focused browser proof verifies Baseline Compare as a compact decision page with OperationRun proof, readiness, diagnostics, matrix path, and evidence-gap internals collapsed by default. |
|
||||
| UI-100 | `/admin/workspaces/{workspace}/environments/{environment}/baseline-subject-resolution` | page | Baseline Subject Resolution | Governance | environment-bound | browser-verified | workspace + environment entitlement; view requires baseline view capability, mutations require baseline manage capability | Drift / Diff | Evidence / Audit | Strategic Surface | browser-verified | [desktop](../../specs/384-baseline-subject-resolution-ui/artifacts/screenshots/spec384-01-baseline-subject-resolution.png) | [report](page-reports/ui-100-baseline-subject-resolution.md) | Focused operator worklist for persisted baseline subject identity and coverage decisions; reachable from Baseline Compare and Operation detail only when actionable outcomes exist. |
|
||||
| UI-062 | `/admin/workspaces/{workspace}/environments/{environment}/inventory` | cluster | Inventory Cluster | Inventory | environment-bound | route exists | environment entitlement | Inventory | Workspace / Tenant Context | Domain Pattern Surface | repo-verified | - | - | Cluster landing/navigation surface. |
|
||||
| UI-063 | `/admin/workspaces/{workspace}/environments/{environment}/inventory/inventory-coverage` | page | Inventory Coverage | Inventory | environment-bound | route exists | environment entitlement | Inventory | Evidence / Audit | Strategic Surface | repo-verified | - | - | Coverage truth page; strategic because it gates evidence confidence. |
|
||||
|
||||
@ -0,0 +1,84 @@
|
||||
# Requirements Checklist: Spec 398 - Decision Page Contract Migration v1
|
||||
|
||||
**Purpose**: Validate specification and preparation quality before implementation.
|
||||
**Created**: 2026-06-22
|
||||
**Feature**: `specs/398-decision-page-contract-migration/spec.md`
|
||||
|
||||
## Candidate Selection
|
||||
|
||||
- [x] CHK001 Selected candidate exists in user-provided source material and is not invented.
|
||||
- [x] CHK002 Selected candidate is supported by repo truth: Spec 395 explicitly deferred a Decision Page Reduction Pass.
|
||||
- [x] CHK003 Active auto-prep queue was checked; `docs/product/spec-candidates.md` says no safe automatic target remains, so this is treated as a manually provided candidate.
|
||||
- [x] CHK004 Related specs were checked as context and are not being rewritten or converted back to preparation state.
|
||||
- [x] CHK005 Close alternatives are deferred because they are manual-promotion only, already spec-backed, or outside the selected decision-page migration scope.
|
||||
- [x] CHK006 The selected slice is bounded to existing Baseline Compare and Restore Preview / Readiness surfaces, with Risk Exception optional only if narrow.
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] CHK007 Spec states the concrete operator problem and current failure mode.
|
||||
- [x] CHK008 Spec explains user-visible improvement and business/product value.
|
||||
- [x] CHK009 Primary users/operators are identified through user stories.
|
||||
- [x] CHK010 User stories are prioritized and independently testable.
|
||||
- [x] CHK011 Functional requirements are testable and use stable IDs.
|
||||
- [x] CHK012 Non-functional, UX, RBAC/security, auditability/observability, and data/truth requirements are present.
|
||||
- [x] CHK013 Out-of-scope boundaries are explicit and prevent scope creep.
|
||||
- [x] CHK014 Acceptance criteria and success criteria are measurable enough for implementation close-out.
|
||||
- [x] CHK015 Assumptions and open questions are documented, with no blocking open question.
|
||||
|
||||
## Product Surface Contract
|
||||
|
||||
- [x] CHK016 Spec references `docs/product/standards/product-surface-contract.md`.
|
||||
- [x] CHK017 No-legacy posture is explicit: canonical replacement, no duplicate old/new decision UI, no compatibility toggle.
|
||||
- [x] CHK018 Product Surface Impact names Decision Page / Wizard Page with Decision Sub-Surface archetypes, primary user questions, one-primary-action rule, surface budgets, technical demotion, status vocabulary, visible complexity outcome, and exceptions.
|
||||
- [x] CHK019 Browser proof is required because rendered UI changes are expected.
|
||||
- [x] CHK020 Human Product Sanity is required and has concrete review questions.
|
||||
- [x] CHK021 Product Surface exceptions are `none planned`; any exception is a stop condition requiring spec/plan update.
|
||||
- [x] CHK022 Completed historical specs must not be rewritten, normalized, reopened, or stripped of validation/browser/task history.
|
||||
- [x] CHK023 UI Action Matrix names primary, secondary, technical/audit, dangerous/high-impact actions, and mutation scope for each target surface.
|
||||
- [x] CHK024 UI coverage registry decision is explicit for route inventory, design coverage matrix, page reports, strategic surfaces, grouped follow-ups, and unresolved pages.
|
||||
|
||||
## Constitution And Architecture
|
||||
|
||||
- [x] CHK025 Spec has a completed Spec Candidate Check with approval class, score, red flags, and decision.
|
||||
- [x] CHK026 Proportionality review confirms no new persisted entity, enum/status family, abstraction, registry, resolver, taxonomy, or broad UI framework is approved.
|
||||
- [x] CHK027 Plan states no Graph calls, migrations, queues, env vars, storage changes, or package changes are expected.
|
||||
- [x] CHK028 Plan names likely affected repo surfaces without requiring application edits during preparation.
|
||||
- [x] CHK029 Workspace/tenant isolation and deny-as-not-found requirements are preserved.
|
||||
- [x] CHK030 RBAC/security requirements state UI visibility is not authorization.
|
||||
- [x] CHK031 OperationRun remains execution/evidence truth and no OperationRun lifecycle or notification behavior is changed.
|
||||
- [x] CHK032 Data/truth-source requirements distinguish execution truth, artifact truth, backup/snapshot truth, recovery/evidence truth, and operator next action.
|
||||
|
||||
## Filament / Livewire / Actions
|
||||
|
||||
- [x] CHK033 Plan states Filament v5 / Livewire v4.1.4 compliance.
|
||||
- [x] CHK034 Plan states Laravel 12 provider registration location: `apps/platform/bootstrap/providers.php`.
|
||||
- [x] CHK035 Global search posture must remain safe for any changed resource.
|
||||
- [x] CHK036 Destructive/high-impact actions must keep `->action(...)`, confirmation, authorization, audit where mutating, and tests if touched.
|
||||
- [x] CHK037 Asset strategy is recorded: no new assets expected, `filament:assets` only if implementation registers assets.
|
||||
- [x] CHK038 UI implementation must reuse native Filament and existing shared primitives before local Blade/CSS.
|
||||
|
||||
## Test Governance
|
||||
|
||||
- [x] CHK039 Test lanes are named: Unit only for pure helpers, Feature/Filament for page/action visibility, Browser for focused rendered decision proof.
|
||||
- [x] CHK040 Browser proof is focused and fixture-bounded, not a broad visual regression suite.
|
||||
- [x] CHK041 Planned validation commands are narrow and implementation must choose exact filters after discovery.
|
||||
- [x] CHK042 Fixture/helper cost must stay explicit and minimal.
|
||||
- [x] CHK043 Implementation report fields are specified in `plan.md`.
|
||||
- [x] CHK044 Requirement coverage map includes both FR and NFR coverage.
|
||||
|
||||
## Readiness
|
||||
|
||||
- [x] CHK045 `spec.md` exists and is complete enough for implementation planning.
|
||||
- [x] CHK046 `plan.md` exists and identifies likely affected surfaces, constraints, risks, phases, tests, rollout, and stop conditions.
|
||||
- [x] CHK047 `tasks.md` exists and is ordered, scoped, testable, and suitable for a later implementation loop.
|
||||
- [x] CHK048 Scope is small enough for a bounded implementation loop.
|
||||
- [x] CHK049 No open question blocks safe implementation.
|
||||
|
||||
## Review Outcome
|
||||
|
||||
- [x] CHK050 Review outcome class: `acceptable-special-case`.
|
||||
- [x] CHK051 Workflow outcome: `keep`.
|
||||
|
||||
## Notes
|
||||
|
||||
This checklist validates preparation only. It does not claim runtime UI reduction, test execution, browser verification, human product sanity completion, or implementation close-out.
|
||||
@ -0,0 +1,87 @@
|
||||
# Implementation Report: Spec 398 - Decision Page Contract Migration v1
|
||||
|
||||
**Branch**: `398-decision-page-contract-migration`
|
||||
**HEAD**: `a5b7300c`
|
||||
**Implemented**: 2026-06-22
|
||||
**Selected slice**: Baseline Compare plus Restore Create / Restore Preview / Restore Readiness. Risk Exception Detail was inspected and deferred.
|
||||
**Dirty state at close-out**: dirty working tree with Spec 398 runtime, test, and active-spec documentation changes only.
|
||||
|
||||
## Summary
|
||||
|
||||
Implemented the Product Surface Contract decision-page reduction over the required surfaces:
|
||||
|
||||
- Baseline Compare now uses canonical decision status vocabulary, one visible decision question/action, and closed technical details for OperationRun/audit proof, readiness flow, diagnostics, matrix path, RBAC detail, evidence-gap internals, and explanation detail.
|
||||
- Restore Create / Preview now uses a canonical restore decision status, removes default duplicate readiness status badges from source and confirmation surfaces, reduces preview summary cards to four default cards, caps changed preview rows at eight, and moves readiness/proof/unchanged/all-reviewed details behind deliberate disclosures.
|
||||
- Mobile polish keeps the Baseline decision action above summary/proof detail and uses Filament-native `columnSpanFull()` on Restore step-one schema components so the native backup selector and read-only decision/evidence fields span the wizard grid on all breakpoints.
|
||||
- Existing safety controls were preserved: Baseline `Compare now`, restore dry-run/default confirmation, typed confirmation, capability checks, audit behavior, and execution gates remain in their existing handlers.
|
||||
|
||||
## Product Surface Contract Close-Out
|
||||
|
||||
- **No-legacy posture**: pass. Old proof-first/default-expanded decision content was replaced; no compatibility mode was added.
|
||||
- **Product Surface Impact**: Baseline Compare remains a Decision Page. Restore Create remains a wizard with decision sub-surfaces.
|
||||
- **UI Surface Impact**: rendered default UI changed for `/admin/workspaces/{workspace}/environments/{environment}/baseline-compare` and `/admin/workspaces/{workspace}/environments/{environment}/restore-runs/create`.
|
||||
- **Runtime UI files changed**: yes. Baseline Compare Blade/page and Restore Create Blade/presenter surfaces changed.
|
||||
- **Files changed**: Baseline Compare page/view/tests; Restore Create resource schema, presenter, safety decision, confirmation, preview, and safety evidence views/tests; active Spec 398 report/tasks; UI audit route inventory and design coverage matrix.
|
||||
- **Page archetype**: Decision Page; Wizard Page with Decision Sub-Surface.
|
||||
- **Surface budgets**: one primary decision/action per touched default surface. Baseline `Compare now` is no longer rendered as a header duplicate; it remains the same confirmed Filament action mounted from the decision card. Restore preview defaults to four summary cards; changed rows are capped at eight; readiness/proof/diagnostics move to closed details.
|
||||
- **Technical Annex / deep-link demotion**: OperationRun/audit proof remains reachable for authorized users from closed technical details, not as default product content.
|
||||
- **Canonical status vocabulary**: Baseline Compare uses `Ready`, `Needs attention`, `Blocked`, `Running`, `Failed`, `Not configured`, `Unknown`. Restore Create uses `Ready`, `Needs attention`, `Blocked`, `Not configured`, `Unknown`.
|
||||
- **Product Surface exceptions**: none.
|
||||
- **Human Product Sanity**: pass. Default-visible complexity decreased; safety blockers and material drift remain visible; technical/audit proof is still available through deliberate disclosures.
|
||||
- **Visible complexity outcome**: decreased. Baseline removes default proof/readiness/action duplication; Restore Preview reduces nine default cards to four; Restore Create/Confirm no longer renders a second readiness badge by default; changed/unchanged/all-reviewed expansion is deliberate.
|
||||
- **Completed-spec rewrite assertion**: no completed historical spec artifacts were rewritten to satisfy new Product Surface wording. Only the active Spec 398 artifacts and UI audit inventory/matrix were updated.
|
||||
|
||||
## Risk Exception Decision
|
||||
|
||||
Risk Exception Detail was deferred. Inspection confirmed it has raw evidence-style fields (`Source ID`, `Fingerprint`, JSON summary payload), but including it would reopen the Spec 354 accepted-risk detail surface beyond the required Baseline Compare and Restore scope. Renew/revoke actions were left unchanged and remain confirmation-protected in existing code.
|
||||
|
||||
## Filament / Safety Contract
|
||||
|
||||
- **Livewire v4**: compliant. No Livewire v3 APIs or Filament legacy APIs introduced.
|
||||
- **Provider registration**: unchanged; Laravel providers remain in `apps/platform/bootstrap/providers.php`.
|
||||
- **Global search**: unchanged. `RestoreRunResource` remains `protected static bool $isGloballySearchable = false`; Baseline Compare is a page, not a globally searchable resource; Risk Exception was not touched.
|
||||
- **Destructive/high-impact actions**: existing `Compare now` action remains a Filament action with confirmation and capability gating. Restore execution/preview/confirmation gates remain in existing resource/page handlers with dry-run and typed confirmation behavior. Risk Exception renew/revoke were not changed.
|
||||
- **Asset strategy**: no new assets, no panel asset registration, no Tailwind asset pipeline change. No additional `filament:assets` requirement was introduced beyond standard deploy practice.
|
||||
- **Filament native posture**: no Filament internal Blade views were published or replaced. The wizard and backup selector remain native Filament schema/form components; mobile full-width behavior uses Filament's documented `columnSpanFull()` API. Existing scoped wizard-header CSS remains the only Filament hook-class styling override.
|
||||
- **Deployment impact**: no migrations, env vars, routes, queues, cron, storage, provider scopes, or Graph render calls added.
|
||||
|
||||
## Browser Proof
|
||||
|
||||
Focused browser proof used existing seeded browser smokes rather than adding a new browser file:
|
||||
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec336BaselineCompareProductProcessFlowAlignmentSmokeTest.php`
|
||||
- PASS: 1 test, 53 assertions.
|
||||
- Covered Baseline Compare no-baseline, missing snapshot, compare required, single visible `Compare now` trigger, drift available, evidence-gap states, closed readiness/technical/diagnostic disclosures, and no default raw payload/proof leakage.
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec333RestoreCreateUxFinalProductizationSmokeTest.php`
|
||||
- PASS: 6 tests, 187 assertions.
|
||||
- Covered Restore Create selected source, mapping, validation, preview, confirmation, execution-prerequisite lock, canonical source/quality/readiness vocabulary, closed readiness/proof disclosures, and responsive preview review cards/tables.
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec333RestoreCreateUxFinalProductizationSmokeTest.php tests/Browser/Spec336BaselineCompareProductProcessFlowAlignmentSmokeTest.php`
|
||||
- PASS: 7 tests, 240 assertions.
|
||||
- Re-run after mobile polish and Filament-native full-span restore step changes.
|
||||
|
||||
Screenshots are produced by those existing browser tests under their historical spec artifact folders. Spec 398 records equivalent textual proof here instead of duplicating screenshots.
|
||||
|
||||
## Feature / Unit Proof
|
||||
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=Spec398`
|
||||
- PASS: 4 tests, 55 assertions.
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter='Spec398|Spec336BaselineCompareProductProcessFlowAlignmentTest|BaselineCompareLandingStartSurfaceTest|Spec333RestoreCreateUxFinalProductizationTest|RestoreSelectionQualityTruthTest|RestoreRunCreatePresenterDeterminismTest'`
|
||||
- PASS: 38 tests, 425 assertions.
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter='Spec336BaselineCompareProductProcessFlowAlignmentTest|BaselineCompareLandingStartSurfaceTest|Spec333RestoreCreateUxFinalProductizationTest|RestoreSelectionQualityTruthTest|RestoreRunCreatePresenterDeterminismTest|RestoreRunPreviewProductizationTest|Spec330EnvironmentDashboardBaselineCompareProductizationTest|Spec332ProductProcessFlowSystemTest|Spec398DecisionPageContractMigrationTest'`
|
||||
- PASS: 61 tests, 849 assertions.
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter='Spec398DecisionPageContractMigrationTest|Spec333RestoreCreateUxFinalProductizationTest|Spec336BaselineCompareProductProcessFlowAlignmentTest|BaselineCompareLandingStartSurfaceTest|Spec332ProductProcessFlowSystemTest|RestoreRunCreatePresenterDeterminismTest'`
|
||||
- PASS: 53 tests, 652 assertions.
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter='Spec398DecisionPageContractMigrationTest|Spec333RestoreCreateUxFinalProductizationTest|Spec332ProductProcessFlowSystemTest|RestoreRunCreatePresenterDeterminismTest'`
|
||||
- PASS: 32 tests, 489 assertions.
|
||||
|
||||
## Formatting / Diff Checks
|
||||
|
||||
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||
- PASS.
|
||||
- `git diff --check`
|
||||
- PASS.
|
||||
|
||||
## Remaining Findings
|
||||
|
||||
- In-scope confirmed findings: none after the passing focused feature/unit and browser runs.
|
||||
- Residual risk: Risk Exception Detail still has default raw evidence fields and should remain a separate bounded follow-up if desired.
|
||||
338
specs/398-decision-page-contract-migration/plan.md
Normal file
338
specs/398-decision-page-contract-migration/plan.md
Normal file
@ -0,0 +1,338 @@
|
||||
# Implementation Plan: Spec 398 - Decision Page Contract Migration v1
|
||||
|
||||
**Branch**: `398-decision-page-contract-migration`
|
||||
**Spec**: `specs/398-decision-page-contract-migration/spec.md`
|
||||
**Created**: 2026-06-22
|
||||
**Mode**: Product Surface Contract decision-page runtime reduction over existing surfaces
|
||||
|
||||
## Summary
|
||||
|
||||
Migrate the existing Baseline Compare and Restore Preview / Restore Readiness decision surfaces to the Product Surface Contract's Decision Page rules. The implementation must reduce default-visible proof, diagnostics, long tables, duplicate readiness summaries, and technical deep links while preserving RBAC, auditability, OperationRun evidence access, and restore safety controls.
|
||||
|
||||
Risk Exception Detail is optional. It may be included only if implementation remains a narrow default-visible evidence demotion over existing `FindingExceptionResource` / `ViewFindingException` behavior.
|
||||
|
||||
No application implementation is performed by this preparation package.
|
||||
|
||||
## 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.
|
||||
- **Panel provider location**: Laravel providers remain registered in `apps/platform/bootstrap/providers.php`; this spec must not add a panel provider.
|
||||
- **Livewire v4 compliance**: Filament work must use Filament v5 / Livewire v4 APIs only; no Livewire v3 references.
|
||||
|
||||
## Constitution Check
|
||||
|
||||
- **Workspace/tenant isolation**: Pass with implementation requirement. Existing workspace + managed environment route scoping remains authoritative; out-of-scope records remain deny-as-not-found.
|
||||
- **RBAC first**: Pass with implementation requirement. UI visibility is not authorization; mutating actions keep server-side checks.
|
||||
- **Audit first**: Pass. Audit and OperationRun evidence are demoted from default product view but not removed.
|
||||
- **Read/write separation**: Pass. Baseline Compare rendering and restore preview decisions are read-oriented by default; restore execution and compare start remain existing high-impact operations with confirmation and capabilities.
|
||||
- **Product Surface Contract Gate**: Pass by design. This spec is a Product Surface Contract migration and requires browser proof plus Human Product Sanity.
|
||||
- **Proportionality**: Pass. No new persisted entity, enum/status family, runtime framework, provider abstraction, or broad presenter family is approved.
|
||||
|
||||
## Current Repo Truth
|
||||
|
||||
### Baseline Compare
|
||||
|
||||
Relevant surfaces:
|
||||
|
||||
- `apps/platform/app/Filament/Pages/BaselineCompareLanding.php`
|
||||
- `apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php`
|
||||
- `apps/platform/tests/Feature/Filament/Spec336BaselineCompareProductProcessFlowAlignmentTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/Spec330EnvironmentDashboardBaselineCompareProductizationTest.php`
|
||||
- `apps/platform/tests/Browser/Spec336BaselineCompareProductProcessFlowAlignmentSmokeTest.php`
|
||||
|
||||
Repo observations:
|
||||
|
||||
- Route slug is `workspaces/{workspace}/environments/{environment}/baseline-compare`.
|
||||
- The page already has decision-card, proof-panel, readiness-flow, and diagnostics concepts.
|
||||
- Current code still contains default labels/copy such as `OperationRun proof`, evidence-gap diagnostics, RBAC summaries, and multiple proof/detail sections that can compete with the primary decision.
|
||||
- `Compare now` uses a Filament action with `->requiresConfirmation()` and existing capability/OperationRun behavior. This safety posture must remain.
|
||||
|
||||
### Restore Preview / Readiness
|
||||
|
||||
Relevant surfaces:
|
||||
|
||||
- `apps/platform/app/Filament/Resources/RestoreRunResource.php`
|
||||
- `apps/platform/app/Filament/Resources/RestoreRunResource/Pages/CreateRestoreRun.php`
|
||||
- `apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php`
|
||||
- `apps/platform/tests/Feature/Filament/RestorePreviewTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/RestoreRunPreviewProductizationTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/Spec333RestoreCreateUxFinalProductizationTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/Spec390RestoreReadinessGuidanceTest.php`
|
||||
- `apps/platform/tests/Browser/Spec333RestoreCreateUxFinalProductizationSmokeTest.php`
|
||||
- `apps/platform/tests/Browser/Spec332RestoreRunWizardProductProcessFlowSmokeTest.php`
|
||||
|
||||
Repo observations:
|
||||
|
||||
- `RestoreRunResource` is not globally searchable.
|
||||
- Restore create uses `CreateRestoreRun` with a Filament wizard and `RestoreRunCreatePresenter` contract.
|
||||
- The presenter already distinguishes scope, checks, preview, provider readiness, proof items, diagnostics disclosure, dry-run/acknowledgement, and confirmation state.
|
||||
- Implementation must reduce default duplication without weakening safety or execution gates.
|
||||
|
||||
### Risk Exception Detail (optional)
|
||||
|
||||
Relevant surfaces:
|
||||
|
||||
- `apps/platform/app/Filament/Resources/FindingExceptionResource.php`
|
||||
- `apps/platform/app/Filament/Resources/FindingExceptionResource/Pages/ViewFindingException.php`
|
||||
- `apps/platform/resources/views/filament/infolists/entries/accepted-risk-guidance.blade.php`
|
||||
- `apps/platform/tests/Feature/Findings/FindingExceptionDetailDecisionSummaryTest.php`
|
||||
- `apps/platform/tests/Feature/Findings/Spec354FindingExceptionDetailGuidanceTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/FindingExceptionHeaderDisciplineTest.php`
|
||||
- `apps/platform/tests/Browser/Spec354AcceptedRiskGuidanceSmokeTest.php`
|
||||
|
||||
Repo observations:
|
||||
|
||||
- `FindingExceptionResource` is not globally searchable.
|
||||
- `ViewFindingException` exposes `renew_exception` and `revoke_exception` with `->requiresConfirmation()` and service-backed actions.
|
||||
- `FindingExceptionResource` contains evidence-reference fields including Source ID, Fingerprint, and JSON summary payload. These are candidates for default demotion if the optional slice is included.
|
||||
- Spec 354 already productized accepted-risk guidance; this spec must not reopen that workflow.
|
||||
|
||||
## Technical Approach
|
||||
|
||||
1. Start with discovery and failing/adjusted tests for default-visible hierarchy on Baseline Compare and Restore Preview.
|
||||
2. For each target surface, identify the current default-visible sections and classify each as Product Layer or Technical Evidence Layer.
|
||||
3. Update existing page/presenter output so the Product Layer is first and compact:
|
||||
- one primary decision question
|
||||
- one canonical status
|
||||
- one recommended next action
|
||||
- impact summary
|
||||
- top material blockers/differences
|
||||
4. Move, collapse, cap, or relabel technical detail:
|
||||
- OperationRun proof
|
||||
- evidence/raw links
|
||||
- source keys, detectors, fingerprints, UUIDs
|
||||
- raw payloads/provider responses/logs
|
||||
- full diff/preview/diagnostic tables
|
||||
5. Preserve existing service/action ownership for mutations. Do not move business logic into Blade.
|
||||
6. If optional Risk Exception cleanup is included, only demote default-visible raw evidence/reference fields and preserve Spec 354 guidance and current renew/revoke behavior.
|
||||
7. Update UI coverage registry artifacts for each rendered surface changed.
|
||||
8. Run focused Feature/Filament tests, focused browser smoke, Human Product Sanity, Pint, and `git diff --check`.
|
||||
|
||||
## Domain / Model Implications
|
||||
|
||||
- No database migration is planned.
|
||||
- No model ownership changes are planned.
|
||||
- No new persisted decision state is approved.
|
||||
- No new enum/status family is approved.
|
||||
- No Graph/remote provider call may be introduced during UI render.
|
||||
- Existing sources remain authoritative:
|
||||
- Baseline compare state from `BaselineCompareStats`, compare diagnostics, findings, baseline/snapshot truth, and OperationRun evidence.
|
||||
- Restore readiness from wizard state, `RestoreSafetyResolver`, `RestoreReadinessResolver`, backup quality, preview/check state, provider readiness, and existing OperationRun evidence.
|
||||
- Accepted-risk state from `FindingException`, `FindingExceptionDecision`, `FindingRiskGovernanceResolver`, and current evidence references.
|
||||
|
||||
## UI / Filament / Livewire Plan
|
||||
|
||||
- Use native Filament sections, actions, wizard steps, tables, infolists, and existing Blade partials where they already exist.
|
||||
- Keep custom Blade passive. Presenter/page methods decide state; Blade renders.
|
||||
- Keep one visually dominant primary action per touched decision surface.
|
||||
- Keep secondary and technical/audit actions neutral and subordinate.
|
||||
- Keep status colors in badges/labels, not primary buttons.
|
||||
- Use existing `BadgeCatalog` / `BadgeRenderer` where status-like semantics are already connected.
|
||||
- Avoid new CSS or JS assets.
|
||||
- Do not publish Filament internal views.
|
||||
|
||||
## Filament v5 Contract
|
||||
|
||||
- **Livewire v4.0+ compliance**: required; project baseline is Livewire 4.1.4.
|
||||
- **Provider registration**: unchanged in `apps/platform/bootstrap/providers.php`.
|
||||
- **Global search posture**:
|
||||
- `RestoreRunResource`: currently `protected static bool $isGloballySearchable = false`; keep unchanged.
|
||||
- `FindingExceptionResource`: currently `protected static bool $isGloballySearchable = false`; keep unchanged if touched.
|
||||
- `BaselineCompareLanding`: page, not a globally searchable resource.
|
||||
- **Destructive/high-impact actions**:
|
||||
- Baseline Compare `Compare now`: existing high-impact OperationRun start must remain `Action::make(...)->action(...)`, confirmation-protected, capability-gated, and observable.
|
||||
- Restore execution: dry-run default, acknowledgement, typed environment confirmation, confirmation protection, capability checks, and execution gates remain.
|
||||
- Risk Exception `renew_exception` / `revoke_exception`, if touched: both must remain `->requiresConfirmation()`, service-backed, authorized, and notification-backed.
|
||||
- **Asset strategy**: no new assets expected. If implementation registers Filament assets unexpectedly, deployment must include `cd apps/platform && php artisan filament:assets`.
|
||||
|
||||
## RBAC / Policy Plan
|
||||
|
||||
- Baseline Compare remains environment-owned and must not reveal data outside the route workspace/environment.
|
||||
- Restore Run Create remains environment-scoped and requires manage capability for creation/preparation mutation paths.
|
||||
- Finding Exception Detail, if touched, remains environment-scoped with `FINDING_EXCEPTION_VIEW` / manage capability boundaries.
|
||||
- Technical/audit/detail links must apply the same or stricter entitlement as the current default-visible content.
|
||||
- Server-side action handlers remain authoritative for compare, restore, and exception lifecycle mutations.
|
||||
|
||||
## Audit / OperationRun / Evidence Plan
|
||||
|
||||
- No audit stream is removed.
|
||||
- No OperationRun type, lifecycle, notification path, or evidence creation behavior changes.
|
||||
- Default product views should not lead with OperationRun proof; authorized technical/detail paths may still expose it.
|
||||
- Restore execution remains the only path that queues real restore execution.
|
||||
- Baseline Compare `Compare now` remains the only compare-start path touched by this spec.
|
||||
|
||||
## Data / Migration Plan
|
||||
|
||||
- No migrations.
|
||||
- No backfill.
|
||||
- No data compatibility shim.
|
||||
- No new cache/table/column.
|
||||
- No new persisted source of truth.
|
||||
|
||||
If implementation discovers persistence is required, stop and update `spec.md`, `plan.md`, and `tasks.md` with a proportionality review before creating a migration.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Feature / Filament
|
||||
|
||||
Add or update focused tests to prove:
|
||||
|
||||
- Baseline Compare default view has one decision question/action and hides OperationRun proof/raw diagnostics by default.
|
||||
- Baseline Compare shows top material drift/blockers and caps default rows.
|
||||
- Restore Preview default view has one decision summary/action, hides raw payload/OperationRun proof/provider responses, and does not expand all changed/unchanged/reviewed tables by default.
|
||||
- Restore safety controls remain present/reachable and server-side enforced.
|
||||
- Risk Exception raw Source ID/Fingerprint/JSON/evidence references are hidden by default if optional slice is included.
|
||||
- Authorized detail/audit paths remain reachable where repo-supported.
|
||||
|
||||
### Browser
|
||||
|
||||
Create or update a focused browser file such as:
|
||||
|
||||
- `apps/platform/tests/Browser/Spec398DecisionPageContractMigrationSmokeTest.php`
|
||||
|
||||
Browser proof must cover:
|
||||
|
||||
- Baseline Compare.
|
||||
- Restore Preview / Readiness.
|
||||
- Risk Exception Detail only if included.
|
||||
|
||||
Assertions:
|
||||
|
||||
- page loads without 500/Livewire/Filament/console errors
|
||||
- purpose is clear above the fold
|
||||
- one primary decision question visible
|
||||
- one primary action visible
|
||||
- top blockers/differences visible
|
||||
- technical details not default-expanded
|
||||
- OperationRun proof not default-visible
|
||||
- raw evidence/source key/detector/payload not default-visible
|
||||
- long tables capped or moved
|
||||
- restore safety controls still present
|
||||
- destructive/high-impact actions separated where present
|
||||
|
||||
### Validation Commands
|
||||
|
||||
Planned focused commands:
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --filter=Spec398
|
||||
cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec398DecisionPageContractMigrationSmokeTest.php
|
||||
cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Affected existing test candidates:
|
||||
|
||||
```text
|
||||
apps/platform/tests/Feature/Filament/Spec336BaselineCompareProductProcessFlowAlignmentTest.php
|
||||
apps/platform/tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php
|
||||
apps/platform/tests/Feature/Filament/RestorePreviewTest.php
|
||||
apps/platform/tests/Feature/Filament/RestoreRunPreviewProductizationTest.php
|
||||
apps/platform/tests/Feature/Filament/Spec333RestoreCreateUxFinalProductizationTest.php
|
||||
apps/platform/tests/Feature/Filament/Spec390RestoreReadinessGuidanceTest.php
|
||||
apps/platform/tests/Feature/Findings/Spec354FindingExceptionDetailGuidanceTest.php
|
||||
apps/platform/tests/Feature/Filament/FindingExceptionHeaderDisciplineTest.php
|
||||
```
|
||||
|
||||
## Rollout / Deployment Considerations
|
||||
|
||||
- **Environment variables**: none expected.
|
||||
- **Migrations**: none expected.
|
||||
- **Queues / scheduler**: no new queue or scheduler behavior.
|
||||
- **Storage / volumes**: no change.
|
||||
- **Assets**: no new assets expected; `filament:assets` only if implementation unexpectedly registers assets.
|
||||
- **Staging**: focused browser smoke and human sanity should be repeated on staging before production promotion once production exists.
|
||||
- **Production safety**: pre-production no-legacy posture applies; replace old default UI behavior rather than preserving it.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 0 - Discovery And Contract Mapping
|
||||
|
||||
- Confirm current default-visible sections for Baseline Compare, Restore Preview, and optional Risk Exception.
|
||||
- Classify Product Layer vs Technical Evidence Layer for each section.
|
||||
- Confirm exact existing tests that assert old overloaded behavior.
|
||||
- Decide whether Risk Exception Detail is narrow enough to include.
|
||||
|
||||
### Phase 1 - Baseline Compare Migration
|
||||
|
||||
- Update tests for one decision question/action and default technical demotion.
|
||||
- Rework `BaselineCompareLanding` / Blade output to keep decision status, scope, freshness, recommendation, top drift/blockers, impact, and one action first.
|
||||
- Move OperationRun proof, raw diagnostics, evidence-gap internals, full tables, and matrix/proof paths behind secondary/detail/audit disclosure.
|
||||
- Preserve compare-start confirmation/capability/OperationRun behavior.
|
||||
|
||||
### Phase 2 - Restore Preview / Readiness Migration
|
||||
|
||||
- Update tests for one restore safety decision, top blockers/warnings, table caps, and preserved safety controls.
|
||||
- Rework `RestoreRunCreatePresenter` / `RestoreRunResource` wizard output to reduce duplicate readiness/proof cards and demote raw preview/provider/OperationRun detail.
|
||||
- Keep final restore confirmation, acknowledgement, dry-run, typed environment confirmation, and capability checks intact.
|
||||
|
||||
### Phase 3 - Optional Risk Exception Cleanup
|
||||
|
||||
- Include only if discovery confirms narrow default-visible raw evidence demotion.
|
||||
- Update tests to prove Source ID, Fingerprint, JSON summary payload, and raw evidence references are not default-visible.
|
||||
- Preserve existing Spec 354 guidance and renew/revoke action safety.
|
||||
- If not included, record the defer rationale in the implementation report.
|
||||
|
||||
### Phase 4 - UI Coverage, Browser, Human Sanity, Close-Out
|
||||
|
||||
- Update route inventory/design coverage matrix for changed surfaces.
|
||||
- Run focused Feature/Filament tests.
|
||||
- Run focused browser smoke.
|
||||
- Complete Human Product Sanity.
|
||||
- Complete implementation report with Product Surface Contract close-out fields.
|
||||
|
||||
## Risks And Controls
|
||||
|
||||
| Risk | Likelihood | Impact | Control |
|
||||
|---|---:|---:|---|
|
||||
| Baseline Compare loses needed diagnostics | Medium | Medium | Keep authorized detail/audit disclosure and tests. |
|
||||
| Restore safety weakened by reducing visible sections | Low | High | Preserve existing final confirmation, dry-run, typed confirmation, capabilities, and server-side gates. |
|
||||
| Risk Exception scope reopens Spec 354 | Medium | Medium | Optional slice with hard defer if broader redesign is required. |
|
||||
| Implementation adds a generic decision framework | Low | High | Proportionality stop condition and tasks forbid new framework/persistence. |
|
||||
| Browser fixture cost grows too much | Medium | Medium | Focused Spec 398 smoke only; document unrelated full-suite failures separately. |
|
||||
|
||||
## Stop Conditions
|
||||
|
||||
Stop and update spec/plan before continuing if implementation requires:
|
||||
|
||||
- a migration, new table, new persisted state, or compatibility shim
|
||||
- a new Product Surface runtime framework, presenter family, enum/status family, or component system
|
||||
- new routes, navigation, panel providers, global search behavior, packages, assets, queues, scheduler, or env vars
|
||||
- changes to restore execution semantics
|
||||
- a broad accepted-risk workflow redesign
|
||||
- rewriting completed specs or removing historical validation/browser/close-out evidence
|
||||
|
||||
## Implementation Report Requirements
|
||||
|
||||
Implementation must create or update `specs/398-decision-page-contract-migration/implementation-report.md` with:
|
||||
|
||||
1. Files changed.
|
||||
2. Target decision surfaces changed.
|
||||
3. Primary decision question per touched surface.
|
||||
4. Before/after summary of visible sections.
|
||||
5. Primary action chosen per touched surface.
|
||||
6. Technical details moved/collapsed/demoted.
|
||||
7. Tables capped or moved.
|
||||
8. Status labels normalized.
|
||||
9. Destructive/high-impact actions separated where touched.
|
||||
10. Tests added/updated.
|
||||
11. Browser smokes run.
|
||||
12. Human sanity check status.
|
||||
13. No-legacy confirmation.
|
||||
14. No point-fix-only implementation confirmation.
|
||||
15. No Product Surface runtime framework confirmation.
|
||||
16. Visible complexity outcome.
|
||||
17. Livewire v4 compliance.
|
||||
18. Provider registration location.
|
||||
19. Global search posture.
|
||||
20. Asset strategy and whether `filament:assets` is required.
|
||||
21. Deployment impact.
|
||||
22. Known unrelated failures.
|
||||
23. Screenshot artifacts cleaned or intentionally retained.
|
||||
418
specs/398-decision-page-contract-migration/spec.md
Normal file
418
specs/398-decision-page-contract-migration/spec.md
Normal file
@ -0,0 +1,418 @@
|
||||
# Feature Specification: Spec 398 - Decision Page Contract Migration v1
|
||||
|
||||
**Feature Branch**: `398-decision-page-contract-migration`
|
||||
**Created**: 2026-06-22
|
||||
**Status**: Draft / Ready for preparation review
|
||||
**Type**: Product Surface Contract migration / decision-page runtime reduction
|
||||
**Input**: User-provided "Spec 398 - Decision Page Contract Migration v1" candidate, plus repo truth from Specs 336, 354, 390, 395, 397, `docs/product/spec-candidates.md`, `docs/product/roadmap.md`, and `docs/product/standards/product-surface-contract.md`.
|
||||
|
||||
## Candidate Selection Context
|
||||
|
||||
- **Selected candidate**: Decision Page Contract Migration v1.
|
||||
- **Source**: Direct user-provided Spec 398 draft. The active automatic queue in `docs/product/spec-candidates.md` remains empty, but Spec 395 explicitly recorded a "Decision Page Reduction Pass" follow-up covering Baseline Compare and Restore Preview.
|
||||
- **Why selected**: This is the next explicit Product Surface Contract runtime-reduction slice after Spec 397 completed the receipt-page pass. It targets existing overloaded decision-family surfaces instead of adding new status models, persisted truth, or a runtime product-surface framework.
|
||||
- **Close alternatives deferred**:
|
||||
- `management-report-pdf-staging-runtime-validation`: already represented by Spec 380 and not a decision-page migration.
|
||||
- `governance-artifact-lifecycle-retention-runtime`: manual-promotion only and broader than the selected page-archetype reduction.
|
||||
- `provider-readiness-onboarding-productization`: optional/manual provider-readiness work, not a decision-page contract pass.
|
||||
- `cross-domain-indicator-runtime-follow-through`: broader guardrail follow-through; this spec is page-archetype based and narrower.
|
||||
- `Dashboard and Inbox Link Budget Pass`: explicitly remains a later Product Surface Contract follow-up.
|
||||
- Review Publication Resolution / Decision Register work: already repo-real or completed context; not reopened here.
|
||||
- **Roadmap relationship**: Supports the roadmap's UI/Product Maturity Polish lane and the Product Surface Contract runtime-reduction direction without reopening completed productization lanes.
|
||||
- **Completed-spec guardrail result**: Specs 336, 354, 390, 395, and 397 are context only. Completed close-out, validation, screenshots, browser evidence, and checked task history must not be rewritten. Spec 395 deferred a Decision Page Reduction Pass, so this package is a new preparation target rather than a rewrite of Spec 395 or Spec 397.
|
||||
- **Smallest viable implementation slice**: Migrate Baseline Compare and Restore Preview / Restore Readiness default decision surfaces to the existing Product Surface Contract. Include Risk Exception Detail only if discovery confirms the cleanup is narrow and does not compete with Spec 354's accepted-risk guidance work.
|
||||
|
||||
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: Several decision-family pages still ask operators to choose a safe action while also showing proof panels, readiness flows, diagnostics, long tables, raw evidence links, OperationRun proof, and multiple competing actions by default.
|
||||
- **Today's failure**: Operators must reconstruct the primary decision from mixed technical sections. Baseline Compare can read like a workbench/proof explorer, Restore Preview can read like several readiness models at once, and Risk Exception Detail can expose raw accepted-risk evidence fields beside renewal/revocation actions.
|
||||
- **User-visible improvement**: Operators see one primary decision question, one primary action, top material blockers or differences, and a compact impact summary. Full diagnostics remain available deliberately for authorized users.
|
||||
- **Smallest enterprise-capable version**: A product-surface migration over existing pages and presenters only. Reduce default-visible technical detail, cap or move long tables, map top-level statuses to the existing Product Surface vocabulary, preserve restore/risk safety, and verify with focused Feature/Filament plus browser proof.
|
||||
- **Explicit non-goals**: No new Product Surface runtime framework, no new presenter family, no new status enum family, no new persisted taxonomy, no restore execution redesign, no baseline engine rebuild, no review-publication rewrite, no Governance Inbox or Decision Register reopen, no broad dashboard/inbox reduction, and no compatibility mode for old overloaded layouts.
|
||||
- **Permanent complexity imported**: Focused tests, browser proof, and possibly page-local helper cleanup if existing code becomes less reviewable. No new database table, migration, provider framework, status taxonomy, cross-domain UI framework, or broad abstraction is approved by this spec.
|
||||
- **Why now**: Spec 395 installed the Product Surface Contract gate, Spec 397 completed the receipt-page reduction pass, and the user provided the next explicit decision-page migration candidate.
|
||||
- **Why not local**: One-card or one-label tweaks would leave the same contract failure on adjacent decision pages. This spec keeps the migration cross-surface enough to enforce the Decision Page contract while still bounded to named pages.
|
||||
- **Approval class**: Cleanup / Workflow Compression.
|
||||
- **Red flags triggered**: Multiple surfaces and UI contract migration. Defense: the scope reduces visible complexity on existing surfaces, adds no new product truth, preserves existing safety controls, and forbids broad frameworks or compatibility paths.
|
||||
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 12/12**
|
||||
- **Decision**: approve as a bounded Product Surface Contract decision-page migration slice.
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: canonical-view over existing `/admin` workspace/environment decision-family surfaces.
|
||||
- **Primary Routes**:
|
||||
- Baseline Compare: `/admin/workspaces/{workspace}/environments/{environment}/baseline-compare`
|
||||
- Restore Preview / Restore Readiness: `/admin/workspaces/{workspace}/environments/{environment}/restore-runs/create`
|
||||
- Optional Risk Exception Detail: `/admin/workspaces/{workspace}/environments/{environment}/finding-exceptions/{record}`
|
||||
- **Data Ownership**:
|
||||
- Baseline Compare remains derived from existing `ManagedEnvironment`, `BaselineProfile`, `BaselineTenantAssignment`, `OperationRun`, `Finding`, `InventoryItem`, baseline snapshots, and existing compare diagnostics.
|
||||
- Restore Preview remains derived from existing wizard state, `BackupSet`, `BackupItem`, `RestoreRun`, restore safety/check/preview state, provider readiness, and existing OperationRun/evidence links.
|
||||
- Risk Exception remains owned by existing `FindingException`, `FindingExceptionDecision`, linked `Finding`, and existing evidence-reference records.
|
||||
- No new persisted decision-page state is introduced.
|
||||
- **RBAC**:
|
||||
- Existing workspace membership, managed-environment entitlement, capabilities, policies, `UiEnforcement`, and deny-as-not-found behavior remain authoritative.
|
||||
- Technical/audit detail requires the same or stricter authorization as today.
|
||||
- UI visibility must not become the only security control.
|
||||
|
||||
For canonical-view specs:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: Target pages remain route-owned by explicit workspace and managed environment route parameters. Hidden session or shell context must not change which environment or record is displayed.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: All linked baselines, findings, operation runs, backup sets, restore runs, evidence references, and technical/audit paths must resolve through existing workspace/environment entitlement checks before display.
|
||||
|
||||
## No Legacy / No Backward Compatibility Constraint *(mandatory)*
|
||||
|
||||
TenantPilot is pre-production for this product-surface behavior.
|
||||
|
||||
- **Compatibility posture**: canonical replacement.
|
||||
- **Legacy aliases, fallback readers, hidden routes, duplicate UI, old labels, or historical fixtures kept?**: no.
|
||||
- **Why clean replacement is safe now**: There is no production customer-data compatibility requirement. Existing tests that assert old default-visible proof panels, broad diagnostics, all preview tables, or raw evidence payloads must be updated to the canonical decision-page behavior.
|
||||
|
||||
Forbidden implementation behavior:
|
||||
|
||||
- Keeping old overloaded layouts in parallel.
|
||||
- Adding a toggle that switches between old and new decision models.
|
||||
- Renaming proof panels while leaving them default-visible.
|
||||
- Preserving broad tables because existing tests expect them.
|
||||
- Adding compatibility helpers for old action labels, route aliases, or decision states.
|
||||
- Treating OperationRun proof as default product content on decision pages.
|
||||
|
||||
## UI Surface Impact *(mandatory - UI-COV-001)*
|
||||
|
||||
Does this spec add, remove, rename, or materially change any reachable UI surface?
|
||||
|
||||
- [ ] No UI surface impact
|
||||
- [x] Existing page changed
|
||||
- [ ] New page/route added
|
||||
- [ ] Navigation changed
|
||||
- [ ] Filament panel/provider surface changed
|
||||
- [ ] New modal/drawer/wizard/action added
|
||||
- [x] Existing table/form/state presentation changed
|
||||
- [ ] Customer-facing surface changed
|
||||
- [x] Dangerous or high-impact action presentation changed
|
||||
- [x] Status/evidence/review presentation changed
|
||||
- [x] Workspace/environment context presentation changed
|
||||
|
||||
## UI/Productization Coverage *(mandatory when UI Surface Impact is not "No UI surface impact")*
|
||||
|
||||
| Route/page/surface | Current/new archetype | Design depth | Repo-truth level | Existing pattern reused | New pattern required | Screenshot/browser proof | Page audit required | Customer-safe review | Dangerous-action review |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| Baseline Compare Landing | Decision Page | Strategic Surface | repo-verified | `BaselineCompareLanding`, `baseline-compare-landing` Blade, Spec 336 flow pattern, Product Surface Contract | none expected; page-local reduction only | yes | no full page audit unless classification changes | no customer default; operator-safe copy required | yes for `Compare now` high-impact operation start |
|
||||
| Restore Run Create preview/readiness decision section | Wizard Page with Decision Sub-Surface | Strategic Surface | repo-verified | `RestoreRunResource`, `CreateRestoreRun`, `RestoreRunCreatePresenter`, restore safety/resolution support from Specs 332/390 | none expected; reuse existing presenter/wizard patterns | yes | no full page audit unless route/archetype changes | no | yes for restore execution/dry-run/confirmation controls |
|
||||
| Risk Exception Detail (optional) | Decision Page | Strategic Surface | repo-verified | `FindingExceptionResource`, `ViewFindingException`, `accepted-risk-guidance`, Spec 354 accepted-risk guidance | none expected; include only if narrow | yes if touched | no full page audit unless UI-036 registry is inaccurate | yes for accepted-risk wording continuity | yes for renew/revoke |
|
||||
|
||||
Coverage files to update or explicitly not needed during implementation:
|
||||
|
||||
- [x] `docs/ui-ux-enterprise-audit/route-inventory.md`
|
||||
- [x] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/page-reports/...`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/strategic-surfaces.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/unresolved-pages.md`
|
||||
- [ ] `N/A - no reachable UI surface impact`
|
||||
|
||||
Spec-level coverage registry decision:
|
||||
|
||||
| Coverage artifact | Decision | Reason |
|
||||
|---|---|---|
|
||||
| `docs/ui-ux-enterprise-audit/route-inventory.md` | Update during implementation for each target surface whose default rendered UI changes. | Decision Page classification and Product Surface migration must be visible in durable route inventory. |
|
||||
| `docs/ui-ux-enterprise-audit/design-coverage-matrix.md` | Update during implementation for each target surface whose default rendered UI changes. | The matrix must reflect the Decision Page migration and focused browser-proof expectation. |
|
||||
| `docs/ui-ux-enterprise-audit/page-reports/...` | Not required by default. | Add or update page reports only if implementation changes archetype, expands default content, or discovers the existing page report is materially inaccurate. |
|
||||
| `docs/ui-ux-enterprise-audit/strategic-surfaces.md` | Not required by default. | No new strategic surface or navigation prominence is introduced. |
|
||||
| `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md` | Not required by default. | Follow-up candidates are recorded in this active spec. |
|
||||
| `docs/ui-ux-enterprise-audit/unresolved-pages.md` | Not required unless implementation cannot classify or render a touched surface. | Planned target surfaces are classifiable as Decision Page or Wizard Page with Decision Sub-Surface. |
|
||||
|
||||
## Product Surface Impact *(mandatory for UI-affecting specs)*
|
||||
|
||||
Reference: `docs/product/standards/product-surface-contract.md`.
|
||||
|
||||
- **Product Surface Contract applies?**: yes. This spec materially changes rendered product decision pages.
|
||||
- **Page archetype**: Baseline Compare and Risk Exception Detail are Decision Pages. Restore Run Create remains a Wizard Page, but its preview/readiness section must behave as a Decision Sub-Surface.
|
||||
- **Primary user question**:
|
||||
- Baseline Compare: Which baseline drift requires action, and what should I do next?
|
||||
- Restore Preview / Readiness: Is this restore safe to continue, and what must be resolved before execution?
|
||||
- Risk Exception Detail, if touched: Should this accepted risk remain active, be renewed, or be revoked?
|
||||
- **Primary action**: Exactly one state-based product action per touched surface.
|
||||
- **Surface budget result**: planned pass. Any exception must be documented in this spec before runtime UI edits continue.
|
||||
- **Technical Annex / deep-link demotion**: OperationRun links, raw evidence links, source keys, detectors, fingerprints, UUIDs, JSON payloads, raw provider responses, low-level validation logs, full diff tables, and full diagnostics must be hidden, collapsed, capped, or moved to authorized detail/audit paths by default.
|
||||
- **Canonical status vocabulary**: Use `Ready`, `Needs attention`, `Blocked`, `Running`, `Failed`, `Expired`, `Not configured`, `Unknown`, `Historical`, `Superseded`, plus allowed severity states where needed.
|
||||
- **Visible complexity impact**: decreased.
|
||||
- **Product Surface exceptions**: none planned. Any unavoidable exception is a stop condition requiring spec and plan update.
|
||||
|
||||
## UI Action Matrix
|
||||
|
||||
| Surface | Primary action | Secondary actions | Technical/audit actions | Dangerous/high-impact actions | Mutation scope |
|
||||
|---|---|---|---|---|---|
|
||||
| Baseline Compare Landing | One state-based action such as `Review material drift`, `Refresh comparison`, `Resolve blockers`, `Retry comparison`, or `Return to overview`. | Up to two contextual actions such as `View full comparison`, `Open baseline snapshot`, or `View audit trail`. | `View details`, `View full comparison`, or `View audit trail`; not default-dominant and only when authorized. | `Compare now` remains a high-impact OperationRun start when present; it must stay confirmed, capability-gated, and audited/observable through existing paths. | Mostly read-only decision rendering; compare start queues existing TenantPilot operation only. |
|
||||
| Restore Preview / Readiness | One next wizard action such as `Continue to confirmation`, `Run readiness checks`, `Regenerate preview`, `Resolve blockers`, or `Review restore readiness`. | Up to two contextual actions such as `View restore details`, `View diagnostics`, or `Open backup set` when relevant and authorized. | `View restore diagnostics` or `View audit trail`; raw payload/provider/log details hidden by default. | Restore execution remains high-impact and must retain dry-run default, acknowledgement, typed environment confirmation, confirmation protection, and capability checks. | Wizard preparation remains TenantPilot-owned until execution; real provider mutation remains existing restore execution path only. |
|
||||
| Risk Exception Detail (optional) | One state-based action such as `Review renewal`, `Review exception`, `Resolve blockers`, or `Return to accepted risk`. | Up to two contextual actions such as `Open finding` or `Return to queue`. | `View internal evidence details` or `View audit trail`; raw evidence fields hidden by default. | `Renew exception` and `Revoke exception` remain confirmed, authorized, notification-backed, and visually separated; `Revoke` must not compete as a normal primary action. | Mutations affect existing `FindingException` lifecycle only through current service/audit paths. |
|
||||
|
||||
## Browser Verification Plan *(mandatory)*
|
||||
|
||||
- **Browser proof required?**: yes.
|
||||
- **No-browser rationale**: N/A - rendered UI changes are the purpose of this spec.
|
||||
- **Focused path when required**:
|
||||
- Baseline Compare route for at least one material drift or blocker state.
|
||||
- Restore Run Create wizard through the preview/readiness decision section.
|
||||
- Risk Exception Detail only if implementation includes the optional cleanup.
|
||||
- **Primary interaction to execute**:
|
||||
- Render first viewport and confirm one primary decision question/action.
|
||||
- Expand the detail/audit disclosure where present and confirm technical detail is deliberate, not default-visible.
|
||||
- For Restore, verify dry-run/acknowledgement/typed confirmation controls remain visible or reachable in the correct step.
|
||||
- **Console, Livewire, Filament, network, and 500-error checks**: required for focused browser smoke.
|
||||
- **Full-suite failure triage**: If unrelated browser failures remain, implementation must document the command, affected failures, why they are unrelated, and prove no touched decision surface failed.
|
||||
|
||||
## Human Product Sanity Check *(mandatory)*
|
||||
|
||||
- **Required?**: yes.
|
||||
- **No-human-sanity rationale**: N/A - rendered product decision surfaces change.
|
||||
- **Reviewer questions**:
|
||||
- Does the page feel like a decision page, not a dashboard/workbench?
|
||||
- Is the primary decision question obvious above the fold?
|
||||
- Is there exactly one dominant next action?
|
||||
- Are only the most material blockers/differences visible by default?
|
||||
- Are technical details demoted?
|
||||
- Are long tables hidden, capped, or moved?
|
||||
- Are restore safety controls still clear?
|
||||
- Are destructive/high-impact actions separated?
|
||||
- Does the page feel less complex than before?
|
||||
- Would an operator trust the recommendation?
|
||||
- **Planned result location**: implementation report and PR close-out.
|
||||
|
||||
## Product Surface Merge Gate Checklist *(mandatory)*
|
||||
|
||||
- [ ] No-legacy posture or approved exception recorded.
|
||||
- [ ] Product Surface Impact is completed and exceptions are `none` or explicitly approved.
|
||||
- [ ] Browser proof is completed for every rendered UI surface changed.
|
||||
- [ ] Human Product Sanity is completed.
|
||||
- [ ] Implementation report states Livewire v4 compliance, provider registration location, global search posture, destructive/high-impact action posture, asset strategy, tests/browser result, deployment impact, and visible complexity outcome.
|
||||
|
||||
## Shared Pattern Reuse
|
||||
|
||||
- **Cross-cutting feature?**: yes.
|
||||
- **Interaction classes**: status messaging, decision cards, action links, technical/audit links, table caps, restore readiness, accepted-risk actions, OperationRun evidence links.
|
||||
- **Systems touched**:
|
||||
- `App\Filament\Pages\BaselineCompareLanding`
|
||||
- `apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php`
|
||||
- `App\Filament\Resources\RestoreRunResource`
|
||||
- `App\Filament\Resources\RestoreRunResource\Pages\CreateRestoreRun`
|
||||
- `App\Filament\Resources\RestoreRunResource\Presenters\RestoreRunCreatePresenter`
|
||||
- optional `App\Filament\Resources\FindingExceptionResource`
|
||||
- optional `App\Filament\Resources\FindingExceptionResource\Pages\ViewFindingException`
|
||||
- **Existing pattern(s) to extend**: Product Surface Contract, Spec 336 Baseline Compare decision-first flow, Spec 390 Restore readiness guidance, Spec 354 accepted-risk guidance, existing `BadgeCatalog` / `BadgeRenderer`, `UiEnforcement`, `WorkspaceUiEnforcement`, OperationRun URL helpers, Filament sections/details.
|
||||
- **Shared contract / presenter / builder / renderer to reuse**: existing page presenters/helpers first. New helper extraction is allowed only when it reduces real review burden inside a touched surface and does not become a generic decision-page framework.
|
||||
- **Why the existing shared path is sufficient or insufficient**: The Product Surface Contract already supplies the vocabulary and budgets. Existing page presenters already compute much of the state. The remaining gap is default-visible hierarchy and demotion, not missing product truth.
|
||||
- **Allowed deviation and why**: page-local collapsed detail or table cap helpers may be used if existing Filament/presenter structures cannot express the contract cleanly. No new cross-domain Product Surface runtime framework is approved.
|
||||
- **Consistency impact**: decision question, primary action, status vocabulary, technical demotion, and high-impact action separation must stay consistent across Baseline Compare, Restore Preview, and optional Risk Exception Detail.
|
||||
- **Review focus**: no point fixes, no default OperationRun proof, no raw evidence/payload leakage, no duplicate readiness summaries, no fake "all clear", no weakened restore/risk action safety.
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: yes, by demoting default OperationRun proof links on decision surfaces and preserving existing operation-start behavior.
|
||||
- **Shared OperationRun UX contract/layer reused**: existing `OperationRunLinks`, `OperationUxPresenter`, `OpsUxBrowserEvents`, and current operation resource links.
|
||||
- **Delegated start/completion UX behaviors**: existing queued toast, `Open operation` links, run-enqueued browser event, and tenant/workspace-safe operation URL resolution remain delegated to existing helpers.
|
||||
- **Local surface-owned behavior that remains**: deciding whether an operation proof link is product-secondary/audit detail or hidden from default decision view.
|
||||
- **Queued DB-notification policy**: unchanged.
|
||||
- **Terminal notification path**: unchanged.
|
||||
- **Exception required?**: none planned.
|
||||
|
||||
## Provider Boundary / Platform Core Check
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no new shared provider/platform boundary.
|
||||
- **Boundary classification**: N/A - UI hierarchy migration over existing provider-backed state.
|
||||
- **Seams affected**: No contract registry, provider identity, target scope, compare strategy, or Graph endpoint semantics are changed.
|
||||
- **Neutral platform terms preserved or introduced**: use `provider`, `target environment`, `restore source`, `managed environment`, and `governed subject` where those are already repo terms.
|
||||
- **Provider-specific semantics retained and why**: Microsoft/Graph/Entra terminology may remain only where it is existing provider-owned technical/detail context.
|
||||
- **Why this does not deepen provider coupling accidentally**: No new provider-shaped persisted truth, enum, route family, or platform-core taxonomy is introduced.
|
||||
- **Follow-up path**: none planned.
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: no.
|
||||
- **New persisted entity/table/artifact?**: no.
|
||||
- **New abstraction?**: no broad abstraction approved. Page-local helper extraction is allowed only if it replaces scattered logic and stays inside the touched surface.
|
||||
- **New enum/state/reason family?**: no. Use existing Product Surface Contract vocabulary as display contract.
|
||||
- **New cross-domain UI framework/taxonomy?**: no.
|
||||
- **Current operator problem**: overloaded decision pages hide the one decision and next action behind proof, diagnostics, and competing tables/actions.
|
||||
- **Existing structure is insufficient because**: the data exists, but default rendering violates Product Surface budgets and deep-link demotion rules.
|
||||
- **Narrowest correct implementation**: reduce existing page defaults and tests; do not introduce new decision truth.
|
||||
- **Ownership cost**: focused tests, browser proof, and page-specific cleanup only.
|
||||
- **Alternative intentionally rejected**: a reusable Decision Page engine, persisted decision state, generic technical-annex framework, or broad dashboard/inbox migration.
|
||||
- **Release truth**: current-release productization cleanup.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Feature/Filament for rendered page/action behavior, Unit only for pure local helpers if introduced, Browser for focused UI smoke.
|
||||
- **Validation lane(s)**: confidence + browser; fast-feedback for pure helper tests; `git diff --check`; Pint for touched PHP.
|
||||
- **Why this classification and these lanes are sufficient**: The change is visible UI hierarchy over existing state and safety controls. Feature/Filament tests prove content/action visibility and browser smoke proves first-viewport decision clarity and runtime health.
|
||||
- **New or expanded test families**: one focused Spec 398 browser smoke may be added; no broad visual regression family.
|
||||
- **Fixture / helper cost impact**: Use existing workspace, managed environment, backup/restore, baseline, finding exception, and browser fixtures. Do not widen shared helpers with implicit provider/workspace/session defaults.
|
||||
- **Heavy-family visibility / justification**: Browser coverage is explicit because rendered decision surfaces are the product change.
|
||||
- **Special surface test profile**: Product Surface Decision Page migration.
|
||||
- **Reviewer handoff**: Confirm lane fit, browser proof scope, no hidden helper cost, and no old overloaded expectations preserved.
|
||||
- **Budget / baseline / trend impact**: none expected; document any material browser fixture expansion as `document-in-feature`.
|
||||
- **Escalation needed**: none if implementation stays bounded; `reject-or-split` if it expands into dashboard/inbox, review-publication, or runtime framework scope.
|
||||
- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage.
|
||||
- **Planned validation commands**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=Spec398`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec398DecisionPageContractMigrationSmokeTest.php` if a new browser file is added
|
||||
- affected existing tests for Baseline Compare, Restore Create/Preview, and optional Finding Exception Detail
|
||||
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||
- `git diff --check`
|
||||
|
||||
## User Stories & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Baseline Compare answers one drift decision (Priority: P1)
|
||||
|
||||
As a governance operator, I want Baseline Compare to show whether baseline drift requires action and what to do next, so that I do not have to read OperationRun proof, evidence-gap diagnostics, or full compare tables before deciding.
|
||||
|
||||
**Independent Test**: Render Baseline Compare with material drift, blocker/stale/failure, and no-material-drift states. The default view shows one decision question, one status, top material drift or blocker summary, one primary action, and no default OperationRun proof/raw diagnostics.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. Given material drift exists, when an authorized operator opens Baseline Compare, then the page shows top material drift and a single dominant review action.
|
||||
2. Given compare input is missing, stale, blocked, failed, or running, when the page renders, then the top state maps to the canonical status vocabulary and shows one next action.
|
||||
3. Given full diagnostics exist, when the default view renders, then OperationRun proof, raw evidence links, source keys, detector output, raw payloads, and full diagnostics are not default-visible.
|
||||
|
||||
### User Story 2 - Restore Preview answers one safety decision (Priority: P1)
|
||||
|
||||
As a restore operator, I want the Restore Preview / Readiness section to show whether the restore is safe to continue and which blockers must be resolved, so that restore execution remains clear and safe.
|
||||
|
||||
**Independent Test**: Render Restore Run Create wizard states for blocked, needs-attention, and ready paths. The default preview/readiness decision area shows source/target, top blockers or warnings, impact summary, one next wizard action, and preserves dry-run/acknowledgement/typed confirmation safety.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. Given restore blockers exist, when the preview/readiness step renders, then the page shows top blockers and one safe next action without exposing raw restore payload, provider response, or OperationRun proof by default.
|
||||
2. Given restore is ready to continue, when the preview/readiness step renders, then the page shows one continue/final-confirmation action and still requires existing final restore safety controls.
|
||||
3. Given many changed/unchanged/reviewed items exist, when the default view renders, then long tables are capped to at most eight visible rows or moved behind details.
|
||||
|
||||
### User Story 3 - Risk Exception Detail avoids raw evidence leakage if included (Priority: P2)
|
||||
|
||||
As an operator reviewing accepted risk, I want the detail page to show whether the risk should remain active, be renewed, or be revoked without exposing raw source IDs, fingerprints, or JSON payloads by default.
|
||||
|
||||
**Independent Test**: If included, render Risk Exception Detail and assert one accepted-risk decision question/action, demoted raw evidence fields, and preserved renew/revoke confirmation and authorization behavior.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. Given an expiring accepted risk, when an authorized operator opens the detail page, then the page emphasizes renewal review and does not expose raw evidence fields by default.
|
||||
2. Given renew/revoke are available, when the page renders, then those actions remain confirmation-protected, authorized, visually separated, and not both shown as equal primary actions.
|
||||
3. Given the cleanup would require broader accepted-risk redesign, when discovery completes, then Risk Exception Detail is deferred with a documented rationale instead of expanding this spec.
|
||||
|
||||
### User Story 4 - Technical detail remains available deliberately (Priority: P2)
|
||||
|
||||
As a senior engineer or support reviewer, I need authorized access to full diagnostics and audit evidence after the product decision is clear, so that product simplification does not remove auditability.
|
||||
|
||||
**Independent Test**: Feature/Filament tests assert authorized users can still reach existing detail/audit paths while default product views hide raw technical content.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. Given a user lacks the required capability for technical/audit detail, when they access a decision page, then raw internal detail is not visible and scoped authorization remains enforced.
|
||||
2. Given an authorized user opens the deliberate detail/audit path, then full comparison, restore diagnostics, or evidence detail remains reachable where existing routes/actions support it.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
- **FR-398-001**: Baseline Compare MUST default to a Decision Page layout with one primary decision question, decision status, recommendation, top material drift or blocker summary, impact summary, and exactly one primary action.
|
||||
- **FR-398-002**: Baseline Compare MUST NOT show OperationRun proof, raw evidence links, source keys, detector output, raw payloads, full candidate tables, full diff tables, full access diagnostics, or raw evidence-gap internals by default.
|
||||
- **FR-398-003**: Baseline Compare MUST cap default material drift rows to at most eight visible rows, with top three preferred for summary.
|
||||
- **FR-398-004**: Restore Preview / Readiness MUST default to one decision sub-surface with restore decision status, source backup, target environment, top blockers/warnings, impact summary, and exactly one next action.
|
||||
- **FR-398-005**: Restore Preview / Readiness MUST NOT show all summary cards, all changed/unchanged/all-reviewed tables, raw restore payloads, OperationRun proof, internal job IDs, raw provider responses, low-level validation logs, raw backup metadata, or duplicate proof/readiness summaries by default.
|
||||
- **FR-398-006**: Restore Preview / Readiness MUST preserve dry-run default behavior where applicable, acknowledgement, typed environment confirmation, confirmation protection, and capability checks.
|
||||
- **FR-398-007**: If Risk Exception Detail is included, Source ID, Fingerprint, JSON summary payload, raw evidence references, detector output, and source keys MUST NOT be default-visible.
|
||||
- **FR-398-008**: Every touched decision surface MUST have exactly one primary action and at most two default secondary actions.
|
||||
- **FR-398-009**: Full diagnostics, full comparison, restore diagnostics, proof, and raw evidence MAY remain available only through deliberate secondary/detail/audit paths for authorized users.
|
||||
- **FR-398-010**: Top-level decision statuses MUST map to the Product Surface Contract vocabulary.
|
||||
- **FR-398-011**: Destructive/high-impact actions touched by this spec MUST execute through existing Filament action handlers, require confirmation, enforce server-side authorization, and preserve audit/notification behavior.
|
||||
- **FR-398-012**: Implementation MUST NOT add a Product Surface runtime framework, decision presenter family, status enum family, component framework, persisted taxonomy, migration, provider abstraction, or compatibility shim.
|
||||
- **FR-398-013**: Existing tests that assert old overloaded default UI MUST be updated to assert the new decision contract rather than preserving old behavior.
|
||||
- **FR-398-014**: Focused browser smoke MUST cover every touched rendered decision surface.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
- **NFR-398-001**: Default-visible complexity must decrease or remain neutral; any increase requires a documented Product Surface exception before runtime edits continue.
|
||||
- **NFR-398-002**: Page rendering must not introduce Graph calls or remote provider calls.
|
||||
- **NFR-398-003**: Table caps and detail demotion must not hide safety blockers; top material blockers/differences must remain visible.
|
||||
- **NFR-398-004**: Browser proof must remain focused and fixture-bounded, not a broad visual regression suite.
|
||||
- **NFR-398-005**: Test fixture/helper cost must remain explicit and local to this spec.
|
||||
|
||||
## RBAC / Security Requirements
|
||||
|
||||
- Workspace and managed-environment authorization remain non-negotiable and deny-as-not-found for out-of-scope records.
|
||||
- Restore preparation/execution actions continue to require existing manage capabilities and server-side checks.
|
||||
- Finding exception renew/revoke actions continue to require existing manage capabilities and service-layer authorization.
|
||||
- Technical/audit detail links must not reveal inaccessible operation, evidence, backup, baseline, or finding records.
|
||||
- Customer/read-only paths must not expose raw technical decision internals by default.
|
||||
|
||||
## Auditability / Observability Requirements
|
||||
|
||||
- No audit trail is removed.
|
||||
- Existing OperationRun, restore, compare, and finding-exception audit behavior remains authoritative.
|
||||
- Demoting OperationRun proof from default product content must not remove the existing authorized path to technical proof.
|
||||
- Implementation report must state whether any audit/logging behavior changed. The planned answer is `none`.
|
||||
|
||||
## Data / Truth Requirements
|
||||
|
||||
- Existing domain truth remains authoritative:
|
||||
- execution truth: `OperationRun` and existing restore/compare execution paths
|
||||
- artifact truth: existing backup, baseline, evidence, restore, and risk records
|
||||
- backup/snapshot truth: existing `BackupSet`, `BackupItem`, baseline snapshot, and evidence snapshot state
|
||||
- recovery/evidence truth: existing restore preview/check/result/evidence data
|
||||
- operator next action: derived from current page state only
|
||||
- No new persisted decision state, derived cache, or compatibility field is approved.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- Rewriting Spec 395 or Spec 397.
|
||||
- Reopening Review Publication Resolution, Decision Register, Governance Inbox, Customer Review Workspace, Evidence Snapshot, Baseline Snapshot, or System Panel.
|
||||
- Broad dashboard/inbox link-budget pass.
|
||||
- Baseline compare engine rebuild, drift algorithm changes, snapshot generation changes, or new evidence generation.
|
||||
- Restore execution redesign, provider-specific restore adapter expansion, or new restore functionality.
|
||||
- Accepted-risk workflow redesign beyond optional narrow default-visible evidence demotion.
|
||||
- New navigation, panel provider, route family, global search type, migration, queue family, scheduler, env var, storage behavior, package, or runtime framework.
|
||||
|
||||
## Follow-Up Spec Candidates
|
||||
|
||||
- Dashboard and Inbox Link Budget Pass: Environment Dashboard, Workspace Overview, Governance Inbox, Findings, and Operations Hub.
|
||||
- Review Publication Resolution decision-page check only if fresh evidence shows Product Surface regression after this spec.
|
||||
- Decision Page Contract hardening for additional surfaces discovered during implementation, recorded separately rather than hidden in this slice.
|
||||
- Cross-domain indicator runtime follow-through, if status/readiness drift remains after page-level reductions.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. Baseline Compare default view shows one primary decision question, decision status, recommendation, top material drift or blockers, impact summary, and one primary action.
|
||||
2. Baseline Compare default view does not show OperationRun proof, full diagnostics, raw evidence links, source keys, detector output, raw payloads, or long uncapped tables.
|
||||
3. Restore Preview / Readiness default decision view shows source/target, restore decision status, top blockers/warnings, impact summary, and one next action.
|
||||
4. Restore Preview / Readiness default view does not show all summary cards, all preview tables, raw restore payloads, OperationRun proof, raw provider responses, or duplicate readiness/proof summaries.
|
||||
5. Restore execution safety controls remain intact.
|
||||
6. Risk Exception Detail, if touched, hides raw source/fingerprint/JSON/evidence fields by default and keeps renew/revoke safe.
|
||||
7. Every touched surface has exactly one primary action and at most two default secondary actions.
|
||||
8. Technical details remain reachable only through deliberate authorized detail/audit paths.
|
||||
9. Default tables show at most eight visible rows or are moved behind details.
|
||||
10. Focused Feature/Filament tests and focused browser proof pass, or unrelated failures are documented honestly.
|
||||
11. No new Product Surface runtime framework, status family, persisted truth, or compatibility mode is created.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Operators can identify the decision and next action within a few seconds on each touched surface.
|
||||
- Product Surface Contract browser proof passes for Baseline Compare and Restore Preview / Readiness.
|
||||
- Visible technical proof and raw-detail exposure decreases by default.
|
||||
- Restore safety and risk-exception lifecycle safety remain intact.
|
||||
- Implementation report confirms no legacy behavior, no point-fix-only implementation, and no runtime framework was introduced.
|
||||
|
||||
## Risks And Mitigations
|
||||
|
||||
- **Risk: Operators lose access to full diagnostics.** Mitigation: retain authorized secondary/detail/audit paths and test their availability where supported.
|
||||
- **Risk: Restore safety weakens during UI simplification.** Mitigation: make dry-run, acknowledgement, typed confirmation, confirmation protection, and capability checks explicit acceptance criteria and browser assertions.
|
||||
- **Risk: Tests preserve old overloaded defaults.** Mitigation: update old expectations to assert the new contract; no compatibility behavior.
|
||||
- **Risk: Scope expands into broad redesign.** Mitigation: keep primary scope to Baseline Compare and Restore Preview / Readiness; Risk Exception is optional only if narrow.
|
||||
- **Risk: Implementation creates a forbidden framework.** Mitigation: proportionality review forbids new persisted truth, enum/status family, presenter family, or product-surface runtime framework.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- The repo remains pre-production under the constitution's no-legacy posture.
|
||||
- Spec 395 Product Surface Contract is the source of truth for page budgets and deep-link demotion.
|
||||
- Baseline Compare and Restore Preview have enough existing state to derive product decisions without new persistence.
|
||||
- Risk Exception Detail may be skipped if implementing it safely would reopen Spec 354 or require broad accepted-risk redesign.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None blocking preparation.
|
||||
- Implementation discovery must decide whether Risk Exception Detail is included or explicitly deferred with rationale.
|
||||
157
specs/398-decision-page-contract-migration/tasks.md
Normal file
157
specs/398-decision-page-contract-migration/tasks.md
Normal file
@ -0,0 +1,157 @@
|
||||
# Tasks: Spec 398 - Decision Page Contract Migration v1
|
||||
|
||||
**Input**: `specs/398-decision-page-contract-migration/spec.md`, `specs/398-decision-page-contract-migration/plan.md`
|
||||
**Prerequisites**: Existing `/admin` decision-family surfaces, Product Surface Contract, Filament v5 / Livewire v4
|
||||
**Tests**: Required. This spec changes rendered UI defaults and must include Feature/Filament proof plus focused browser smoke.
|
||||
**Implementation status**: Keep all tasks unchecked until implementation work is actually completed.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
|
||||
- [x] New or changed tests stay in the smallest honest family; Browser coverage is one explicit focused decision-page proof path.
|
||||
- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default; any widening is isolated or documented.
|
||||
- [x] Planned validation commands cover the changed decision surfaces without pulling in unrelated lane cost.
|
||||
- [x] The declared surface test profile is explicit: Product Surface Decision Page migration.
|
||||
- [x] Browser proof is required because rendered UI changes are expected.
|
||||
- [x] Human Product Sanity and Product Surface implementation-report close-out are planned and completed before implementation close-out.
|
||||
- [x] Any material budget, baseline, trend, or escalation note is recorded in the active spec or implementation report.
|
||||
|
||||
## Phase 1: Discovery And Guardrails
|
||||
|
||||
**Purpose**: Confirm current render paths, overloaded default content, and existing tests before runtime edits.
|
||||
|
||||
- [x] T001 Re-read `specs/398-decision-page-contract-migration/spec.md`, `specs/398-decision-page-contract-migration/plan.md`, and `docs/product/standards/product-surface-contract.md`; record selected implementation slice in `specs/398-decision-page-contract-migration/implementation-report.md`.
|
||||
- [x] T002 Inspect Baseline Compare default-visible decision, proof, readiness, diagnostics, matrix, evidence-gap, and action sections in `apps/platform/app/Filament/Pages/BaselineCompareLanding.php` and `apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php`.
|
||||
- [x] T003 Inspect Baseline Compare tests that may assert old proof/diagnostics visibility in `apps/platform/tests/Feature/Filament/Spec336BaselineCompareProductProcessFlowAlignmentTest.php`, `apps/platform/tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php`, and related Baseline Compare feature files.
|
||||
- [x] T004 Inspect Restore Preview / Readiness decision sections, summary cards, preview tables, diagnostics, proof links, and safety controls in `apps/platform/app/Filament/Resources/RestoreRunResource.php`, `apps/platform/app/Filament/Resources/RestoreRunResource/Pages/CreateRestoreRun.php`, and `apps/platform/app/Filament/Resources/RestoreRunResource/Presenters/RestoreRunCreatePresenter.php`.
|
||||
- [x] T005 Inspect Restore Preview tests that may assert old broad preview/readiness content in `apps/platform/tests/Feature/Filament/RestorePreviewTest.php`, `apps/platform/tests/Feature/Filament/RestoreRunPreviewProductizationTest.php`, `apps/platform/tests/Feature/Filament/Spec333RestoreCreateUxFinalProductizationTest.php`, and `apps/platform/tests/Feature/Filament/Spec390RestoreReadinessGuidanceTest.php`.
|
||||
- [x] T006 Decide whether optional Risk Exception Detail is narrow enough to include by inspecting `apps/platform/app/Filament/Resources/FindingExceptionResource.php`, `apps/platform/app/Filament/Resources/FindingExceptionResource/Pages/ViewFindingException.php`, and existing Spec 354 tests.
|
||||
- [x] T007 Confirm global search posture remains unchanged for `RestoreRunResource` and optional `FindingExceptionResource`; record in implementation report.
|
||||
- [x] T008 Confirm destructive/high-impact actions that may be touched and their confirmation/authorization/audit coverage: Baseline Compare `Compare now`, Restore execution/confirmation, and optional Risk Exception `renew_exception` / `revoke_exception`.
|
||||
- [x] T009 Decide whether existing browser tests can carry Spec 398 proof or whether a new `apps/platform/tests/Browser/Spec398DecisionPageContractMigrationSmokeTest.php` file is required; record the decision.
|
||||
|
||||
## Phase 2: Foundational Test Harness
|
||||
|
||||
**Purpose**: Add failing or adjusted tests before/alongside implementation so the decision-page contract is explicit.
|
||||
|
||||
- [x] T010 [P] Add or update Baseline Compare Feature/Filament assertions for one primary decision question, one primary action, top material drift/blocker visibility, no default OperationRun proof, no raw evidence links/source keys/detector output, and capped default rows.
|
||||
- [x] T011 [P] Add or update Restore Preview / Readiness Feature/Filament assertions for one restore safety decision, one next action, top blockers/warnings, no default raw payload/OperationRun proof/provider responses, no all-expanded changed/unchanged/reviewed tables, and preserved safety controls.
|
||||
- [x] T012 [P] If Risk Exception Detail is included, add or update assertions for hidden Source ID, Fingerprint, JSON summary payload, and raw evidence references by default while renew/revoke remain confirmed and authorized. N/A - Risk Exception deferred.
|
||||
- [x] T013 Add or update authorized detail/audit path assertions for Baseline Compare and Restore Preview; include optional Risk Exception technical detail only if touched.
|
||||
- [x] T014 Add or update focused browser proof in `apps/platform/tests/Browser/Spec398DecisionPageContractMigrationSmokeTest.php` unless T009 proves existing browser files cover every required surface. Existing browser files carry proof.
|
||||
|
||||
## Phase 3: User Story 1 - Baseline Compare Decision Migration
|
||||
|
||||
**Goal**: Baseline Compare answers which baseline drift requires action and what the operator should do next.
|
||||
|
||||
**Independent Test**: Baseline Compare Feature/Filament and browser tests pass with decision-first default content and demoted technical proof.
|
||||
|
||||
- [x] T015 [US1] Update Baseline Compare decision card/status mapping in `apps/platform/app/Filament/Pages/BaselineCompareLanding.php` to use Product Surface status vocabulary.
|
||||
- [x] T016 [US1] Ensure `apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php` shows one primary decision question, recommendation, impact, top material drift/blockers, and one primary action above technical/proof detail.
|
||||
- [x] T017 [US1] Demote OperationRun proof from default product content in `BaselineCompareLanding.php` and the Blade view; keep authorized technical/audit access where repo-supported.
|
||||
- [x] T018 [US1] Move or collapse full diagnostics, evidence-gap internals, source keys, detector output, raw payloads, RBAC diagnostic grids, and full compare/matrix launch details behind secondary/detail/audit paths.
|
||||
- [x] T019 [US1] Cap default Baseline Compare material drift/detail rows to at most eight visible rows, preferring top three material items for summary.
|
||||
- [x] T020 [US1] Preserve `Compare now` confirmation, capability gating, OperationRun creation/link behavior, queued toast, and browser event behavior.
|
||||
- [x] T021 [US1] Run the focused Baseline Compare tests selected in T010 and document results in `implementation-report.md`.
|
||||
|
||||
## Phase 4: User Story 2 - Restore Preview / Readiness Decision Migration
|
||||
|
||||
**Goal**: Restore Preview / Readiness answers whether restore is safe to continue and what must be resolved before execution.
|
||||
|
||||
**Independent Test**: Restore Feature/Filament and browser tests pass with a compact decision sub-surface and preserved restore safety controls.
|
||||
|
||||
- [x] T022 [US2] Update `RestoreRunCreatePresenter` output to prefer one restore decision status, source/target, top blockers/warnings, impact summary, and one next action.
|
||||
- [x] T023 [US2] Reduce duplicate readiness/proof summaries and broad default summary card count in `RestoreRunResource` / `RestoreRunCreatePresenter` without removing safety blockers.
|
||||
- [x] T024 [US2] Move or collapse raw restore payloads, OperationRun proof, internal job IDs, raw provider responses, low-level validation logs, raw backup metadata, and full diagnostics behind deliberate detail/audit paths.
|
||||
- [x] T025 [US2] Cap default changed/unchanged/all-reviewed or equivalent restore preview tables to at most eight visible rows, or move full tables behind details.
|
||||
- [x] T026 [US2] Preserve dry-run default behavior where applicable, acknowledgement, typed environment confirmation, confirmation protection, capability checks, and final execution gates.
|
||||
- [x] T027 [US2] Preserve existing restore preparation and execution authorization tests, adding assertions only where the Product Surface migration changes visible affordances.
|
||||
- [x] T028 [US2] Run the focused Restore tests selected in T011 and document results in `implementation-report.md`.
|
||||
|
||||
## Phase 5: User Story 3 - Optional Risk Exception Detail Cleanup
|
||||
|
||||
**Goal**: Include Risk Exception Detail only if the implementation remains narrow and does not reopen Spec 354.
|
||||
|
||||
**Independent Test**: If included, Finding Exception tests prove raw evidence fields are not default-visible and renew/revoke safety remains intact.
|
||||
|
||||
- [x] T029 [US3] Record the include/defer decision for Risk Exception Detail in `implementation-report.md`.
|
||||
- [x] T030 [US3] If included, demote Source ID, Fingerprint, JSON summary payload, raw evidence references, detector output, and source keys from the default `FindingExceptionResource` detail view. N/A - Risk Exception deferred.
|
||||
- [x] T031 [US3] If included, keep accepted-risk guidance as the top product decision using existing Spec 354 paths; do not create a new accepted-risk workflow layer. N/A - Risk Exception deferred.
|
||||
- [x] T032 [US3] If included, ensure `renew_exception` and `revoke_exception` remain confirmation-protected, authorized, service-backed, notification-backed, and visually separated. N/A - Risk Exception deferred.
|
||||
- [x] T033 [US3] If included, run focused Finding Exception tests selected in T012 and document results in `implementation-report.md`. N/A - Risk Exception deferred.
|
||||
|
||||
## Phase 6: User Story 4 - Technical Detail And Authorization Proof
|
||||
|
||||
**Goal**: Product simplification does not remove authorized diagnostics, auditability, or tenant/workspace isolation.
|
||||
|
||||
**Independent Test**: Authorized detail/audit paths remain reachable and unauthorized users do not see technical internals.
|
||||
|
||||
- [x] T034 [US4] Verify Baseline Compare authorized detail/audit paths remain reachable where supported and unauthorized users cannot access out-of-scope operation/evidence/baseline records.
|
||||
- [x] T035 [US4] Verify Restore detail/audit/diagnostic paths remain reachable where supported and unauthorized users cannot access out-of-scope restore, backup, operation, or provider detail.
|
||||
- [x] T036 [US4] If Risk Exception is included, verify evidence/detail access remains authorized and no raw fields appear by default. N/A - Risk Exception deferred.
|
||||
- [x] T037 [US4] Run focused authorization/detail tests selected in T013 and document results in `implementation-report.md`.
|
||||
|
||||
## Phase 7: UI Coverage, Browser Proof, Product Sanity, And Validation
|
||||
|
||||
**Purpose**: Prove rendered UI reduction and close the Product Surface Contract gate.
|
||||
|
||||
- [x] T038 Update `docs/ui-ux-enterprise-audit/route-inventory.md` for every changed target surface; if a planned surface is skipped, record no-update rationale in `implementation-report.md`.
|
||||
- [x] T039 Update `docs/ui-ux-enterprise-audit/design-coverage-matrix.md` for every changed target surface; if a planned surface is skipped, record no-update rationale in `implementation-report.md`.
|
||||
- [x] T040 Run focused Spec 398 browser smoke for Baseline Compare and Restore Preview / Readiness, and Risk Exception Detail only if included.
|
||||
- [x] T041 Capture browser proof as screenshots under `specs/398-decision-page-contract-migration/artifacts/screenshots/` or record equivalent textual proof in `implementation-report.md`.
|
||||
- [x] T042 Complete Human Product Sanity review and record the result in `implementation-report.md`.
|
||||
- [x] T043 Record Product Surface exceptions as `none` or document approved exceptions in `implementation-report.md`; unapproved exceptions block completion.
|
||||
- [x] T044 Record Livewire v4 compliance, provider registration location, global search posture, destructive/high-impact actions, asset strategy, tests/browser result, visible complexity outcome, and deployment impact in `implementation-report.md`.
|
||||
- [x] T045 Run focused Feature/Filament tests selected in T010-T013 and T034-T037; document exact commands and results.
|
||||
- [x] T046 Run affected existing browser tests if shared flows changed; document exact commands and results.
|
||||
- [x] T047 Run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`.
|
||||
- [x] T048 Run `git diff --check`.
|
||||
- [x] T049 Confirm no application code introduced migrations, new persisted truth, new broad decision framework, Graph render calls, compatibility toggles, new assets, new routes, or panel/provider changes; record the result.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Phase 1 must complete before runtime edits.
|
||||
- Phase 2 tests should be added before or alongside each corresponding implementation phase.
|
||||
- Baseline Compare and Restore Preview work can proceed in parallel after discovery because they touch different primary files.
|
||||
- Optional Risk Exception work depends on T006/T029 include decision.
|
||||
- Browser proof and Human Product Sanity depend on all touched rendered surfaces.
|
||||
|
||||
## Requirement Coverage Map
|
||||
|
||||
| Requirement | Primary task coverage |
|
||||
|---|---|
|
||||
| FR-398-001 Baseline Compare decision layout | T010, T015-T016, T040-T044 |
|
||||
| FR-398-002 Baseline Compare technical demotion | T010, T017-T018, T034 |
|
||||
| FR-398-003 Baseline Compare table caps | T010, T019 |
|
||||
| FR-398-004 Restore decision sub-surface | T011, T022-T023, T040-T044 |
|
||||
| FR-398-005 Restore technical demotion | T011, T024-T025, T035 |
|
||||
| FR-398-006 Restore safety preserved | T011, T026-T027 |
|
||||
| FR-398-007 Risk Exception optional demotion | T012, T029-T033, T036 |
|
||||
| FR-398-008 One primary action | T010-T012, T016, T022, T031 |
|
||||
| FR-398-009 Authorized detail/audit paths | T013, T034-T037 |
|
||||
| FR-398-010 Canonical status vocabulary | T015, T022, T044 |
|
||||
| FR-398-011 High-impact action safety | T008, T020, T026, T032, T044 |
|
||||
| FR-398-012 No new framework/persistence | T001, T049 |
|
||||
| FR-398-013 Tests updated from old behavior | T003, T005, T010-T013, T045 |
|
||||
| FR-398-014 Focused browser proof | T014, T040-T041 |
|
||||
| NFR-398-001 Visible complexity decreases | T040-T044 |
|
||||
| NFR-398-002 No Graph calls during render | T049 |
|
||||
| NFR-398-003 Safety blockers visible | T016, T022-T026 |
|
||||
| NFR-398-004 Focused browser scope | T009, T014, T040-T041 |
|
||||
| NFR-398-005 Fixture/helper cost bounded | T014, T040-T046 |
|
||||
|
||||
## Parallel Execution Examples
|
||||
|
||||
```text
|
||||
After Phase 1:
|
||||
- Agent A: T010, T015-T021 for Baseline Compare.
|
||||
- Agent B: T011, T022-T028 for Restore Preview / Readiness.
|
||||
- Agent C: T012, T029-T033 only if Risk Exception is included.
|
||||
- Agent D: T034-T039 coverage and authorization proof once implementation surfaces stabilize.
|
||||
```
|
||||
|
||||
## Explicit Non-Goals For Implementation
|
||||
|
||||
- Do not create a new Decision Page engine, Technical Annex framework, persisted decision state, enum/status family, registry, resolver, provider abstraction, restore behavior, baseline compare engine behavior, evidence generation flow, navigation architecture, or compatibility toggle.
|
||||
- Do not rewrite completed specs or remove their validation, browser, screenshot, task, or close-out history.
|
||||
- Do not reopen Review Publication Resolution, Decision Register, Governance Inbox, Customer Review Workspace, receipt pages, dashboard/inbox link budgets, or system panel surfaces inside this spec.
|
||||
Loading…
Reference in New Issue
Block a user