|null */ public ?array $statusCounts = null; public static function canAccess(): bool { return FindingResource::canAccess(); } public function mount(): void { $tenant = Tenant::current(); $user = auth()->user(); if (! $user instanceof User) { abort(403, 'Not allowed'); } $latestSuccessful = InventorySyncRun::query() ->where('tenant_id', $tenant->getKey()) ->where('status', InventorySyncRun::STATUS_SUCCESS) ->whereNotNull('finished_at') ->orderByDesc('finished_at') ->first(); if (! $latestSuccessful instanceof InventorySyncRun) { $this->state = 'blocked'; $this->message = 'No successful inventory runs found yet.'; return; } $scopeKey = (string) $latestSuccessful->selection_hash; $this->scopeKey = $scopeKey; $selector = app(DriftRunSelector::class); $comparison = $selector->selectBaselineAndCurrent($tenant, $scopeKey); if ($comparison === null) { $this->state = 'blocked'; $this->message = 'Need at least 2 successful runs for this scope to calculate drift.'; return; } $baseline = $comparison['baseline']; $current = $comparison['current']; $this->baselineRunId = (int) $baseline->getKey(); $this->currentRunId = (int) $current->getKey(); $this->baselineFinishedAt = $baseline->finished_at?->toDateTimeString(); $this->currentFinishedAt = $current->finished_at?->toDateTimeString(); $idempotencyKey = RunIdempotency::buildKey( tenantId: (int) $tenant->getKey(), operationType: 'drift.generate', targetId: $scopeKey, context: [ 'scope_key' => $scopeKey, 'baseline_run_id' => (int) $baseline->getKey(), 'current_run_id' => (int) $current->getKey(), ], ); $exists = Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_DRIFT) ->where('scope_key', $scopeKey) ->where('baseline_run_id', $baseline->getKey()) ->where('current_run_id', $current->getKey()) ->exists(); if ($exists) { $this->state = 'ready'; $newCount = (int) Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_DRIFT) ->where('scope_key', $scopeKey) ->where('baseline_run_id', $baseline->getKey()) ->where('current_run_id', $current->getKey()) ->where('status', Finding::STATUS_NEW) ->count(); $this->statusCounts = [Finding::STATUS_NEW => $newCount]; return; } $latestRun = BulkOperationRun::query() ->where('tenant_id', $tenant->getKey()) ->where('idempotency_key', $idempotencyKey) ->latest('id') ->first(); $activeRun = RunIdempotency::findActiveBulkOperationRun((int) $tenant->getKey(), $idempotencyKey); if ($activeRun instanceof BulkOperationRun) { $this->state = 'generating'; $this->bulkOperationRunId = (int) $activeRun->getKey(); return; } if ($latestRun instanceof BulkOperationRun && $latestRun->status === 'completed') { $this->state = 'ready'; $this->bulkOperationRunId = (int) $latestRun->getKey(); $newCount = (int) Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_DRIFT) ->where('scope_key', $scopeKey) ->where('baseline_run_id', $baseline->getKey()) ->where('current_run_id', $current->getKey()) ->where('status', Finding::STATUS_NEW) ->count(); $this->statusCounts = [Finding::STATUS_NEW => $newCount]; if ($newCount === 0) { $this->message = 'No drift findings for this comparison. If you changed settings after the current run, run Inventory Sync again to capture a newer snapshot.'; } return; } if ($latestRun instanceof BulkOperationRun && in_array($latestRun->status, ['failed', 'aborted'], true)) { $this->state = 'error'; $this->message = 'Drift generation failed for this comparison. See the run for details.'; $this->bulkOperationRunId = (int) $latestRun->getKey(); return; } $bulkOperationService = app(BulkOperationService::class); $run = $bulkOperationService->createRun( tenant: $tenant, user: $user, resource: 'drift', action: 'generate', itemIds: [$scopeKey], totalItems: 1, ); $run->update(['idempotency_key' => $idempotencyKey]); $this->state = 'generating'; $this->bulkOperationRunId = (int) $run->getKey(); GenerateDriftFindingsJob::dispatch( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), baselineRunId: (int) $baseline->getKey(), currentRunId: (int) $current->getKey(), scopeKey: $scopeKey, bulkOperationRunId: (int) $run->getKey(), ); } public function getFindingsUrl(): string { return FindingResource::getUrl('index', tenant: Tenant::current()); } public function getBaselineRunUrl(): ?string { if (! is_int($this->baselineRunId)) { return null; } return InventorySyncRunResource::getUrl('view', ['record' => $this->baselineRunId], tenant: Tenant::current()); } public function getCurrentRunUrl(): ?string { if (! is_int($this->currentRunId)) { return null; } return InventorySyncRunResource::getUrl('view', ['record' => $this->currentRunId], tenant: Tenant::current()); } public function getBulkRunUrl(): ?string { if (! is_int($this->bulkOperationRunId)) { return null; } return BulkOperationRunResource::getUrl('view', ['record' => $this->bulkOperationRunId], tenant: Tenant::current()); } }