TenantAtlas/resources/views/livewire/bulk-operation-progress.blade.php
ahmido 3030dd9af2 054-unify-runs-suitewide (#63)
Summary

Kurz: Implementiert Feature 054 — canonical OperationRun-flow, Monitoring UI, dispatch-safety, notifications, dedupe, plus small UX safety clarifications (RBAC group search delegated; Restore group mapping DB-only).
What Changed

Core service: OperationRun lifecycle, dedupe and dispatch helpers — OperationRunService.php.
Model + migration: OperationRun model and migration — OperationRun.php, 2026_01_16_180642_create_operation_runs_table.php.
Notifications: queued + terminal DB notifications (initiator-only) — OperationRunQueued.php, OperationRunCompleted.php.
Monitoring UI: Filament list/detail + Livewire pieces (DB-only render) — OperationRunResource.php and related pages/views.
Start surfaces / Jobs: instrumented start surfaces, job middleware, and job updates to use canonical runs — multiple app/Jobs/* and app/Filament/* updates (see tests for full coverage).
RBAC + Restore UX clarifications: RBAC group search is delegated-Graph-based and disabled without delegated token; Restore group mapping remains DB-only (directory cache) and helper text always visible — TenantResource.php, RestoreRunResource.php.
Specs / Constitution: updated spec & quickstart and added one-line constitution guideline about Graph usage:
spec.md
quickstart.md
constitution.md
Tests & Verification

Unit / Feature tests added/updated for run lifecycle, notifications, idempotency, and UI guards: see tests/Feature/* (notably OperationRunServiceTest, MonitoringOperationsTest, OperationRunNotificationTest, and various Filament feature tests).
Full test run locally: ./vendor/bin/sail artisan test → 587 passed, 5 skipped.
Migrations

Adds create_operation_runs_table migration; run php artisan migrate in staging after review.
Notes / Rationale

Monitoring pages are explicitly DB-only at render time (no Graph calls). Start surfaces enqueue work only and return a “View run” link.
Delegated Graph access is used only for explicit user actions (RBAC group search); restore mapping intentionally uses cached DB data only to avoid render-time Graph calls.
Dispatch wrapper marks runs failed immediately if background dispatch throws synchronously to avoid misleading “queued” states.
Upgrade / Deploy Considerations

Run migrations: ./vendor/bin/sail artisan migrate.
Background workers should be running to process queued jobs (recommended to monitor queue health during rollout).
No secret or token persistence changes.
PR checklist

 Tests updated/added for changed behavior
 Specs updated: 054-unify-runs-suitewide docs + quickstart
 Constitution note added (.specify)
 Pint formatting applied

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #63
2026-01-17 22:25:00 +00:00

86 lines
5.6 KiB
PHP

@php($runs = $runs ?? collect())
@php($interval = $runs->isEmpty() ? max((int) $pollSeconds, 10) : (int) $pollSeconds)
<div wire:poll.{{ $interval }}s="loadRuns">
<!-- Bulk Operation Progress Component: {{ $runs->count() }} active runs -->
@if($runs->isNotEmpty())
<div class="fixed bottom-4 right-4 z-[999999] w-96 space-y-2" style="pointer-events: auto;">
@foreach ($runs as $run)
@php($effectiveTotal = max((int) $run->total_items, (int) $run->processed_items))
@php($percent = $effectiveTotal > 0 ? min(100, round(($run->processed_items / $effectiveTotal) * 100)) : 0)
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl border-2 border-primary-500 dark:border-primary-400 p-4 transition-all animate-in slide-in-from-right duration-300"
wire:key="run-{{ $run->id }}">
<div class="flex justify-between items-start mb-3">
<div class="flex-1">
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ ucfirst($run->action) }} {{ ucfirst(str_replace('_', ' ', $run->resource)) }}
</h4>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
@if($run->status === 'pending')
@php($isStalePending = $run->created_at->lt(now()->subSeconds(30)))
<span class="inline-flex items-center">
<svg class="animate-spin -ml-1 mr-1.5 h-3 w-3 text-primary-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ $isStalePending ? 'Queued…' : 'Starting...' }}
</span>
@elseif($run->status === 'running')
<span class="inline-flex items-center">
<svg class="animate-spin -ml-1 mr-1.5 h-3 w-3 text-primary-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Processing...
</span>
@elseif(in_array($run->status, ['completed', 'completed_with_errors'], true))
<span class="text-success-600 dark:text-success-400">Done</span>
@elseif(in_array($run->status, ['failed', 'aborted'], true))
<span class="text-danger-600 dark:text-danger-400">Failed</span>
@endif
</p>
</div>
<div class="text-right">
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">
{{ $run->processed_items }} / {{ $effectiveTotal }}
</span>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{{ $percent }}%
</div>
</div>
</div>
<div class="w-full bg-gray-200 rounded-full h-3 dark:bg-gray-700 overflow-hidden">
<div class="bg-primary-600 dark:bg-primary-500 h-3 rounded-full transition-all duration-300 ease-out"
style="width: {{ $percent }}%"></div>
</div>
<div class="mt-2 flex items-center justify-between text-xs">
<div class="flex items-center gap-3">
@if ($run->succeeded > 0)
<span class="text-success-600 dark:text-success-400">
{{ $run->succeeded }} succeeded
</span>
@endif
@if ($run->failed > 0)
<span class="text-danger-600 dark:text-danger-400">
{{ $run->failed }} failed
</span>
@endif
@if ($run->skipped > 0)
<span class="text-gray-500 dark:text-gray-400">
{{ $run->skipped }} skipped
</span>
@endif
</div>
<span class="text-gray-400 dark:text-gray-500">
{{ $run->created_at->diffForHumans(null, true, true) }}
</span>
</div>
</div>
@endforeach
</div>
@endif
</div>