label('Backfill findings lifecycle') ->icon('heroicon-o-wrench-screwdriver') ->color('gray') ->requiresConfirmation() ->modalHeading('Backfill findings lifecycle') ->modalDescription('This will backfill legacy Findings data (lifecycle fields, SLA due dates, and drift duplicate consolidation) for the current tenant. The operation runs in the background.') ->action(function (OperationRunService $operationRuns): void { $user = auth()->user(); if (! $user instanceof User) { abort(403); } $tenant = \Filament\Facades\Filament::getTenant(); if (! $tenant instanceof Tenant) { abort(404); } $opRun = $operationRuns->ensureRunWithIdentity( tenant: $tenant, type: 'findings.lifecycle.backfill', identityInputs: [ 'tenant_id' => (int) $tenant->getKey(), 'trigger' => 'backfill', ], context: [ 'workspace_id' => (int) $tenant->workspace_id, 'initiator_user_id' => (int) $user->getKey(), ], initiator: $user, ); $runUrl = OperationRunLinks::view($opRun, $tenant); if ($opRun->wasRecentlyCreated === false) { OpsUxBrowserEvents::dispatchRunEnqueued($this); OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) ->actions([ Actions\Action::make('view_run') ->label('View run') ->url($runUrl), ]) ->send(); return; } $operationRuns->dispatchOrFail($opRun, function () use ($tenant, $user): void { BackfillFindingLifecycleJob::dispatch( tenantId: (int) $tenant->getKey(), workspaceId: (int) $tenant->workspace_id, initiatorUserId: (int) $user->getKey(), ); }); OpsUxBrowserEvents::dispatchRunEnqueued($this); OperationUxPresenter::queuedToast((string) $opRun->type) ->body('The backfill will run in the background. You can continue working while it completes.') ->actions([ Actions\Action::make('view_run') ->label('View run') ->url($runUrl), ]) ->send(); }) ) ->preserveVisibility() ->requireCapability(Capabilities::TENANT_MANAGE) ->tooltip(UiTooltips::INSUFFICIENT_PERMISSION) ->apply(), UiEnforcement::forAction( Actions\Action::make('triage_all_matching') ->label('Triage all matching') ->icon('heroicon-o-check') ->color('gray') ->requiresConfirmation() ->visible(fn (): bool => $this->getStatusFilterValue() === Finding::STATUS_NEW) ->modalDescription(function (): string { $count = $this->getAllMatchingCount(); return "You are about to triage {$count} finding".($count === 1 ? '' : 's').' matching the current filters.'; }) ->form(function (): array { $count = $this->getAllMatchingCount(); if ($count <= 100) { return []; } return [ TextInput::make('confirmation') ->label('Type TRIAGE to confirm') ->required() ->in(['TRIAGE']) ->validationMessages([ 'in' => 'Please type TRIAGE to confirm.', ]), ]; }) ->action(function (FindingWorkflowService $workflow): void { $query = $this->buildAllMatchingQuery(); $count = (clone $query)->count(); if ($count === 0) { Notification::make() ->title('No matching findings') ->body('There are no new findings matching the current filters to triage.') ->warning() ->send(); return; } $user = auth()->user(); $tenant = \Filament\Facades\Filament::getTenant(); if (! $user instanceof User) { abort(403); } if (! $tenant instanceof Tenant) { abort(404); } $triagedCount = 0; $skippedCount = 0; $failedCount = 0; $query->orderBy('id')->chunkById(200, function ($findings) use ($workflow, $tenant, $user, &$triagedCount, &$skippedCount, &$failedCount): void { foreach ($findings as $finding) { if (! $finding instanceof Finding) { $skippedCount++; continue; } if (! in_array((string) $finding->status, [ Finding::STATUS_NEW, Finding::STATUS_REOPENED, Finding::STATUS_ACKNOWLEDGED, ], true)) { $skippedCount++; continue; } try { $workflow->triage($finding, $tenant, $user); $triagedCount++; } catch (Throwable) { $failedCount++; } } }); $this->deselectAllTableRecords(); $this->resetPage(); $body = "Triaged {$triagedCount} finding".($triagedCount === 1 ? '' : 's').'.'; if ($skippedCount > 0) { $body .= " Skipped {$skippedCount}."; } if ($failedCount > 0) { $body .= " Failed {$failedCount}."; } Notification::make() ->title('Bulk triage completed') ->body($body) ->status($failedCount > 0 ? 'warning' : 'success') ->send(); }) ) ->preserveVisibility() ->requireCapability(Capabilities::TENANT_FINDINGS_TRIAGE) ->tooltip(UiTooltips::INSUFFICIENT_PERMISSION) ->apply(), ]; } protected function buildAllMatchingQuery(): Builder { $query = Finding::query(); $tenantId = \Filament\Facades\Filament::getTenant()?->getKey(); if (! is_numeric($tenantId)) { return $query->whereRaw('1 = 0'); } $query->where('tenant_id', (int) $tenantId); $query->where('status', Finding::STATUS_NEW); $findingType = $this->getFindingTypeFilterValue(); if (is_string($findingType) && $findingType !== '') { $query->where('finding_type', $findingType); } if ($this->filterIsActive('overdue')) { $query->whereNotNull('due_at')->where('due_at', '<', now()); } if ($this->filterIsActive('high_severity')) { $query->whereIn('severity', [ Finding::SEVERITY_HIGH, Finding::SEVERITY_CRITICAL, ]); } if ($this->filterIsActive('my_assigned')) { $userId = auth()->id(); if (is_numeric($userId)) { $query->where('assignee_user_id', (int) $userId); } else { $query->whereRaw('1 = 0'); } } $scopeKeyState = $this->getTableFilterState('scope_key') ?? []; $scopeKey = Arr::get($scopeKeyState, 'scope_key'); if (is_string($scopeKey) && $scopeKey !== '') { $query->where('scope_key', $scopeKey); } $runIdsState = $this->getTableFilterState('run_ids') ?? []; $baselineRunId = Arr::get($runIdsState, 'baseline_operation_run_id'); if (is_numeric($baselineRunId)) { $query->where('baseline_operation_run_id', (int) $baselineRunId); } $currentRunId = Arr::get($runIdsState, 'current_operation_run_id'); if (is_numeric($currentRunId)) { $query->where('current_operation_run_id', (int) $currentRunId); } return $query; } private function filterIsActive(string $filterName): bool { $state = $this->getTableFilterState($filterName); if ($state === true) { return true; } if (is_array($state)) { $isActive = Arr::get($state, 'isActive'); return $isActive === true; } return false; } protected function getAllMatchingCount(): int { return (int) $this->buildAllMatchingQuery()->count(); } protected function getStatusFilterValue(): string { $state = $this->getTableFilterState('status') ?? []; $value = Arr::get($state, 'value'); return is_string($value) && $value !== '' ? $value : Finding::STATUS_NEW; } protected function getFindingTypeFilterValue(): ?string { $state = $this->getTableFilterState('finding_type') ?? []; $value = Arr::get($state, 'value'); return is_string($value) && $value !== '' ? $value : null; } }