diff --git a/app/Filament/Pages/DriftLanding.php b/app/Filament/Pages/DriftLanding.php index 3b9a715..941abc2 100644 --- a/app/Filament/Pages/DriftLanding.php +++ b/app/Filament/Pages/DriftLanding.php @@ -272,7 +272,7 @@ public function mount(): void ); }); - $this->dispatch(OpsUxBrowserEvents::RunEnqueued); + OpsUxBrowserEvents::dispatchRunEnqueued($this); OperationUxPresenter::queuedToast((string) $opRun->type) ->actions([ Action::make('view_run') diff --git a/app/Filament/Pages/InventoryLanding.php b/app/Filament/Pages/InventoryLanding.php index 05cfd9e..caecbf5 100644 --- a/app/Filament/Pages/InventoryLanding.php +++ b/app/Filament/Pages/InventoryLanding.php @@ -156,7 +156,10 @@ protected function getHeaderActions(): array ); if (! $opRun->wasRecentlyCreated && in_array($opRun->status, ['queued', 'running'], true)) { - OperationUxPresenter::queuedToast((string) $opRun->type) + Notification::make() + ->title('Inventory sync already active') + ->body('This operation is already queued or running.') + ->warning() ->actions([ Action::make('view_run') ->label('View Run') @@ -168,7 +171,6 @@ protected function getHeaderActions(): array return; } - // ---------------------------------------------- // Legacy checks (kept for safety if parallel usage needs it, though OpRun handles idempotency now) $existing = InventorySyncRun::query() diff --git a/app/Filament/Pages/Monitoring/Operations.php b/app/Filament/Pages/Monitoring/Operations.php index a8bc8ee..394d983 100644 --- a/app/Filament/Pages/Monitoring/Operations.php +++ b/app/Filament/Pages/Monitoring/Operations.php @@ -52,9 +52,9 @@ public function table(Table $table): Table TextColumn::make('status') ->badge() ->colors([ - 'warning' => 'queued', - 'info' => 'running', - 'secondary' => 'completed', + 'secondary' => 'queued', + 'warning' => 'running', + 'success' => 'completed', ]), TextColumn::make('outcome') diff --git a/app/Filament/Resources/BackupScheduleResource.php b/app/Filament/Resources/BackupScheduleResource.php index 0249fd4..9f1e051 100644 --- a/app/Filament/Resources/BackupScheduleResource.php +++ b/app/Filament/Resources/BackupScheduleResource.php @@ -685,7 +685,7 @@ public static function table(Table $table): Table $operationRunService->dispatchOrFail($operationRun, function () use ($run, $bulkRun, $operationRun): void { Bus::dispatch(new RunBackupScheduleJob($run->id, $bulkRun?->id, $operationRun)); - }); + }, emitQueuedNotification: false); } $notification = Notification::make() @@ -825,7 +825,7 @@ public static function table(Table $table): Table $operationRunService->dispatchOrFail($operationRun, function () use ($run, $bulkRun, $operationRun): void { Bus::dispatch(new RunBackupScheduleJob($run->id, $bulkRun?->id, $operationRun)); - }); + }, emitQueuedNotification: false); } $notification = Notification::make() diff --git a/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php b/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php index cb604e5..67c7e3e 100644 --- a/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php +++ b/app/Filament/Resources/BackupSetResource/RelationManagers/BackupItemsRelationManager.php @@ -210,7 +210,7 @@ public function table(Table $table): Table ); }); - $this->dispatch(OpsUxBrowserEvents::RunEnqueued); + OpsUxBrowserEvents::dispatchRunEnqueued($this); OperationUxPresenter::queuedToast((string) $opRun->type) ->actions([ Actions\Action::make('view_run') @@ -304,17 +304,13 @@ public function table(Table $table): Table ); }); - $this->dispatch(OpsUxBrowserEvents::RunEnqueued); - Notification::make() - ->title('Removal queued') - ->body('A background job has been queued. You can monitor progress in the run details or progress widget.') - ->success() + OpsUxBrowserEvents::dispatchRunEnqueued($this); + OperationUxPresenter::queuedToast((string) $opRun->type) ->actions([ Actions\Action::make('view_run') ->label('View run') ->url(OperationRunLinks::view($opRun, $tenant)), ]) - ->sendToDatabase($user) ->send(); $this->resetTable(); diff --git a/app/Filament/Resources/OperationRunResource.php b/app/Filament/Resources/OperationRunResource.php index dab9305..355f0f5 100644 --- a/app/Filament/Resources/OperationRunResource.php +++ b/app/Filament/Resources/OperationRunResource.php @@ -256,9 +256,9 @@ public static function getPages(): array private static function statusColor(?string $status): string { return match ($status) { - 'queued' => 'warning', - 'running' => 'info', - 'completed' => 'secondary', + 'queued' => 'secondary', + 'running' => 'warning', + 'completed' => 'success', default => 'gray', }; } diff --git a/app/Filament/Resources/PolicyResource.php b/app/Filament/Resources/PolicyResource.php index e02a3ce..b55c53d 100644 --- a/app/Filament/Resources/PolicyResource.php +++ b/app/Filament/Resources/PolicyResource.php @@ -435,7 +435,6 @@ public static function table(Table $table): Table policyIds: [(int) $record->getKey()], operationRun: $opRun ); - OpsUxBrowserEvents::dispatchRunEnqueued($livewire); OperationUxPresenter::queuedToast((string) $opRun->type) ->actions([ @@ -635,7 +634,6 @@ public static function table(Table $table): Table policyIds: $ids, operationRun: $opRun ); - OpsUxBrowserEvents::dispatchRunEnqueued($livewire); OperationUxPresenter::queuedToast((string) $opRun->type) ->actions([ diff --git a/app/Filament/Resources/PolicyResource/Pages/ListPolicies.php b/app/Filament/Resources/PolicyResource/Pages/ListPolicies.php index bddb230..c83bd8c 100644 --- a/app/Filament/Resources/PolicyResource/Pages/ListPolicies.php +++ b/app/Filament/Resources/PolicyResource/Pages/ListPolicies.php @@ -90,7 +90,6 @@ protected function getHeaderActions(): array operationRun: $opRun ); }); - OpsUxBrowserEvents::dispatchRunEnqueued($livewire); OperationUxPresenter::queuedToast((string) $opRun->type) ->actions([ diff --git a/app/Filament/Resources/TenantResource.php b/app/Filament/Resources/TenantResource.php index df23178..ad6e67c 100644 --- a/app/Filament/Resources/TenantResource.php +++ b/app/Filament/Resources/TenantResource.php @@ -210,7 +210,7 @@ public static function table(Table $table): Table initiator: auth()->user() ); - if (! $opRun->wasRecentlyCreated && in_array($opRun->status, ['queued', 'running'])) { + if (! $opRun->wasRecentlyCreated && in_array($opRun->status, ['queued', 'running'], true)) { Notification::make() ->title('Policy sync already active') ->body('This operation is already queued or running.') diff --git a/app/Notifications/OperationRunCompleted.php b/app/Notifications/OperationRunCompleted.php index 29b5514..807abc9 100644 --- a/app/Notifications/OperationRunCompleted.php +++ b/app/Notifications/OperationRunCompleted.php @@ -28,7 +28,6 @@ public function toDatabase(object $notifiable): array return OperationUxPresenter::terminalDatabaseNotification( run: $this->run, tenant: $tenant instanceof Tenant ? $tenant : null, - ) - ->getDatabaseMessage(); + )->getDatabaseMessage(); } } diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 2b51d14..9d90f61 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -20,7 +20,6 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\StartSession; -use Illuminate\Support\Facades\Blade; use Illuminate\View\Middleware\ShareErrorsFromSession; class AdminPanelProvider extends PanelProvider @@ -42,7 +41,7 @@ public function panel(Panel $panel): Panel ->renderHook( PanelsRenderHook::BODY_END, fn () => (bool) config('tenantpilot.bulk_operations.progress_widget_enabled', true) - ? Blade::render("@livewire('bulk-operation-progress', [], key('ops-ux-progress'))") + ? view('livewire.bulk-operation-progress-wrapper')->render() : '' ) ->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources') diff --git a/app/Services/OperationRunService.php b/app/Services/OperationRunService.php index 4faacc9..db85eb8 100644 --- a/app/Services/OperationRunService.php +++ b/app/Services/OperationRunService.php @@ -6,6 +6,7 @@ use App\Models\Tenant; use App\Models\User; use App\Notifications\OperationRunCompleted as OperationRunCompletedNotification; +use App\Notifications\OperationRunQueued as OperationRunQueuedNotification; use App\Support\OperationRunOutcome; use App\Support\OperationRunStatus; use App\Support\OpsUx\SummaryCountsNormalizer; @@ -142,10 +143,14 @@ public function updateRun( * If dispatch fails synchronously (misconfiguration, serialization errors, etc.), * the OperationRun is marked terminal failed so we do not leave a misleading queued run behind. */ - public function dispatchOrFail(OperationRun $run, callable $dispatcher): void + public function dispatchOrFail(OperationRun $run, callable $dispatcher, bool $emitQueuedNotification = true): void { try { $dispatcher(); + + if ($emitQueuedNotification && $run->wasRecentlyCreated && $run->user instanceof User) { + $run->user->notify(new OperationRunQueuedNotification($run)); + } } catch (Throwable $e) { $this->updateRun( $run, diff --git a/tests/Feature/Notifications/OperationRunNotificationTest.php b/tests/Feature/Notifications/OperationRunNotificationTest.php index a4494e9..e9b38c8 100644 --- a/tests/Feature/Notifications/OperationRunNotificationTest.php +++ b/tests/Feature/Notifications/OperationRunNotificationTest.php @@ -2,11 +2,12 @@ use App\Models\OperationRun; use App\Notifications\OperationRunCompleted; +use App\Notifications\OperationRunQueued; use App\Services\OperationRunService; use App\Support\OperationRunLinks; use Filament\Facades\Filament; -it('does not emit a queued database notification after successful dispatch (toast-only)', function () { +it('emits a queued notification after successful dispatch (initiator only) with view link', function () { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); @@ -25,7 +26,18 @@ // no-op (dispatch succeeded) }); - expect($user->notifications()->count())->toBe(0); + $this->assertDatabaseHas('notifications', [ + 'notifiable_id' => $user->getKey(), + 'notifiable_type' => $user->getMorphClass(), + 'type' => OperationRunQueued::class, + 'data->format' => 'filament', + 'data->title' => 'Policy sync queued', + ]); + + $notification = $user->notifications()->latest('id')->first(); + expect($notification)->not->toBeNull(); + expect($notification->data['actions'][0]['url'] ?? null) + ->toBe(OperationRunLinks::view($run, $tenant)); }); it('does not emit queued notifications for runs without an initiator', function () { diff --git a/tests/Feature/OperationRunServiceTest.php b/tests/Feature/OperationRunServiceTest.php index fb7eb83..13fc55b 100644 --- a/tests/Feature/OperationRunServiceTest.php +++ b/tests/Feature/OperationRunServiceTest.php @@ -138,7 +138,6 @@ $fresh = $run->fresh(); expect($fresh?->status)->toBe('running'); expect($fresh?->started_at)->not->toBeNull(); - $service->updateRun($run, 'completed', 'succeeded', ['succeeded' => 1]); $fresh = $run->fresh(); diff --git a/tests/Feature/OpsUx/NoQueuedDbNotificationsTest.php b/tests/Feature/OpsUx/NoQueuedDbNotificationsTest.php index 062c15c..d387753 100644 --- a/tests/Feature/OpsUx/NoQueuedDbNotificationsTest.php +++ b/tests/Feature/OpsUx/NoQueuedDbNotificationsTest.php @@ -2,10 +2,11 @@ declare(strict_types=1); +use App\Notifications\OperationRunQueued; use App\Services\OperationRunService; use Filament\Facades\Filament; -it('does not emit database notifications when dispatching a queued operation', function (): void { +it('emits at most one queued database notification per newly created run', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); @@ -26,5 +27,10 @@ // no-op (dispatch succeeded) }); - expect($user->notifications()->count())->toBe(0); + expect($user->notifications()->count())->toBe(1); + $this->assertDatabaseHas('notifications', [ + 'notifiable_id' => $user->getKey(), + 'notifiable_type' => $user->getMorphClass(), + 'type' => OperationRunQueued::class, + ]); })->group('ops-ux');