## Summary - add the Spec 181 restore-safety layer with scope fingerprinting, preview/check integrity states, execution safety snapshots, result attention, and operator-facing copy across the wizard, restore detail, and canonical operation detail - add focused unit and feature coverage for restore-safety assessment, result attention, and restore-linked operation detail - switch the finding exceptions queue `Inspect exception` action to a native Filament slide-over while preserving query-param-backed inline summary behavior ## Testing - `vendor/bin/sail artisan test --compact tests/Feature/Monitoring/FindingExceptionsQueueTest.php tests/Feature/Filament/RestoreSafetyIntegrityWizardTest.php tests/Feature/Filament/RestoreResultAttentionSurfaceTest.php tests/Feature/Operations/RestoreLinkedOperationDetailTest.php tests/Unit/Support/RestoreSafety` ## Notes - Spec 181 checklist is complete (`specs/181-restore-safety-integrity/checklists/requirements.md`) - the branch still has unchecked follow-up tasks in `specs/181-restore-safety-integrity/tasks.md`: `T012`, `T018`, `T019`, `T023`, `T025`, `T029`, `T032`, `T033`, `T041`, `T042`, `T043`, `T044` - Filament v5 / Livewire v4 compliance is preserved, no panel provider registration changes were made, no global-search behavior was added, destructive actions remain confirmation-gated, and no new Filament assets were introduced Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #210
193 lines
11 KiB
PHP
193 lines
11 KiB
PHP
@php
|
|
$state = $getState() ?? [];
|
|
$state = is_array($state) ? $state : [];
|
|
|
|
$preview = is_array($state['preview'] ?? null) ? $state['preview'] : $state;
|
|
$previewIntegrity = is_array($state['previewIntegrity'] ?? null) ? $state['previewIntegrity'] : [];
|
|
$checksIntegrity = is_array($state['checksIntegrity'] ?? null) ? $state['checksIntegrity'] : [];
|
|
$executionSafetySnapshot = is_array($state['executionSafetySnapshot'] ?? null) ? $state['executionSafetySnapshot'] : [];
|
|
$scopeBasis = is_array($state['scopeBasis'] ?? null) ? $state['scopeBasis'] : [];
|
|
|
|
$integritySpec = \App\Support\Badges\BadgeRenderer::spec(
|
|
\App\Support\Badges\BadgeDomain::RestorePreviewDecision,
|
|
$previewIntegrity['state'] ?? 'not_generated'
|
|
);
|
|
|
|
$checksSpec = \App\Support\Badges\BadgeRenderer::spec(
|
|
\App\Support\Badges\BadgeDomain::RestoreCheckSeverity,
|
|
$checksIntegrity['state'] ?? 'not_run'
|
|
);
|
|
$recoveryBoundary = \App\Support\RestoreSafety\RestoreSafetyCopy::recoveryBoundary(
|
|
is_string($executionSafetySnapshot['follow_up_boundary'] ?? null)
|
|
? $executionSafetySnapshot['follow_up_boundary']
|
|
: 'preview_only_no_execution_proven'
|
|
);
|
|
|
|
$actionPresentation = static function (array $item): array {
|
|
$action = is_string($item['action'] ?? null) ? $item['action'] : null;
|
|
|
|
return match ($action) {
|
|
'create' => ['label' => 'Will create', 'color' => 'success'],
|
|
'update' => ['label' => 'Will update existing', 'color' => 'info'],
|
|
default => ['label' => \Illuminate\Support\Str::headline((string) ($action ?? 'action')), 'color' => 'gray'],
|
|
};
|
|
};
|
|
|
|
$foundationItems = collect($preview)->filter(function ($item) {
|
|
return is_array($item) && array_key_exists('decision', $item) && array_key_exists('sourceId', $item);
|
|
});
|
|
$policyItems = collect($preview)->reject(function ($item) {
|
|
return is_array($item) && array_key_exists('decision', $item) && array_key_exists('sourceId', $item);
|
|
});
|
|
@endphp
|
|
|
|
@if (empty($preview))
|
|
<p class="text-sm text-gray-600">No preview has been generated yet.</p>
|
|
@else
|
|
<div class="space-y-4">
|
|
<div class="rounded-lg border border-slate-200 bg-slate-50 p-4 text-sm text-slate-900 dark:border-white/10 dark:bg-white/5 dark:text-slate-100">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<x-filament::badge :color="$integritySpec->color" :icon="$integritySpec->icon" size="sm">
|
|
{{ $integritySpec->label }}
|
|
</x-filament::badge>
|
|
<x-filament::badge :color="$checksSpec->color" :icon="$checksSpec->icon" size="sm">
|
|
{{ $checksSpec->label }}
|
|
</x-filament::badge>
|
|
</div>
|
|
|
|
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
|
<div>
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">What the preview proves</div>
|
|
<div class="mt-1">{{ $previewIntegrity['display_summary'] ?? 'Preview basis is unavailable.' }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">What this record does not prove</div>
|
|
<div class="mt-1">{{ $recoveryBoundary }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (($scopeBasis['fingerprint'] ?? null) !== null)
|
|
<div class="mt-3 text-xs text-slate-600 dark:text-slate-300">
|
|
Scope mode: {{ $scopeBasis['scope_mode'] ?? 'all' }}
|
|
@if (($scopeBasis['selected_item_ids'] ?? []) !== [])
|
|
• selected items: {{ count($scopeBasis['selected_item_ids']) }}
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if ($foundationItems->isNotEmpty())
|
|
<div class="space-y-2">
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500">Foundations</div>
|
|
@foreach ($foundationItems as $item)
|
|
@php
|
|
$decision = $item['decision'] ?? 'mapped_existing';
|
|
$foundationIsPreviewOnly = ($item['reason'] ?? null) === 'preview_only'
|
|
|| ($item['restore_mode'] ?? null) === 'preview-only'
|
|
|| $decision === 'dry_run';
|
|
$decisionSpec = $foundationIsPreviewOnly
|
|
? \App\Support\Badges\BadgeRenderer::spec(\App\Support\Badges\BadgeDomain::PolicyRestoreMode, 'preview_only')
|
|
: \App\Support\Badges\BadgeRenderer::spec(\App\Support\Badges\BadgeDomain::RestorePreviewDecision, $decision);
|
|
$foundationReason = $item['reason'] ?? null;
|
|
|
|
if ($foundationReason === 'preview_only') {
|
|
$foundationReason = 'Preview only. This foundation type is not applied during execution.';
|
|
}
|
|
@endphp
|
|
<div class="rounded border border-gray-200 bg-white p-3 shadow-sm">
|
|
<div class="flex items-center justify-between text-sm text-gray-800">
|
|
<span class="font-semibold">{{ $item['sourceName'] ?? $item['sourceId'] ?? 'Foundation' }}</span>
|
|
<x-filament::badge :color="$decisionSpec->color" :icon="$decisionSpec->icon" size="sm">
|
|
{{ $decisionSpec->label }}
|
|
</x-filament::badge>
|
|
</div>
|
|
<div class="mt-1 text-xs text-gray-600">
|
|
{{ $item['type'] ?? 'foundation' }}
|
|
</div>
|
|
@if (! empty($item['targetName']))
|
|
<div class="mt-1 text-xs text-gray-600">
|
|
Target: {{ $item['targetName'] }}
|
|
</div>
|
|
@endif
|
|
@if (! empty($foundationReason))
|
|
<div class="mt-2 rounded border border-amber-300 bg-amber-50 px-2 py-1 text-xs text-amber-800">
|
|
{{ $foundationReason }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
|
|
@if ($policyItems->isNotEmpty())
|
|
<div class="space-y-2">
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500">Policies</div>
|
|
@foreach ($policyItems as $item)
|
|
@php
|
|
$restoreMode = $item['restore_mode'] ?? null;
|
|
$actionState = $actionPresentation(is_array($item) ? $item : []);
|
|
@endphp
|
|
<div class="rounded border border-gray-200 bg-white p-3 shadow-sm">
|
|
<div class="flex items-center justify-between text-sm text-gray-800">
|
|
<span class="font-semibold">{{ $item['policy_identifier'] ?? 'Policy' }}</span>
|
|
<div class="flex items-center gap-2">
|
|
@if ($restoreMode === 'preview-only')
|
|
@php
|
|
$restoreModeSpec = \App\Support\Badges\BadgeRenderer::spec(\App\Support\Badges\BadgeDomain::PolicyRestoreMode, $restoreMode);
|
|
@endphp
|
|
<x-filament::badge :color="$restoreModeSpec->color" :icon="$restoreModeSpec->icon" size="sm">
|
|
{{ $restoreModeSpec->label }}
|
|
</x-filament::badge>
|
|
@endif
|
|
<span class="rounded bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700">
|
|
{{ $actionState['label'] }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-1 text-xs text-gray-600">
|
|
{{ $item['policy_type'] ?? 'type' }} • {{ $item['platform'] ?? 'platform' }}
|
|
</div>
|
|
|
|
@if (! empty($item['validation_warning']))
|
|
<div class="mt-2 rounded border border-amber-300 bg-amber-50 px-2 py-1 text-xs text-amber-800">
|
|
{{ $item['validation_warning'] }}
|
|
</div>
|
|
@endif
|
|
|
|
@if (! empty($item['compliance_action_warning']))
|
|
<div class="mt-2 rounded border border-amber-300 bg-amber-50 px-2 py-1 text-xs text-amber-800">
|
|
{{ $item['compliance_action_warning'] }}
|
|
</div>
|
|
@endif
|
|
@if (! empty($item['compliance_action_summary']) && is_array($item['compliance_action_summary']))
|
|
@php
|
|
$summary = $item['compliance_action_summary'];
|
|
$missingTemplates = $item['compliance_action_missing_templates'] ?? [];
|
|
$total = (int) ($summary['total'] ?? 0);
|
|
$missing = (int) ($summary['missing'] ?? 0);
|
|
@endphp
|
|
|
|
<div class="mt-2 text-xs text-gray-600">
|
|
Compliance notifications: {{ $total }} total • {{ $missing }} missing
|
|
</div>
|
|
|
|
@if (! empty($missingTemplates) && is_array($missingTemplates))
|
|
<details class="mt-2 rounded border border-amber-200 bg-amber-50 px-2 py-1 text-xs text-amber-900">
|
|
<summary class="cursor-pointer font-semibold">Missing notification templates</summary>
|
|
<div class="mt-2 space-y-1">
|
|
@foreach ($missingTemplates as $templateId)
|
|
<div class="rounded border border-amber-200 bg-white px-2 py-1 text-[11px] text-gray-800">
|
|
{{ $templateId }}
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</details>
|
|
@endif
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endif
|