TenantAtlas/resources/views/filament/pages/monitoring/audit-log.blade.php
ahmido 28cfe38ba4 feat: lay audit log foundation (#163)
## 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
2026-03-11 09:39:37 +00:00

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>