## Summary - add the Evidence Snapshot domain with immutable tenant-scoped snapshots, per-dimension items, queued generation, audit actions, badge mappings, and Filament list/detail surfaces - add the workspace evidence overview, capability and policy wiring, Livewire update-path hardening, and review-pack integration through explicit evidence snapshot resolution - add spec 153 artifacts, migrations, factories, and focused Pest coverage for evidence, review-pack reuse, authorization, action-surface regressions, and audit behavior ## Testing - `vendor/bin/sail artisan test --compact --stop-on-failure` - `CI=1 vendor/bin/sail artisan test --compact` - `vendor/bin/sail bin pint --dirty --format agent` ## Notes - branch: `153-evidence-domain-foundation` - commit: `b7dfa279` - spec: `specs/153-evidence-domain-foundation/` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #183
117 lines
4.4 KiB
PHP
117 lines
4.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Pages\Monitoring;
|
|
|
|
use App\Filament\Resources\EvidenceSnapshotResource;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use BackedEnum;
|
|
use Filament\Actions\Action;
|
|
use Filament\Pages\Page;
|
|
use Illuminate\Auth\AuthenticationException;
|
|
use UnitEnum;
|
|
|
|
class EvidenceOverview extends Page
|
|
{
|
|
protected static bool $isDiscovered = false;
|
|
|
|
protected static bool $shouldRegisterNavigation = false;
|
|
|
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shield-check';
|
|
|
|
protected static string|UnitEnum|null $navigationGroup = 'Monitoring';
|
|
|
|
protected static ?string $title = 'Evidence Overview';
|
|
|
|
protected string $view = 'filament.pages.monitoring.evidence-overview';
|
|
|
|
/**
|
|
* @var list<array<string, mixed>>
|
|
*/
|
|
public array $rows = [];
|
|
|
|
public ?int $tenantFilter = null;
|
|
|
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
|
{
|
|
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly)
|
|
->satisfy(ActionSurfaceSlot::ListHeader, 'The overview header exposes a clear-filters action when a tenant prefilter is active.')
|
|
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
|
|
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'The overview exposes a single drill-down link per row without a More menu.')
|
|
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The overview does not expose bulk actions.')
|
|
->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state explains the current scope and offers a clear-filters CTA.');
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
throw new AuthenticationException;
|
|
}
|
|
|
|
$workspaceContext = app(WorkspaceContext::class);
|
|
$workspace = $workspaceContext->currentWorkspaceForMemberOrFail($user, request());
|
|
$workspaceId = (int) $workspace->getKey();
|
|
|
|
$accessibleTenants = $user->tenants()
|
|
->where('tenants.workspace_id', $workspaceId)
|
|
->orderBy('tenants.name')
|
|
->get()
|
|
->filter(fn (Tenant $tenant): bool => (int) $tenant->workspace_id === $workspaceId && $user->can('evidence.view', $tenant))
|
|
->values();
|
|
|
|
$this->tenantFilter = is_numeric(request()->query('tenant_id')) ? (int) request()->query('tenant_id') : null;
|
|
|
|
$tenantIds = $accessibleTenants->pluck('id')->map(static fn (mixed $id): int => (int) $id)->all();
|
|
|
|
$query = EvidenceSnapshot::query()
|
|
->with('tenant')
|
|
->where('workspace_id', $workspaceId)
|
|
->whereIn('tenant_id', $tenantIds)
|
|
->where('status', 'active')
|
|
->latest('generated_at');
|
|
|
|
if ($this->tenantFilter !== null) {
|
|
$query->where('tenant_id', $this->tenantFilter);
|
|
}
|
|
|
|
$snapshots = $query->get()->unique('tenant_id')->values();
|
|
|
|
$this->rows = $snapshots->map(function (EvidenceSnapshot $snapshot): array {
|
|
return [
|
|
'tenant_name' => $snapshot->tenant?->name ?? 'Unknown tenant',
|
|
'tenant_id' => (int) $snapshot->tenant_id,
|
|
'snapshot_id' => (int) $snapshot->getKey(),
|
|
'completeness_state' => (string) $snapshot->completeness_state,
|
|
'generated_at' => $snapshot->generated_at?->toDateTimeString(),
|
|
'missing_dimensions' => (int) (($snapshot->summary['missing_dimensions'] ?? 0)),
|
|
'stale_dimensions' => (int) (($snapshot->summary['stale_dimensions'] ?? 0)),
|
|
'view_url' => EvidenceSnapshotResource::getUrl('index', tenant: $snapshot->tenant),
|
|
];
|
|
})->all();
|
|
}
|
|
|
|
/**
|
|
* @return array<Action>
|
|
*/
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return [
|
|
Action::make('clear_filters')
|
|
->label('Clear filters')
|
|
->color('gray')
|
|
->visible(fn (): bool => $this->tenantFilter !== null)
|
|
->url(route('admin.evidence.overview')),
|
|
];
|
|
}
|
|
}
|