title("{$operationLabel} queued") ->body('Queued for execution. Open the run for progress and next steps.') ->info() ->duration(self::QUEUED_TOAST_DURATION_MS); } /** * Canonical dedupe feedback when a matching run is already active. */ public static function alreadyQueuedToast(string $operationType): FilamentNotification { $operationLabel = OperationCatalog::label($operationType); return FilamentNotification::make() ->title("{$operationLabel} already queued") ->body('A matching run is already queued or running. No action needed unless it stays stuck.') ->info() ->duration(self::QUEUED_TOAST_DURATION_MS); } /** * Terminal DB notification payload. * * Note: We intentionally return the built Filament notification builder to * keep DB formatting consistent with existing Notification classes. */ public static function terminalDatabaseNotification(OperationRun $run, ?Tenant $tenant = null): FilamentNotification { $operationLabel = OperationCatalog::label((string) $run->type); $presentation = self::terminalPresentation($run); $bodyLines = [$presentation['body']]; $failureMessage = self::surfaceFailureDetail($run); if ($failureMessage !== null) { $bodyLines[] = $failureMessage; } $guidance = self::surfaceGuidance($run); if ($guidance !== null) { $bodyLines[] = $guidance; } $summary = SummaryCountsNormalizer::renderSummaryLine(is_array($run->summary_counts) ? $run->summary_counts : []); if ($summary !== null) { $bodyLines[] = $summary; } $integritySummary = RedactionIntegrity::noteForRun($run); if (is_string($integritySummary) && trim($integritySummary) !== '') { $bodyLines[] = trim($integritySummary); } $notification = FilamentNotification::make() ->title("{$operationLabel} {$presentation['titleSuffix']}") ->body(implode("\n", $bodyLines)) ->status($presentation['status']); if ($tenant instanceof Tenant) { $notification->actions([ \Filament\Actions\Action::make('view') ->label('View run') ->url(OperationRunUrl::view($run, $tenant)), ]); } return $notification; } public static function surfaceGuidance(OperationRun $run): ?string { $uxStatus = OperationStatusNormalizer::toUxStatus($run->status, $run->outcome); $reasonEnvelope = self::reasonEnvelope($run); $reasonGuidance = app(ReasonPresenter::class)->guidance($reasonEnvelope); $nextStepLabel = self::firstNextStepLabel($run); if (in_array($uxStatus, ['blocked', 'failed', 'partial'], true) && $reasonGuidance !== null) { return $reasonGuidance; } return match ($uxStatus) { 'queued' => 'No action needed yet. The run is waiting for a worker.', 'running' => 'No action needed yet. The run is currently in progress.', 'succeeded' => 'No action needed.', 'partial' => $nextStepLabel !== null ? 'Next step: '.$nextStepLabel.'.' : (self::requiresFollowUp($run) ? 'Review the affected items before rerunning.' : 'No action needed unless the recorded warnings were unexpected.'), 'blocked' => $nextStepLabel !== null ? 'Next step: '.$nextStepLabel.'.' : 'Review the blocked prerequisite before retrying.', default => $nextStepLabel !== null ? 'Next step: '.$nextStepLabel.'.' : 'Review the run details before retrying.', }; } public static function surfaceFailureDetail(OperationRun $run): ?string { $reasonEnvelope = self::reasonEnvelope($run); if ($reasonEnvelope !== null) { return $reasonEnvelope->shortExplanation; } $failureMessage = (string) (($run->failure_summary[0]['message'] ?? '') ?? ''); return self::sanitizeFailureMessage($failureMessage); } /** * @return array{titleSuffix: string, body: string, status: string} */ private static function terminalPresentation(OperationRun $run): array { $uxStatus = OperationStatusNormalizer::toUxStatus($run->status, $run->outcome); $reasonEnvelope = self::reasonEnvelope($run); return match ($uxStatus) { 'succeeded' => [ 'titleSuffix' => 'completed successfully', 'body' => 'Completed successfully.', 'status' => 'success', ], 'partial' => [ 'titleSuffix' => self::requiresFollowUp($run) ? 'needs follow-up' : 'completed with review notes', 'body' => 'Completed with follow-up.', 'status' => 'warning', ], 'blocked' => [ 'titleSuffix' => 'blocked by prerequisite', 'body' => $reasonEnvelope?->operatorLabel ?? 'Blocked by prerequisite.', 'status' => 'warning', ], default => [ 'titleSuffix' => 'execution failed', 'body' => $reasonEnvelope?->operatorLabel ?? 'Execution failed.', 'status' => 'danger', ], }; } private static function requiresFollowUp(OperationRun $run): bool { if (self::firstNextStepLabel($run) !== null) { return true; } $counts = SummaryCountsNormalizer::normalize(is_array($run->summary_counts) ? $run->summary_counts : []); return (int) ($counts['failed'] ?? 0) > 0; } private static function firstNextStepLabel(OperationRun $run): ?string { $context = is_array($run->context) ? $run->context : []; $nextSteps = $context['next_steps'] ?? null; if (! is_array($nextSteps)) { return null; } foreach ($nextSteps as $nextStep) { if (! is_array($nextStep)) { continue; } $label = trim((string) ($nextStep['label'] ?? '')); if ($label !== '') { return $label; } } return null; } private static function sanitizeFailureMessage(string $failureMessage): ?string { $failureMessage = trim($failureMessage); if ($failureMessage === '') { return null; } $failureMessage = RunFailureSanitizer::sanitizeMessage($failureMessage); if (mb_strlen($failureMessage) > self::FAILURE_MESSAGE_MAX_CHARS) { $failureMessage = mb_substr($failureMessage, 0, self::FAILURE_MESSAGE_MAX_CHARS - 1).'…'; } return $failureMessage !== '' ? $failureMessage : null; } private static function reasonEnvelope(OperationRun $run): ?\App\Support\ReasonTranslation\ReasonResolutionEnvelope { return app(ReasonPresenter::class)->forOperationRun($run, 'notification'); } }