feat/005-bulk-operations #5

Merged
ahmido merged 25 commits from feat/005-bulk-operations into dev 2025-12-25 13:32:37 +00:00
19 changed files with 49 additions and 19 deletions
Showing only changes of commit eef9618889 - Show all commits

View File

@ -27,6 +27,17 @@ ## TenantPilot setup
- Ensure queue workers are running for jobs (e.g., policy sync) after deploy. - Ensure queue workers are running for jobs (e.g., policy sync) after deploy.
- Keep secrets/env in Dokploy, never in code. - Keep secrets/env in Dokploy, never in code.
## Bulk operations (Feature 005)
- Bulk actions are available in Filament resource tables (Policies, Policy Versions, Backup Sets, Restore Runs).
- Destructive operations require type-to-confirm at higher thresholds (e.g. `DELETE`).
- Long-running bulk ops are queued; the bottom-right progress widget polls for active runs.
### Configuration
- `TENANTPILOT_BULK_CHUNK_SIZE` (default `10`): job refresh/progress chunk size.
- `TENANTPILOT_BULK_POLL_INTERVAL_SECONDS` (default `3`): Livewire polling interval for the progress widget (clamped to 110s).
## Intune RBAC Onboarding Wizard ## Intune RBAC Onboarding Wizard
- Entry point: Tenant detail in Filament (`Setup Intune RBAC` in the ⋯ ActionGroup). Visible only for active tenants with `app_client_id`. - Entry point: Tenant detail in Filament (`Setup Intune RBAC` in the ⋯ ActionGroup). Visible only for active tenants with `app_client_id`.

View File

@ -37,7 +37,7 @@ public function handle(BulkOperationService $service): void
$skipped = 0; $skipped = 0;
$skipReasons = []; $skipReasons = [];
$chunkSize = 10; $chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
$totalItems = $run->total_items ?: count($run->item_ids ?? []); $totalItems = $run->total_items ?: count($run->item_ids ?? []);
$failureThreshold = (int) floor($totalItems / 2); $failureThreshold = (int) floor($totalItems / 2);

View File

@ -37,7 +37,7 @@ public function handle(BulkOperationService $service): void
$skipped = 0; $skipped = 0;
$skipReasons = []; $skipReasons = [];
$chunkSize = 10; $chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
$totalItems = $run->total_items ?: count($run->item_ids ?? []); $totalItems = $run->total_items ?: count($run->item_ids ?? []);
$failureThreshold = (int) floor($totalItems / 2); $failureThreshold = (int) floor($totalItems / 2);

View File

@ -37,7 +37,7 @@ public function handle(BulkOperationService $service): void
$skipped = 0; $skipped = 0;
$skipReasons = []; $skipReasons = [];
$chunkSize = 10; $chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
$totalItems = $run->total_items ?: count($run->item_ids ?? []); $totalItems = $run->total_items ?: count($run->item_ids ?? []);
$failureThreshold = (int) floor($totalItems / 2); $failureThreshold = (int) floor($totalItems / 2);

View File

