Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m12s
Added BaselineSubjectResolution page and supporting logic to visualize missing identities, ambiguous matches, and skipped coverages per Spec 384.
632 lines
35 KiB
PHP
632 lines
35 KiB
PHP
<x-filament::page>
|
|
{{-- Auto-refresh while comparison is running --}}
|
|
@if ($state === 'comparing')
|
|
<div wire:poll.5s="refreshStats"></div>
|
|
@endif
|
|
|
|
@php
|
|
$duplicateNamePoliciesCountValue = (int) ($duplicateNamePoliciesCount ?? 0);
|
|
$duplicateNameSubjectsCountValue = (int) ($duplicateNameSubjectsCount ?? 0);
|
|
$explanation = is_array($operatorExplanation ?? null) ? $operatorExplanation : null;
|
|
$reasonSemantics = is_array($reasonSemantics ?? null) ? $reasonSemantics : null;
|
|
$summary = is_array($summaryAssessment ?? null) ? $summaryAssessment : null;
|
|
$matrixNavigationContext = is_array($navigationContext ?? null) ? $navigationContext : null;
|
|
$arrivedFromCompareMatrix = str_starts_with((string) ($matrixNavigationContext['source_surface'] ?? ''), 'baseline_compare_matrix');
|
|
$explanationCounts = collect(is_array($explanation['countDescriptors'] ?? null) ? $explanation['countDescriptors'] : []);
|
|
$evaluationSpec = is_string($explanation['evaluationResult'] ?? null)
|
|
? \App\Support\Badges\BadgeCatalog::spec(\App\Support\Badges\BadgeDomain::OperatorExplanationEvaluationResult, $explanation['evaluationResult'])
|
|
: null;
|
|
$trustSpec = is_string($explanation['trustworthinessLevel'] ?? null)
|
|
? \App\Support\Badges\BadgeCatalog::spec(\App\Support\Badges\BadgeDomain::OperatorExplanationTrustworthiness, $explanation['trustworthinessLevel'])
|
|
: null;
|
|
$summaryLabel = match ($summary['stateFamily'] ?? null) {
|
|
'positive' => 'Aligned',
|
|
'caution' => 'Needs review',
|
|
'stale' => 'Refresh recommended',
|
|
'action_required' => 'Action required',
|
|
'in_progress' => 'In progress',
|
|
default => 'Unavailable',
|
|
};
|
|
$showCompareExplanation = $explanation !== null && $state !== 'no_assignment';
|
|
$baselineCompareStatusBadgeClasses = static fn (?string $tone): string => 'inline-flex items-center rounded-md border px-2 py-0.5 text-left text-xs font-medium leading-5 whitespace-normal break-words '.match ($tone) {
|
|
'danger' => 'border-danger-200 bg-danger-50 text-danger-700 dark:border-danger-800 dark:bg-danger-500/10 dark:text-danger-300',
|
|
'success' => 'border-success-200 bg-success-50 text-success-700 dark:border-success-800 dark:bg-success-500/10 dark:text-success-300',
|
|
'warning' => 'border-warning-200 bg-warning-50 text-warning-700 dark:border-warning-800 dark:bg-warning-500/10 dark:text-warning-300',
|
|
'info' => 'border-info-200 bg-info-50 text-info-700 dark:border-info-800 dark:bg-info-500/10 dark:text-info-300',
|
|
'primary' => 'border-primary-200 bg-primary-50 text-primary-700 dark:border-primary-800 dark:bg-primary-500/10 dark:text-primary-300',
|
|
default => 'border-gray-200 bg-gray-50 text-gray-700 dark:border-white/10 dark:bg-white/5 dark:text-gray-200',
|
|
};
|
|
@endphp
|
|
|
|
<x-filament::section>
|
|
<div data-testid="baseline-compare-decision-workbench" class="grid gap-5 lg:grid-cols-[minmax(0,1fr)_19rem]">
|
|
<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['question'] ?? 'Which baseline drift requires action?' }}
|
|
</h2>
|
|
<span data-testid="baseline-compare-status-badge" class="{{ $baselineCompareStatusBadgeClasses($decisionCard['tone'] ?? 'gray') }}">
|
|
{{ $decisionCard['status'] ?? 'Compare unavailable' }}
|
|
</span>
|
|
</div>
|
|
|
|
<dl class="overflow-hidden rounded-lg border border-gray-200 bg-gray-50/70 dark:border-gray-800 dark:bg-gray-950/40">
|
|
<div class="grid gap-1 px-4 py-3 sm:grid-cols-[8rem_minmax(0,1fr)] sm:gap-4">
|
|
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">
|
|
{{ $decisionCard['statusLabel'] ?? 'Status' }}
|
|
</dt>
|
|
<dd class="text-sm font-medium text-gray-950 dark:text-white">
|
|
{{ $decisionCard['status'] ?? 'Compare unavailable' }}
|
|
</dd>
|
|
</div>
|
|
|
|
<div class="grid gap-1 border-t border-gray-200 px-4 py-3 dark:border-gray-800 sm:grid-cols-[8rem_minmax(0,1fr)] sm:gap-4">
|
|
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">
|
|
{{ $decisionCard['reasonLabel'] ?? 'Reason' }}
|
|
</dt>
|
|
<dd class="text-sm leading-6 text-gray-700 dark:text-gray-200">
|
|
{{ $decisionCard['reason'] ?? 'Compare state is unavailable.' }}
|
|
</dd>
|
|
</div>
|
|
|
|
<div class="grid gap-1 border-t border-gray-200 px-4 py-3 dark:border-gray-800 sm:grid-cols-[8rem_minmax(0,1fr)] sm:gap-4">
|
|
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">
|
|
{{ $decisionCard['impactLabel'] ?? 'Impact' }}
|
|
</dt>
|
|
<dd class="text-sm leading-6 text-gray-700 dark:text-gray-200">
|
|
{{ $decisionCard['impact'] ?? 'No governance decision should rely on this compare state yet.' }}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
|
|
@if (! empty($decisionSummaryItems))
|
|
<div class="grid gap-3 md:grid-cols-3">
|
|
@foreach ($decisionSummaryItems as $item)
|
|
<div data-testid="baseline-compare-decision-summary" 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">{{ $item['label'] }}</div>
|
|
<div class="mt-1 text-sm font-medium text-gray-950 dark:text-white">{{ $item['value'] }}</div>
|
|
<p class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-400">{{ $item['description'] }}</p>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<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>
|
|
</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">
|
|
<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>
|
|
</aside>
|
|
</div>
|
|
</x-filament::section>
|
|
|
|
@if (($subjectResolutionActionCount ?? 0) > 0 && filled($subjectResolutionUrl ?? null))
|
|
<x-filament::section>
|
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div class="min-w-0">
|
|
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
|
Baseline subject decisions
|
|
</div>
|
|
<div class="mt-1 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
|
{{ (int) $subjectResolutionActionCount }} {{ \Illuminate\Support\Str::plural('subject', (int) $subjectResolutionActionCount) }} need identity or coverage decisions before compare output is fully trustworthy.
|
|
</div>
|
|
</div>
|
|
|
|
<x-filament::button tag="a" :href="$subjectResolutionUrl" icon="heroicon-o-puzzle-piece">
|
|
Resolve baseline subjects
|
|
</x-filament::button>
|
|
</div>
|
|
</x-filament::section>
|
|
@endif
|
|
|
|
@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,
|
|
])
|
|
|
|
<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-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
|
|
</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
|
|
</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>
|
|
</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>
|
|
|
|
<x-filament::button tag="a" :href="$openCompareMatrixUrl" color="gray" icon="heroicon-o-squares-2x2">
|
|
Open compare matrix
|
|
</x-filament::button>
|
|
</div>
|
|
</x-filament::section>
|
|
@endif
|
|
|
|
@if ($arrivedFromCompareMatrix)
|
|
<x-filament::section>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<x-filament::badge color="info" icon="heroicon-m-squares-2x2" size="sm">
|
|
Arrived from compare matrix
|
|
</x-filament::badge>
|
|
|
|
@if ($matrixBaselineProfileId)
|
|
<x-filament::badge color="gray" size="sm">
|
|
Baseline profile #{{ (int) $matrixBaselineProfileId }}
|
|
</x-filament::badge>
|
|
@endif
|
|
|
|
@if (filled($matrixSubjectKey))
|
|
<x-filament::badge color="gray" size="sm">
|
|
Subject {{ $matrixSubjectKey }}
|
|
</x-filament::badge>
|
|
@endif
|
|
</div>
|
|
</x-filament::section>
|
|
@endif
|
|
|
|
@if ($duplicateNamePoliciesCountValue > 0)
|
|
<div role="alert" class="rounded-lg border border-warning-300 bg-warning-50 p-4 dark:border-warning-700 dark:bg-warning-950/40">
|
|
<div class="flex items-start gap-3">
|
|
<x-heroicon-s-exclamation-triangle class="h-6 w-6 shrink-0 text-warning-600 dark:text-warning-400" />
|
|
<div class="flex flex-col gap-1">
|
|
<div class="text-base font-semibold text-warning-900 dark:text-warning-200">
|
|
{{ __('baseline-compare.duplicate_warning_title') }}
|
|
</div>
|
|
<div class="text-sm text-warning-800 dark:text-warning-300">
|
|
{{ __($duplicateNamePoliciesCountValue === 1 ? 'baseline-compare.duplicate_warning_body_singular' : 'baseline-compare.duplicate_warning_body_plural', [
|
|
'count' => $duplicateNamePoliciesCountValue,
|
|
'ambiguous_count' => $duplicateNameSubjectsCountValue,
|
|
'app' => config('app.name', 'TenantPilot'),
|
|
]) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@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
|
|
|
|
@if ($evaluationSpec && $evaluationSpec->label !== 'Unknown')
|
|
<x-filament::badge :color="$evaluationSpec->color" :icon="$evaluationSpec->icon" size="sm">
|
|
{{ $evaluationSpec->label }}
|
|
</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>
|
|
|
|
<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($summary['supportingMessage'] ?? null))
|
|
<p class="text-sm text-gray-700 dark:text-gray-200">
|
|
{{ $summary['supportingMessage'] }}
|
|
</p>
|
|
@endif
|
|
|
|
@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">
|
|
<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-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Reason owner</dt>
|
|
<dd class="mt-1 text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ $reasonSemantics['owner_label'] ?? 'Platform core' }}
|
|
</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-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Platform reason family</dt>
|
|
<dd class="mt-1 text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ $reasonSemantics['family_label'] ?? 'Compatibility' }}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
@endif
|
|
|
|
<dl class="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
|
<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-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Execution outcome</dt>
|
|
<dd class="mt-1 text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ $explanation['executionOutcomeLabel'] ?? 'Completed' }}
|
|
</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-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Result trust</dt>
|
|
<dd class="mt-1 text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ $explanation['trustworthinessLabel'] ?? 'Needs review' }}
|
|
</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 md:col-span-2">
|
|
<dt class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">What to do next</dt>
|
|
<dd class="mt-1 text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ data_get($summary, 'nextAction.label') ?? data_get($explanation, 'nextAction.text', 'Review the latest compare run.') }}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
|
|
@if (filled($explanation['coverageStatement'] ?? null))
|
|
<div class="rounded-lg border border-primary-200 bg-primary-50/70 px-4 py-3 text-sm text-primary-950 dark:border-primary-900/40 dark:bg-primary-950/20 dark:text-primary-100">
|
|
<span class="font-semibold">Coverage:</span>
|
|
{{ $explanation['coverageStatement'] }}
|
|
</div>
|
|
@endif
|
|
|
|
@if ($explanationCounts->isNotEmpty())
|
|
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
|
@foreach ($explanationCounts as $count)
|
|
@continue(! is_array($count))
|
|
|
|
<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-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
|
{{ $count['label'] ?? 'Count' }}
|
|
</div>
|
|
<div class="mt-1 text-2xl font-semibold text-gray-950 dark:text-white">
|
|
{{ (int) ($count['value'] ?? 0) }}
|
|
</div>
|
|
@if (filled($count['qualifier'] ?? null))
|
|
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
{{ $count['qualifier'] }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</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>
|
|
|
|
<div class="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>
|
|
<x-filament::badge color="success">
|
|
{{ __('baseline-compare.rbac_summary_unchanged') }}: {{ (int) ($rbacRoleDefinitionSummary['unchanged'] ?? 0) }}
|
|
</x-filament::badge>
|
|
<x-filament::badge color="warning">
|
|
{{ __('baseline-compare.rbac_summary_modified') }}: {{ (int) ($rbacRoleDefinitionSummary['modified'] ?? 0) }}
|
|
</x-filament::badge>
|
|
<x-filament::badge color="danger">
|
|
{{ __('baseline-compare.rbac_summary_missing') }}: {{ (int) ($rbacRoleDefinitionSummary['missing'] ?? 0) }}
|
|
</x-filament::badge>
|
|
<x-filament::badge color="info">
|
|
{{ __('baseline-compare.rbac_summary_unexpected') }}: {{ (int) ($rbacRoleDefinitionSummary['unexpected'] ?? 0) }}
|
|
</x-filament::badge>
|
|
</div>
|
|
</x-filament::section>
|
|
@endif
|
|
|
|
{{-- Coverage warnings banner --}}
|
|
@if ($state === 'ready' && $hasCoverageWarnings)
|
|
<div role="alert" class="rounded-lg border border-warning-300 bg-warning-50 p-4 dark:border-warning-700 dark:bg-warning-950/40">
|
|
<div class="flex items-start gap-3">
|
|
<x-heroicon-s-exclamation-triangle class="h-6 w-6 shrink-0 text-warning-600 dark:text-warning-400" />
|
|
<div class="flex flex-col gap-1">
|
|
<div class="text-base font-semibold text-warning-900 dark:text-warning-200">
|
|
{{ __('baseline-compare.coverage_warning_title') }}
|
|
</div>
|
|
<div class="text-sm text-warning-800 dark:text-warning-300">
|
|
@if (($coverageStatus ?? null) === 'unproven')
|
|
{{ __('baseline-compare.coverage_unproven_body') }}
|
|
@else
|
|
{{ __('baseline-compare.coverage_incomplete_body', [
|
|
'count' => (int) ($uncoveredTypesCount ?? 0),
|
|
'types' => Str::plural('type', (int) ($uncoveredTypesCount ?? 0)),
|
|
]) }}
|
|
@endif
|
|
|
|
@if (! empty($uncoveredTypes))
|
|
<div class="mt-2 text-xs text-warning-800 dark:text-warning-300">
|
|
{{ __('baseline-compare.coverage_uncovered_label', [
|
|
'list' => implode(', ', array_slice($uncoveredTypes, 0, 6)) . (count($uncoveredTypes) > 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>
|
|
@endif
|
|
|
|
{{-- Failed run banner --}}
|
|
@if ($state === 'failed')
|
|
<div role="alert" class="rounded-lg border border-danger-300 bg-danger-50 p-4 dark:border-danger-700 dark:bg-danger-950/50">
|
|
<div class="flex items-start gap-3">
|
|
<x-heroicon-s-x-circle class="h-6 w-6 shrink-0 text-danger-600 dark:text-danger-400" />
|
|
<div class="flex flex-col gap-1">
|
|
<div class="text-base font-semibold text-danger-800 dark:text-danger-200">
|
|
{{ __('baseline-compare.failed_title') }}
|
|
</div>
|
|
<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>
|
|
@endif
|
|
|
|
{{-- Critical drift banner --}}
|
|
@if ($state === 'ready' && ($severityCounts['high'] ?? 0) > 0)
|
|
<div role="alert" class="rounded-lg border border-danger-300 bg-danger-50 p-4 dark:border-danger-700 dark:bg-danger-950/50">
|
|
<div class="flex items-start gap-3">
|
|
<x-heroicon-s-exclamation-triangle class="h-6 w-6 shrink-0 text-danger-600 dark:text-danger-400" />
|
|
<div class="flex flex-col gap-1">
|
|
<div class="text-base font-semibold text-danger-800 dark:text-danger-200">
|
|
{{ __('baseline-compare.critical_drift_title') }}
|
|
</div>
|
|
<div class="text-sm text-danger-700 dark:text-danger-300">
|
|
{{ __('baseline-compare.critical_drift_body', [
|
|
'profile' => $profileName,
|
|
'count' => $severityCounts['high'],
|
|
'findings' => Str::plural('finding', $severityCounts['high']),
|
|
]) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@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>
|
|
|
|
@include('filament.infolists.entries.evidence-gap-subjects', [
|
|
'summary' => $evidenceGapSummary,
|
|
'buckets' => $evidenceGapBuckets ?? [],
|
|
'searchId' => 'tenant-baseline-compare-gap-search',
|
|
])
|
|
</x-filament::section>
|
|
@endif
|
|
|
|
{{-- Severity breakdown + actions --}}
|
|
@if ($state === 'ready' && ($findingsCount ?? 0) > 0)
|
|
<x-filament::section>
|
|
<x-slot name="heading">
|
|
{{ $findingsCount }} {{ Str::plural('Finding', $findingsCount) }}
|
|
</x-slot>
|
|
<x-slot name="description">
|
|
{{ __('baseline-compare.findings_description') }}
|
|
</x-slot>
|
|
|
|
<div class="flex flex-col gap-4">
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
@if (($severityCounts['high'] ?? 0) > 0)
|
|
<x-filament::badge color="danger">
|
|
{{ $severityCounts['high'] }} High
|
|
</x-filament::badge>
|
|
@endif
|
|
|
|
@if (($severityCounts['medium'] ?? 0) > 0)
|
|
<x-filament::badge color="warning">
|
|
{{ $severityCounts['medium'] }} Medium
|
|
</x-filament::badge>
|
|
@endif
|
|
|
|
@if (($severityCounts['low'] ?? 0) > 0)
|
|
<x-filament::badge color="gray">
|
|
{{ $severityCounts['low'] }} Low
|
|
</x-filament::badge>
|
|
@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>
|
|
|
|
<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>
|
|
</x-filament::section>
|
|
@endif
|
|
</x-filament::page>
|