|null */ public ?array $severityCounts = 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 { $tenant = Tenant::current(); if (! $tenant instanceof Tenant) { $this->state = 'no_tenant'; $this->message = 'No tenant selected.'; return; } $assignment = BaselineTenantAssignment::query() ->where('tenant_id', $tenant->getKey()) ->first(); if (! $assignment instanceof BaselineTenantAssignment) { $this->state = 'no_assignment'; $this->message = 'This tenant has no baseline assignment. A workspace manager can assign a baseline profile to this tenant.'; return; } $profile = $assignment->baselineProfile; if ($profile === null) { $this->state = 'no_assignment'; $this->message = 'The assigned baseline profile no longer exists.'; return; } $this->profileName = (string) $profile->name; $this->profileId = (int) $profile->getKey(); $this->snapshotId = $profile->active_snapshot_id !== null ? (int) $profile->active_snapshot_id : null; if ($this->snapshotId === null) { $this->state = 'no_snapshot'; $this->message = 'The baseline profile has no active snapshot yet. A workspace manager needs to capture a snapshot first.'; return; } $latestRun = OperationRun::query() ->where('tenant_id', $tenant->getKey()) ->where('type', 'baseline_compare') ->latest('id') ->first(); if ($latestRun instanceof OperationRun && in_array($latestRun->status, ['queued', 'running'], true)) { $this->state = 'comparing'; $this->operationRunId = (int) $latestRun->getKey(); $this->message = 'A baseline comparison is currently in progress.'; return; } $scopeKey = 'baseline_profile:' . $profile->getKey(); $findingsQuery = Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_DRIFT) ->where('source', 'baseline.compare') ->where('scope_key', $scopeKey); $totalFindings = (int) (clone $findingsQuery)->count(); if ($totalFindings > 0) { $this->state = 'ready'; $this->findingsCount = $totalFindings; $this->severityCounts = [ 'high' => (int) (clone $findingsQuery)->where('severity', Finding::SEVERITY_HIGH)->count(), 'medium' => (int) (clone $findingsQuery)->where('severity', Finding::SEVERITY_MEDIUM)->count(), 'low' => (int) (clone $findingsQuery)->where('severity', Finding::SEVERITY_LOW)->count(), ]; if ($latestRun instanceof OperationRun) { $this->operationRunId = (int) $latestRun->getKey(); } return; } if ($latestRun instanceof OperationRun && $latestRun->status === 'completed' && $latestRun->outcome === 'succeeded') { $this->state = 'ready'; $this->findingsCount = 0; $this->operationRunId = (int) $latestRun->getKey(); $this->message = 'No drift findings for this baseline comparison. The tenant matches the baseline.'; return; } $this->state = 'idle'; $this->message = 'Baseline profile is assigned and has a snapshot. Run "Compare Now" to check for drift.'; } /** * @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'], 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'; Notification::make() ->title('Baseline comparison started') ->body('A background job will compute drift against the baseline snapshot.') ->success() ->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); } }