@ -44,7 +44,7 @@ public function handle(BulkOperationService $service): void
$skipped = 0; $skipped = 0;
$failures = []; $failures = [];
$chunkSize = 10; $chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
$totalItems = $run->total_items ?: count($run->item_ids ?? []); $totalItems = $run->total_items ?: count($run->item_ids ?? []);
$failureThreshold = (int) floor($totalItems / 2); $failureThreshold = (int) floor($totalItems / 2);
@ -129,7 +129,7 @@ public function handle(BulkOperationService $service): void
} }
// Refresh the run from database every 10 items to avoid stale data // Refresh the run from database every $chunkSize items to avoid stale data
if ($itemCount % $chunkSize === 0) { if ($itemCount % $chunkSize === 0) {

View File

@ -51,7 +51,7 @@ public function handle(BulkOperationService $service): void
$succeeded = 0; $succeeded = 0;
$failed = 0; $failed = 0;
$failures = []; $failures = [];
$chunkSize = 10; $chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
$totalItems = $run->total_items ?: count($run->item_ids ?? []); $totalItems = $run->total_items ?: count($run->item_ids ?? []);
$failureThreshold = (int) floor($totalItems / 2); $failureThreshold = (int) floor($totalItems / 2);

View File

@ -31,7 +31,7 @@ public function handle(BulkOperationService $service, PolicySyncService $syncSer
$service->start($run); $service->start($run);
try { try {
$chunkSize = 10; $chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
$itemCount = 0; $itemCount = 0;
$totalItems = $run->total_items ?: count($run->item_ids ?? []); $totalItems = $run->total_items ?: count($run->item_ids ?? []);

View File

@ -35,7 +35,7 @@ public function handle(BulkOperationService $service): void
$failed = 0; $failed = 0;
$skipped = 0; $skipped = 0;
$chunkSize = 10; $chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
foreach ($run->item_ids as $policyId) { foreach ($run->item_ids as $policyId) {
$itemCount++; $itemCount++;

View File

@ -37,7 +37,7 @@ public function handle(BulkOperationService $service): void
$skipped = 0; $skipped = 0;
$skipReasons = []; $skipReasons = [];
$chunkSize = 10; $chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
$totalItems = $run->total_items ?: count($run->item_ids ?? []); $totalItems = $run->total_items ?: count($run->item_ids ?? []);
$failureThreshold = (int) floor($totalItems / 2); $failureThreshold = (int) floor($totalItems / 2);

View File

@ -38,7 +38,7 @@ public function handle(BulkOperationService $service): void
$skipped = 0; $skipped = 0;
$skipReasons = []; $skipReasons = [];
$chunkSize = 10; $chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
$totalItems = $run->total_items ?: count($run->item_ids ?? []); $totalItems = $run->total_items ?: count($run->item_ids ?? []);
$failureThreshold = (int) floor($totalItems / 2); $failureThreshold = (int) floor($totalItems / 2);

View File

@ -37,7 +37,7 @@ public function handle(BulkOperationService $service): void
$skipped = 0; $skipped = 0;
$skipReasons = []; $skipReasons = [];
$chunkSize = 10; $chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
$totalItems = $run->total_items ?: count($run->item_ids ?? []); $totalItems = $run->total_items ?: count($run->item_ids ?? []);
$failureThreshold = (int) floor($totalItems / 2); $failureThreshold = (int) floor($totalItems / 2);

View File

@ -38,7 +38,7 @@ public function handle(BulkOperationService $service): void
$skipped = 0; $skipped = 0;
$skipReasons = []; $skipReasons = [];
$chunkSize = 10; $chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
$totalItems = $run->total_items ?: count($run->item_ids ?? []); $totalItems = $run->total_items ?: count($run->item_ids ?? []);
$failureThreshold = (int) floor($totalItems / 2); $failureThreshold = (int) floor($totalItems / 2);

View File

@ -37,7 +37,7 @@ public function handle(BulkOperationService $service): void
$skipped = 0; $skipped = 0;
$skipReasons = []; $skipReasons = [];
$chunkSize = 10; $chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
$totalItems = $run->total_items ?: count($run->item_ids ?? []); $totalItems = $run->total_items ?: count($run->item_ids ?? []);
$failureThreshold = (int) floor($totalItems / 2); $failureThreshold = (int) floor($totalItems / 2);

View File

@ -37,7 +37,7 @@ public function handle(BulkOperationService $service): void
$skipped = 0; $skipped = 0;
$skipReasons = []; $skipReasons = [];
$chunkSize = 10; $chunkSize = max(1, (int) config('tenantpilot.bulk_operations.chunk_size', 10));
$totalItems = $run->total_items ?: count($run->item_ids ?? []); $totalItems = $run->total_items ?: count($run->item_ids ?? []);
$failureThreshold = (int) floor($totalItems / 2); $failureThreshold = (int) floor($totalItems / 2);

View File

@ -11,8 +11,11 @@ class BulkOperationProgress extends Component
{ {
public $runs; public $runs;
public int $pollSeconds = 3;
public function mount() public function mount()
{ {
$this->pollSeconds = max(1, min(10, (int) config('tenantpilot.bulk_operations.poll_interval_seconds', 3)));
$this->loadRuns(); $this->loadRuns();
} }

View File

@ -118,4 +118,9 @@
'features' => [ 'features' => [
'conditional_access' => true, 'conditional_access' => true,
], ],
'bulk_operations' => [
'chunk_size' => (int) env('TENANTPILOT_BULK_CHUNK_SIZE', 10),
'poll_interval_seconds' => (int) env('TENANTPILOT_BULK_POLL_INTERVAL_SECONDS', 3),
],
]; ];

View File

@ -1,4 +1,4 @@
<div wire:poll.3s="loadRuns"> <div wire:poll.{{ $pollSeconds }}s="loadRuns">
<!-- Bulk Operation Progress Component: {{ $runs->count() }} active runs --> <!-- Bulk Operation Progress Component: {{ $runs->count() }} active runs -->
@if($runs->isNotEmpty()) @if($runs->isNotEmpty())
<div class="fixed bottom-4 right-4 z-[999999] w-96 space-y-2" style="pointer-events: auto;"> <div class="fixed bottom-4 right-4 z-[999999] w-96 space-y-2" style="pointer-events: auto;">

View File

@ -120,6 +120,17 @@ ### Browser Tests (Pest v4)
--- ---
## Configuration
These defaults are safe for staging/production, but can be tuned per environment.
- **Chunk size** (job refresh/progress cadence):
- `TENANTPILOT_BULK_CHUNK_SIZE` (default `10`)
- **Progress polling interval** (UI updates):
- `TENANTPILOT_BULK_POLL_INTERVAL_SECONDS` (default `3`, clamped to 110 seconds)
- **Policy version prune retention window**:
- Default `90` days (editable in the prune modal as “Retention Days”)
## Manual Testing Workflow ## Manual Testing Workflow
### Scenario 1: Bulk Delete Policies (< 20 items) ### Scenario 1: Bulk Delete Policies (< 20 items)

View File

@ -166,7 +166,7 @@ ### Implementation for User Story 6
- [ ] T053 [US6] Test circuit breaker with mock failures (manual QA) - [ ] T053 [US6] Test circuit breaker with mock failures (manual QA)
- [x] T054 [US6] Run tests: `./vendor/bin/sail artisan test tests/Feature/BulkProgressNotificationTest.php` - [x] T054 [US6] Run tests: `./vendor/bin/sail artisan test tests/Feature/BulkProgressNotificationTest.php`
- [ ] T054a [US6] Define/verify polling interval meets NFR-005.3 (≤10s updates) and document where configured - [x] T054a [US6] Define/verify polling interval meets NFR-005.3 (≤10s updates) and document where configured
- [ ] T054b [US6] Manual QA: force a catastrophic job failure and verify FR-005.13 notification behavior - [ ] T054b [US6] Manual QA: force a catastrophic job failure and verify FR-005.13 notification behavior
- [ ] T054c [US6] Manual QA: verify UI remains responsive during a 100-item queued run (NFR-005.4) - [ ] T054c [US6] Manual QA: verify UI remains responsive during a 100-item queued run (NFR-005.4)
@ -288,8 +288,8 @@ ## Phase 10: Polish & Cross-Cutting Concerns
**Purpose**: Documentation, cleanup, performance optimization **Purpose**: Documentation, cleanup, performance optimization
- [ ] T086 [P] Update README.md with bulk operations feature description - [x] T086 [P] Update README.md with bulk operations feature description
- [ ] T087 [P] Update quickstart.md with manual testing scenarios (already done in planning) - [x] T087 [P] Update quickstart.md with manual testing scenarios (already done in planning)
- [ ] T088 Code cleanup: Remove debug statements, refactor duplicated logic - [ ] T088 Code cleanup: Remove debug statements, refactor duplicated logic
- [ ] T089 Performance test: Bulk delete 500 policies (should complete <5 minutes) - [ ] T089 Performance test: Bulk delete 500 policies (should complete <5 minutes)
- [ ] T090 Load test: Concurrent bulk operations (2-3 admins, different resources) - [ ] T090 Load test: Concurrent bulk operations (2-3 admins, different resources)
@ -298,7 +298,7 @@ ## Phase 10: Polish & Cross-Cutting Concerns
- [ ] T093 Run full test suite: `./vendor/bin/sail artisan test` - [ ] T093 Run full test suite: `./vendor/bin/sail artisan test`
- [ ] T094 Run Pint formatting: `./vendor/bin/sail composer pint` - [ ] T094 Run Pint formatting: `./vendor/bin/sail composer pint`
- [ ] T095 Manual QA checklist: Complete all scenarios from quickstart.md - [ ] T095 Manual QA checklist: Complete all scenarios from quickstart.md
- [ ] T096 Document configuration options (chunk size, polling interval, retention days) - [x] T096 Document configuration options (chunk size, polling interval, retention days)
- [ ] T097 Create BulkOperationRun resource page in Filament (view progress, retry failed) - [ ] T097 Create BulkOperationRun resource page in Filament (view progress, retry failed)
- [ ] T098 Add bulk operation metrics to dashboard (total runs, success rate) - [ ] T098 Add bulk operation metrics to dashboard (total runs, success rate)