|null */ public ?array $severityCounts = null; public ?string $lastComparedAt = null; public ?string $lastComparedIso = null; public ?string $failureReason = null; public static function canAccess(): bool { $user = auth()->user(); if (! $user instanceof User) { return false; } $tenant = Tenant::current(); if (! $tenant instanceof Tenant) { return false; } $resolver = app(CapabilityResolver::class); return $resolver->can($user, $tenant, Capabilities::TENANT_VIEW); } public function mount(): void { $this->refreshStats(); } public function refreshStats(): void { $stats = BaselineCompareStats::forTenant(Tenant::current()); $this->state = $stats->state; $this->message = $stats->message; $this->profileName = $stats->profileName; $this->profileId = $stats->profileId; $this->snapshotId = $stats->snapshotId; $this->operationRunId = $stats->operationRunId; $this->findingsCount = $stats->findingsCount; $this->severityCounts = $stats->severityCounts !== [] ? $stats->severityCounts : null; $this->lastComparedAt = $stats->lastComparedHuman; $this->lastComparedIso = $stats->lastComparedIso; $this->failureReason = $stats->failureReason; } public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly) ->satisfy(ActionSurfaceSlot::ListHeader, 'Header action: Compare Now (confirmation modal, capability-gated).') ->exempt(ActionSurfaceSlot::InspectAffordance, 'This is a tenant-scoped landing page, not a record inspect surface.') ->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'This page does not render table rows with secondary actions.') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'This page has no bulk actions.') ->satisfy(ActionSurfaceSlot::ListEmptyState, 'Page renders explicit empty states for missing tenant, missing assignment, and missing snapshot, with guidance messaging.') ->exempt(ActionSurfaceSlot::DetailHeader, 'This page does not have a record detail header; it uses a page header action instead.'); } /** * @return array */ protected function getHeaderActions(): array { return [ $this->compareNowAction(), ]; } private function compareNowAction(): Action { return Action::make('compareNow') ->label('Compare Now') ->icon('heroicon-o-play') ->requiresConfirmation() ->modalHeading('Start baseline comparison') ->modalDescription('This will compare the current tenant inventory against the assigned baseline snapshot and generate drift findings.') ->visible(fn (): bool => $this->canCompare()) ->disabled(fn (): bool => ! in_array($this->state, ['idle', 'ready', 'failed'], true)) ->action(function (): void { $user = auth()->user(); if (! $user instanceof User) { Notification::make()->title('Not authenticated')->danger()->send(); return; } $tenant = Tenant::current(); if (! $tenant instanceof Tenant) { Notification::make()->title('No tenant context')->danger()->send(); return; } $service = app(BaselineCompareService::class); $result = $service->startCompare($tenant, $user); if (! ($result['ok'] ?? false)) { Notification::make() ->title('Cannot start comparison') ->body('Reason: '.($result['reason_code'] ?? 'unknown')) ->danger() ->send(); return; } $run = $result['run'] ?? null; if ($run instanceof OperationRun) { $this->operationRunId = (int) $run->getKey(); } $this->state = 'comparing'; OpsUxBrowserEvents::dispatchRunEnqueued($this); OperationUxPresenter::queuedToast($run instanceof OperationRun ? (string) $run->type : 'baseline_compare') ->actions($run instanceof OperationRun ? [ Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($run, $tenant)), ] : []) ->send(); }); } private function canCompare(): bool { $user = auth()->user(); if (! $user instanceof User) { return false; } $tenant = Tenant::current(); if (! $tenant instanceof Tenant) { return false; } $resolver = app(CapabilityResolver::class); return $resolver->can($user, $tenant, Capabilities::TENANT_SYNC); } public function getFindingsUrl(): ?string { $tenant = Tenant::current(); if (! $tenant instanceof Tenant) { return null; } return FindingResource::getUrl('index', tenant: $tenant); } public function getRunUrl(): ?string { if ($this->operationRunId === null) { return null; } $tenant = Tenant::current(); if (! $tenant instanceof Tenant) { return null; } return OperationRunLinks::view($this->operationRunId, $tenant); } }