491 lines
26 KiB
PHP
491 lines
26 KiB
PHP
@php
|
||
$report = isset($getState) ? $getState() : ($report ?? null);
|
||
$report = is_array($report) ? $report : null;
|
||
|
||
$run = $run ?? null;
|
||
$run = is_array($run) ? $run : null;
|
||
|
||
$fingerprint = $fingerprint ?? null;
|
||
$fingerprint = is_string($fingerprint) && trim($fingerprint) !== '' ? trim($fingerprint) : null;
|
||
|
||
$changeIndicator = $changeIndicator ?? null;
|
||
$changeIndicator = is_array($changeIndicator) ? $changeIndicator : null;
|
||
|
||
$previousRunUrl = $previousRunUrl ?? null;
|
||
$previousRunUrl = is_string($previousRunUrl) && $previousRunUrl !== '' ? $previousRunUrl : null;
|
||
|
||
$acknowledgements = $acknowledgements ?? [];
|
||
$acknowledgements = is_array($acknowledgements) ? $acknowledgements : [];
|
||
|
||
$summary = $report['summary'] ?? null;
|
||
$summary = is_array($summary) ? $summary : null;
|
||
|
||
$counts = is_array($summary['counts'] ?? null) ? $summary['counts'] : [];
|
||
|
||
$checks = $report['checks'] ?? null;
|
||
$checks = is_array($checks) ? $checks : [];
|
||
|
||
$ackByKey = [];
|
||
|
||
foreach ($acknowledgements as $checkKey => $ack) {
|
||
if (! is_string($checkKey) || $checkKey === '' || ! is_array($ack)) {
|
||
continue;
|
||
}
|
||
|
||
$ackByKey[$checkKey] = $ack;
|
||
}
|
||
|
||
$blockers = [];
|
||
$failures = [];
|
||
$warnings = [];
|
||
$acknowledgedIssues = [];
|
||
$passed = [];
|
||
|
||
foreach ($checks as $check) {
|
||
$check = is_array($check) ? $check : [];
|
||
|
||
$key = $check['key'] ?? null;
|
||
$key = is_string($key) ? trim($key) : '';
|
||
|
||
if ($key === '') {
|
||
continue;
|
||
}
|
||
|
||
$statusValue = $check['status'] ?? null;
|
||
$statusValue = is_string($statusValue) ? strtolower(trim($statusValue)) : '';
|
||
|
||
$blocking = $check['blocking'] ?? false;
|
||
$blocking = is_bool($blocking) ? $blocking : false;
|
||
|
||
if (array_key_exists($key, $ackByKey)) {
|
||
$acknowledgedIssues[] = $check;
|
||
continue;
|
||
}
|
||
|
||
if ($statusValue === 'pass') {
|
||
$passed[] = $check;
|
||
continue;
|
||
}
|
||
|
||
if ($statusValue === 'fail' && $blocking) {
|
||
$blockers[] = $check;
|
||
continue;
|
||
}
|
||
|
||
if ($statusValue === 'fail') {
|
||
$failures[] = $check;
|
||
continue;
|
||
}
|
||
|
||
if ($statusValue === 'warn') {
|
||
$warnings[] = $check;
|
||
}
|
||
}
|
||
|
||
$sortChecks = static function (array $a, array $b): int {
|
||
return strcmp((string) ($a['key'] ?? ''), (string) ($b['key'] ?? ''));
|
||
};
|
||
|
||
usort($blockers, $sortChecks);
|
||
usort($failures, $sortChecks);
|
||
usort($warnings, $sortChecks);
|
||
usort($acknowledgedIssues, $sortChecks);
|
||
usort($passed, $sortChecks);
|
||
@endphp
|
||
|
||
<div class="space-y-4">
|
||
@if ($report === null || $summary === null)
|
||
<div class="rounded-lg border border-gray-200 bg-white p-4 text-sm text-gray-600 shadow-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300">
|
||
<div class="font-medium text-gray-900 dark:text-white">
|
||
Verification report unavailable
|
||
</div>
|
||
<div class="mt-1">
|
||
This run doesn’t have a report yet. If it’s still running, refresh in a moment. If it already completed, start verification again.
|
||
</div>
|
||
<div class="mt-2 text-xs text-gray-600 dark:text-gray-300">
|
||
<span class="font-semibold">Read-only:</span> this view uses stored data and makes no external calls.
|
||
</div>
|
||
</div>
|
||
@else
|
||
@php
|
||
$overallSpec = \App\Support\Badges\BadgeRenderer::spec(
|
||
\App\Support\Badges\BadgeDomain::VerificationReportOverall,
|
||
$summary['overall'] ?? null,
|
||
);
|
||
@endphp
|
||
|
||
<div class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
<x-filament::badge :color="$overallSpec->color" :icon="$overallSpec->icon">
|
||
{{ $overallSpec->label }}
|
||
</x-filament::badge>
|
||
|
||
<x-filament::badge color="gray">
|
||
{{ (int) ($counts['total'] ?? 0) }} total
|
||
</x-filament::badge>
|
||
<x-filament::badge color="success">
|
||
{{ (int) ($counts['pass'] ?? 0) }} pass
|
||
</x-filament::badge>
|
||
<x-filament::badge color="danger">
|
||
{{ (int) ($counts['fail'] ?? 0) }} fail
|
||
</x-filament::badge>
|
||
<x-filament::badge color="warning">
|
||
{{ (int) ($counts['warn'] ?? 0) }} warn
|
||
</x-filament::badge>
|
||
<x-filament::badge color="gray">
|
||
{{ (int) ($counts['skip'] ?? 0) }} skip
|
||
</x-filament::badge>
|
||
<x-filament::badge color="info">
|
||
{{ (int) ($counts['running'] ?? 0) }} running
|
||
</x-filament::badge>
|
||
|
||
@if ($changeIndicator !== null)
|
||
@php
|
||
$state = $changeIndicator['state'] ?? null;
|
||
$state = is_string($state) ? $state : null;
|
||
@endphp
|
||
|
||
@if ($state === 'no_changes')
|
||
<x-filament::badge color="success">
|
||
No changes since previous verification
|
||
</x-filament::badge>
|
||
@elseif ($state === 'changed')
|
||
<x-filament::badge color="warning">
|
||
Changed since previous verification
|
||
</x-filament::badge>
|
||
@endif
|
||
@endif
|
||
</div>
|
||
|
||
<div class="mt-2 text-xs text-gray-600 dark:text-gray-300">
|
||
<span class="font-semibold">Read-only:</span> this view uses stored data and makes no external calls.
|
||
</div>
|
||
</div>
|
||
|
||
<div x-data="{ tab: 'issues' }" class="space-y-4">
|
||
<x-filament::tabs label="Verification report tabs">
|
||
<x-filament::tabs.item
|
||
:active="true"
|
||
alpine-active="tab === 'issues'"
|
||
x-on:click="tab = 'issues'"
|
||
>
|
||
Issues
|
||
</x-filament::tabs.item>
|
||
<x-filament::tabs.item
|
||
:active="false"
|
||
alpine-active="tab === 'passed'"
|
||
x-on:click="tab = 'passed'"
|
||
>
|
||
Passed
|
||
</x-filament::tabs.item>
|
||
<x-filament::tabs.item
|
||
:active="false"
|
||
alpine-active="tab === 'technical'"
|
||
x-on:click="tab = 'technical'"
|
||
>
|
||
Technical details
|
||
</x-filament::tabs.item>
|
||
</x-filament::tabs>
|
||
|
||
<div x-show="tab === 'issues'">
|
||
@if ($blockers === [] && $failures === [] && $warnings === [] && $acknowledgedIssues === [])
|
||
<div class="text-sm text-gray-700 dark:text-gray-200">
|
||
No issues found in this report.
|
||
</div>
|
||
@else
|
||
<div class="space-y-3">
|
||
@php
|
||
$issueGroups = [
|
||
['label' => 'Blockers', 'checks' => $blockers],
|
||
['label' => 'Failures', 'checks' => $failures],
|
||
['label' => 'Warnings', 'checks' => $warnings],
|
||
];
|
||
@endphp
|
||
|
||
@foreach ($issueGroups as $group)
|
||
@php
|
||
$label = $group['label'];
|
||
$groupChecks = $group['checks'];
|
||
@endphp
|
||
|
||
@if ($groupChecks !== [])
|
||
<div class="space-y-2">
|
||
<div class="text-sm font-semibold text-gray-900 dark:text-white">
|
||
{{ $label }}
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
@foreach ($groupChecks as $check)
|
||
@php
|
||
$check = is_array($check) ? $check : [];
|
||
|
||
$title = $check['title'] ?? 'Check';
|
||
$title = is_string($title) && trim($title) !== '' ? trim($title) : 'Check';
|
||
|
||
$message = $check['message'] ?? null;
|
||
$message = is_string($message) && trim($message) !== '' ? trim($message) : null;
|
||
|
||
$statusSpec = \App\Support\Badges\BadgeRenderer::spec(
|
||
\App\Support\Badges\BadgeDomain::VerificationCheckStatus,
|
||
$check['status'] ?? null,
|
||
);
|
||
|
||
$severitySpec = \App\Support\Badges\BadgeRenderer::spec(
|
||
\App\Support\Badges\BadgeDomain::VerificationCheckSeverity,
|
||
$check['severity'] ?? null,
|
||
);
|
||
|
||
$nextSteps = $check['next_steps'] ?? [];
|
||
$nextSteps = is_array($nextSteps) ? array_slice($nextSteps, 0, 2) : [];
|
||
|
||
$blocking = $check['blocking'] ?? false;
|
||
$blocking = is_bool($blocking) ? $blocking : false;
|
||
@endphp
|
||
|
||
<div class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||
<div class="space-y-1">
|
||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||
{{ $title }}
|
||
</div>
|
||
@if ($message)
|
||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||
{{ $message }}
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
<div class="flex shrink-0 flex-wrap items-center justify-end gap-2">
|
||
@if ($blocking)
|
||
<x-filament::badge color="danger" size="sm">
|
||
Blocker
|
||
</x-filament::badge>
|
||
@endif
|
||
|
||
<x-filament::badge :color="$severitySpec->color" :icon="$severitySpec->icon" size="sm">
|
||
{{ $severitySpec->label }}
|
||
</x-filament::badge>
|
||
<x-filament::badge :color="$statusSpec->color" :icon="$statusSpec->icon" size="sm">
|
||
{{ $statusSpec->label }}
|
||
</x-filament::badge>
|
||
</div>
|
||
</div>
|
||
|
||
@if ($nextSteps !== [])
|
||
<div class="mt-4">
|
||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||
Next steps
|
||
</div>
|
||
<ul class="mt-2 space-y-1 text-sm">
|
||
@foreach ($nextSteps as $step)
|
||
@php
|
||
$step = is_array($step) ? $step : [];
|
||
$label = $step['label'] ?? null;
|
||
$url = $step['url'] ?? null;
|
||
$isExternal = is_string($url) && (str_starts_with($url, 'http://') || str_starts_with($url, 'https://'));
|
||
@endphp
|
||
|
||
@if (is_string($label) && $label !== '' && is_string($url) && $url !== '')
|
||
<li>
|
||
<a
|
||
href="{{ $url }}"
|
||
class="text-primary-600 hover:underline dark:text-primary-400"
|
||
@if ($isExternal)
|
||
target="_blank" rel="noreferrer"
|
||
@endif
|
||
>
|
||
{{ $label }}
|
||
</a>
|
||
</li>
|
||
@endif
|
||
@endforeach
|
||
</ul>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
@endif
|
||
@endforeach
|
||
|
||
@if ($acknowledgedIssues !== [])
|
||
<details class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||
<summary class="cursor-pointer text-sm font-semibold text-gray-900 dark:text-white">
|
||
Acknowledged issues
|
||
</summary>
|
||
|
||
<div class="mt-4 space-y-2">
|
||
@foreach ($acknowledgedIssues as $check)
|
||
@php
|
||
$check = is_array($check) ? $check : [];
|
||
$checkKey = is_string($check['key'] ?? null) ? trim((string) $check['key']) : '';
|
||
|
||
$title = $check['title'] ?? 'Check';
|
||
$title = is_string($title) && trim($title) !== '' ? trim($title) : 'Check';
|
||
|
||
$message = $check['message'] ?? null;
|
||
$message = is_string($message) && trim($message) !== '' ? trim($message) : null;
|
||
|
||
$statusSpec = \App\Support\Badges\BadgeRenderer::spec(
|
||
\App\Support\Badges\BadgeDomain::VerificationCheckStatus,
|
||
$check['status'] ?? null,
|
||
);
|
||
|
||
$severitySpec = \App\Support\Badges\BadgeRenderer::spec(
|
||
\App\Support\Badges\BadgeDomain::VerificationCheckSeverity,
|
||
$check['severity'] ?? null,
|
||
);
|
||
|
||
$ack = $checkKey !== '' ? ($ackByKey[$checkKey] ?? null) : null;
|
||
$ack = is_array($ack) ? $ack : null;
|
||
|
||
$ackReason = $ack['ack_reason'] ?? null;
|
||
$ackReason = is_string($ackReason) && trim($ackReason) !== '' ? trim($ackReason) : null;
|
||
|
||
$ackAt = $ack['acknowledged_at'] ?? null;
|
||
$ackAt = is_string($ackAt) && trim($ackAt) !== '' ? trim($ackAt) : null;
|
||
|
||
$ackBy = $ack['acknowledged_by'] ?? null;
|
||
$ackBy = is_array($ackBy) ? $ackBy : null;
|
||
|
||
$ackByName = $ackBy['name'] ?? null;
|
||
$ackByName = is_string($ackByName) && trim($ackByName) !== '' ? trim($ackByName) : null;
|
||
|
||
$expiresAt = $ack['expires_at'] ?? null;
|
||
$expiresAt = is_string($expiresAt) && trim($expiresAt) !== '' ? trim($expiresAt) : null;
|
||
@endphp
|
||
|
||
<div class="rounded-lg border border-gray-100 bg-gray-50 p-4 dark:border-gray-800 dark:bg-gray-950">
|
||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||
<div class="space-y-1">
|
||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||
{{ $title }}
|
||
</div>
|
||
@if ($message)
|
||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||
{{ $message }}
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
<div class="flex shrink-0 flex-wrap items-center justify-end gap-2">
|
||
<x-filament::badge :color="$severitySpec->color" :icon="$severitySpec->icon" size="sm">
|
||
{{ $severitySpec->label }}
|
||
</x-filament::badge>
|
||
<x-filament::badge :color="$statusSpec->color" :icon="$statusSpec->icon" size="sm">
|
||
{{ $statusSpec->label }}
|
||
</x-filament::badge>
|
||
</div>
|
||
</div>
|
||
|
||
@if ($ackReason || $ackAt || $ackByName || $expiresAt)
|
||
<div class="mt-3 space-y-1 text-sm text-gray-700 dark:text-gray-200">
|
||
@if ($ackReason)
|
||
<div>
|
||
<span class="font-semibold">Reason:</span> {{ $ackReason }}
|
||
</div>
|
||
@endif
|
||
@if ($ackByName || $ackAt)
|
||
<div>
|
||
<span class="font-semibold">Acknowledged:</span>
|
||
@if ($ackByName)
|
||
{{ $ackByName }}
|
||
@endif
|
||
@if ($ackAt)
|
||
<span class="text-gray-500 dark:text-gray-400">({{ $ackAt }})</span>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
@if ($expiresAt)
|
||
<div>
|
||
<span class="font-semibold">Expires:</span>
|
||
<span class="text-gray-500 dark:text-gray-400">{{ $expiresAt }}</span>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</details>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
<div x-show="tab === 'passed'" style="display: none;">
|
||
@if ($passed === [])
|
||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||
No passing checks recorded.
|
||
</div>
|
||
@else
|
||
<div class="space-y-2">
|
||
@foreach ($passed as $check)
|
||
@php
|
||
$check = is_array($check) ? $check : [];
|
||
|
||
$title = $check['title'] ?? 'Check';
|
||
$title = is_string($title) && trim($title) !== '' ? trim($title) : 'Check';
|
||
|
||
$statusSpec = \App\Support\Badges\BadgeRenderer::spec(
|
||
\App\Support\Badges\BadgeDomain::VerificationCheckStatus,
|
||
$check['status'] ?? null,
|
||
);
|
||
@endphp
|
||
|
||
<div class="flex items-center justify-between gap-3 rounded-lg border border-gray-200 bg-white p-3 text-sm shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||
<div class="font-medium text-gray-900 dark:text-white">
|
||
{{ $title }}
|
||
</div>
|
||
<x-filament::badge :color="$statusSpec->color" :icon="$statusSpec->icon" size="sm">
|
||
{{ $statusSpec->label }}
|
||
</x-filament::badge>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
<div x-show="tab === 'technical'" style="display: none;">
|
||
<div class="space-y-4 text-sm text-gray-700 dark:text-gray-200">
|
||
<div class="space-y-1">
|
||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||
Identifiers
|
||
</div>
|
||
<div class="flex flex-col gap-1">
|
||
@if ($run !== null)
|
||
<div>
|
||
<span class="text-gray-500 dark:text-gray-400">Run ID:</span>
|
||
<span class="font-mono">{{ (int) ($run['id'] ?? 0) }}</span>
|
||
</div>
|
||
<div>
|
||
<span class="text-gray-500 dark:text-gray-400">Flow:</span>
|
||
<span class="font-mono">{{ (string) ($run['type'] ?? '') }}</span>
|
||
</div>
|
||
@endif
|
||
@if ($fingerprint)
|
||
<div>
|
||
<span class="text-gray-500 dark:text-gray-400">Fingerprint:</span>
|
||
<span class="font-mono text-xs break-all">{{ $fingerprint }}</span>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
@if ($previousRunUrl !== null)
|
||
<div>
|
||
<a
|
||
href="{{ $previousRunUrl }}"
|
||
class="font-medium text-primary-600 hover:underline dark:text-primary-400"
|
||
>
|
||
Open previous verification
|
||
</a>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
</div>
|