## Summary - turn the Monitoring audit log placeholder into a real workspace-scoped audit review surface - introduce a shared audit recorder, richer audit value objects, and additive audit log schema evolution - add audit outcome and actor badges, permission-aware related navigation, and durable audit retention coverage ## Included - canonical `/admin/audit-log` list and detail inspection UI - audit model helpers, taxonomy expansion, actor/target snapshots, and recorder/builder services - operation terminal audit writes and purge command retention changes - spec 134 design artifacts and focused Pest coverage for audit foundation behavior ## Validation - `vendor/bin/sail bin pint --dirty --format agent` - `vendor/bin/sail artisan test --compact tests/Unit/Audit tests/Unit/Badges/AuditBadgesTest.php tests/Feature/Filament/AuditLogPageTest.php tests/Feature/Filament/AuditLogDetailInspectionTest.php tests/Feature/Filament/AuditLogAuthorizationTest.php tests/Feature/Monitoring/AuditCoverageGovernanceTest.php tests/Feature/Monitoring/AuditCoverageOperationsTest.php tests/Feature/Console/TenantpilotPurgeNonPersistentDataTest.php` ## Notes - Livewire v4.0+ compliance is preserved within the existing Filament v5 application. - No provider registration changes were needed; panel provider registration remains in `bootstrap/providers.php`. - No new globally searchable resource was introduced. - The audit page remains read-only; no destructive actions were added. - No new asset pipeline changes were introduced; existing deploy-time `php artisan filament:assets` behavior remains unchanged. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #163
149 lines
8.1 KiB
PHP
149 lines
8.1 KiB
PHP
<x-filament-panels::page>
|
|
<x-filament::section>
|
|
<div class="flex flex-col gap-3">
|
|
<div class="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
|
Summary-first audit history
|
|
</div>
|
|
|
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
|
Review governance, operational, and workspace-admin events in reverse chronological order without leaving the canonical Monitoring route.
|
|
</div>
|
|
|
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
|
Actor, outcome, target, and readable context stay visible even when the original record changes or disappears later.
|
|
</div>
|
|
</div>
|
|
</x-filament::section>
|
|
|
|
{{ $this->table }}
|
|
|
|
@php
|
|
$selectedAudit = $this->selectedAuditLog();
|
|
$selectedAuditLink = $this->selectedAuditLink();
|
|
@endphp
|
|
|
|
@if ($selectedAudit)
|
|
<x-filament::section
|
|
:heading="$selectedAudit->summaryText()"
|
|
:description="$selectedAudit->recorded_at?->toDayDateTimeString()"
|
|
>
|
|
<div class="flex flex-col gap-6">
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<span class="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
|
|
{{ \App\Support\Badges\BadgeRenderer::label(\App\Support\Badges\BadgeDomain::AuditOutcome)($selectedAudit->normalizedOutcome()->value) }}
|
|
</span>
|
|
<span class="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
|
|
{{ \App\Support\Badges\BadgeRenderer::label(\App\Support\Badges\BadgeDomain::AuditActorType)($selectedAudit->actorSnapshot()->type->value) }}
|
|
</span>
|
|
|
|
@if (is_array($selectedAuditLink))
|
|
<a
|
|
class="inline-flex items-center rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-900"
|
|
href="{{ $selectedAuditLink['url'] }}"
|
|
>
|
|
{{ $selectedAuditLink['label'] }}
|
|
</a>
|
|
@endif
|
|
|
|
<button
|
|
class="inline-flex items-center rounded-lg border border-transparent px-3 py-2 text-sm font-medium text-gray-500 transition hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
|
type="button"
|
|
wire:click="clearSelectedAuditLog"
|
|
>
|
|
Close details
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid gap-4 lg:grid-cols-3">
|
|
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
|
Actor
|
|
</div>
|
|
<div class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
{{ $selectedAudit->actorDisplayLabel() }}
|
|
</div>
|
|
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
|
{{ $selectedAudit->actorSnapshot()->type->label() }}
|
|
</div>
|
|
@if ($selectedAudit->actorSnapshot()->email)
|
|
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
{{ $selectedAudit->actorSnapshot()->email }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
|
Target
|
|
</div>
|
|
<div class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
{{ $selectedAudit->targetDisplayLabel() ?? 'No target snapshot' }}
|
|
</div>
|
|
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
|
{{ $selectedAudit->resource_type ? ucfirst(str_replace('_', ' ', $selectedAudit->resource_type)) : 'Workspace event' }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
|
Scope
|
|
</div>
|
|
<div class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
{{ $selectedAudit->tenant?->name ?? 'Workspace-wide event' }}
|
|
</div>
|
|
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
|
Workspace #{{ $selectedAudit->workspace_id }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid gap-4 lg:grid-cols-2">
|
|
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
|
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
Readable context
|
|
</div>
|
|
|
|
@if ($selectedAudit->contextItems() === [])
|
|
<div class="mt-3 text-sm text-gray-600 dark:text-gray-300">
|
|
No additional context was recorded for this event.
|
|
</div>
|
|
@else
|
|
<dl class="mt-3 space-y-3">
|
|
@foreach ($selectedAudit->contextItems() as $item)
|
|
<div>
|
|
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
|
{{ $item['label'] }}
|
|
</dt>
|
|
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
|
|
{{ is_bool($item['value']) ? ($item['value'] ? 'true' : 'false') : $item['value'] }}
|
|
</dd>
|
|
</div>
|
|
@endforeach
|
|
</dl>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
|
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
|
Technical metadata
|
|
</div>
|
|
|
|
<dl class="mt-3 space-y-3">
|
|
@foreach ($selectedAudit->technicalMetadata() as $label => $value)
|
|
<div>
|
|
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
|
{{ $label }}
|
|
</dt>
|
|
<dd class="mt-1 break-all text-sm text-gray-900 dark:text-gray-100">
|
|
{{ $value }}
|
|
</dd>
|
|
</div>
|
|
@endforeach
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</x-filament::section>
|
|
@endif
|
|
</x-filament-panels::page>
|