|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(); $existingOperationRun = OperationRun::query() ->where('tenant_id', $tenant->getKey()) ->where('type', 'drift.generate') ->where('context->scope_key', $scopeKey) ->where('context->baseline_run_id', (int) $baseline->getKey()) ->where('context->current_run_id', (int) $current->getKey()) ->latest('id') ->first(); if ($existingOperationRun instanceof OperationRun) { $this->operationRunId = (int) $existingOperationRun->getKey(); } $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; } if (! $user->canSyncTenant($tenant)) { $this->state = 'blocked'; $this->message = 'You can view existing drift findings and run history, but you do not have permission to generate drift.'; return; } // --- Phase 3: Canonical Operation Run Start --- /** @var OperationRunService $opService */ $opService = app(OperationRunService::class); $opRun = $opService->ensureRun( tenant: $tenant, type: 'drift.generate', inputs: [ 'scope_key' => $scopeKey, 'baseline_run_id' => (int) $baseline->getKey(), 'current_run_id' => (int) $current->getKey(), ], initiator: $user ); $this->operationRunId = (int) $opRun->getKey(); if (! $opRun->wasRecentlyCreated && in_array($opRun->status, ['queued', 'running'])) { $this->state = 'generating'; // Reflect generating state in UI if idempotency hit // Optionally, we could find the related BulkOpRun to link, but the UI might just need state. Notification::make() ->title('Drift generation already active') ->body('This operation is already queued or running.') ->warning() ->actions([ Action::make('view_run') ->label('View Run') ->url(OperationRunLinks::view($opRun, $tenant)), ]) ->send(); return; } // ---------------------------------------------- $bulkOperationService = app(BulkOperationService::class); $run = $bulkOperationService->createRun( tenant: $tenant, user: $user, resource: 'drift', action: 'generate', itemIds: [ 'scope_key' => $scopeKey, 'baseline_run_id' => (int) $baseline->getKey(), 'current_run_id' => (int) $current->getKey(), ], totalItems: 1, ); $run->update(['idempotency_key' => $idempotencyKey]); $this->state = 'generating'; $this->bulkOperationRunId = (int) $run->getKey(); /** @var OperationRunService $opService */ $opService = app(OperationRunService::class); $opService->dispatchOrFail($opRun, function () use ($tenant, $user, $baseline, $current, $scopeKey, $run, $opRun): void { GenerateDriftFindingsJob::dispatch( tenantId: (int) $tenant->getKey(), userId: (int) $user->getKey(), baselineRunId: (int) $baseline->getKey(), currentRunId: (int) $current->getKey(), scopeKey: $scopeKey, bulkOperationRunId: (int) $run->getKey(), operationRun: $opRun ); }); Notification::make() ->title('Drift generation queued') ->body('Drift generation has been queued. Monitor progress in Monitoring → Operations.') ->success() ->actions([ Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($opRun, $tenant)), ]) ->sendToDatabase($user) ->send(); } 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 getOperationRunUrl(): ?string { if (! is_int($this->operationRunId)) { return null; } return OperationRunLinks::view($this->operationRunId, Tenant::current()); } }