Adds: - plan.md: Technical context, constitution check, phases - research.md: 7 research decisions (progress tracking, chunking, type-to-confirm) - data-model.md: BulkOperationRun model, schema changes, query patterns - quickstart.md: Developer onboarding, testing workflows, debugging Key Decisions: - BulkOperationRun model + Livewire polling for progress - collect()->chunk(10) for memory-efficient processing - Filament form + validation for type-to-confirm - ignored_at flag to prevent sync re-adding deleted policies - Eligibility scopes for safe Policy Version pruning Estimated: 26-34 hours (3 phases for P1/P2 features) Next: /speckit.tasks to generate task breakdown
426 lines
9.9 KiB
Markdown
426 lines
9.9 KiB
Markdown
# Quickstart: Feature 005 - Bulk Operations
|
|
|
|
**Feature**: Bulk Operations for Resource Management
|
|
**Date**: 2025-12-22
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
This quickstart guide helps developers get up and running with Feature 005 (Bulk Operations) for local development, testing, and debugging.
|
|
|
|
---
|
|
|
|
## Prerequisites
|
|
|
|
- Laravel Sail installed and running
|
|
- Composer dependencies installed
|
|
- NPM dependencies installed
|
|
- Database migrated
|
|
- At least one Tenant configured
|
|
- Sample Policies, PolicyVersions, BackupSets, RestoreRuns seeded
|
|
|
|
---
|
|
|
|
## Local Development Setup
|
|
|
|
### 1. Start Sail
|
|
|
|
```bash
|
|
cd /path/to/TenantAtlas
|
|
./vendor/bin/sail up -d
|
|
```
|
|
|
|
### 2. Run Migrations
|
|
|
|
```bash
|
|
./vendor/bin/sail artisan migrate
|
|
```
|
|
|
|
This creates:
|
|
- `bulk_operation_runs` table
|
|
- `ignored_at` column on `policies` table
|
|
|
|
### 3. Seed Test Data (Optional)
|
|
|
|
```bash
|
|
./vendor/bin/sail artisan db:seed --class=BulkOperationsTestSeeder
|
|
```
|
|
|
|
Creates:
|
|
- 100 test policies
|
|
- 50 policy versions (some old, some referenced)
|
|
- 10 backup sets
|
|
- 20 restore runs (mixed statuses)
|
|
|
|
### 4. Start Queue Worker
|
|
|
|
Bulk operations require queue processing:
|
|
|
|
```bash
|
|
./vendor/bin/sail artisan queue:work --tries=3 --timeout=300
|
|
```
|
|
|
|
Or run in background with Supervisor (production):
|
|
|
|
```bash
|
|
./vendor/bin/sail artisan queue:restart
|
|
```
|
|
|
|
### 5. Access Filament Panel
|
|
|
|
```bash
|
|
open http://localhost/admin
|
|
```
|
|
|
|
Navigate to:
|
|
- **Policies** → Select multiple → Bulk Actions dropdown
|
|
- **Policy Versions** → Bulk Prune
|
|
- **Backup Sets** → Bulk Delete
|
|
- **Restore Runs** → Bulk Delete
|
|
|
|
---
|
|
|
|
## Running Tests
|
|
|
|
### Unit Tests
|
|
|
|
Test individual components (jobs, scopes, helpers):
|
|
|
|
```bash
|
|
./vendor/bin/sail artisan test tests/Unit/BulkPolicyDeleteJobTest.php
|
|
./vendor/bin/sail artisan test tests/Unit/BulkActionPermissionTest.php
|
|
./vendor/bin/sail artisan test tests/Unit/BulkEligibilityCheckTest.php
|
|
```
|
|
|
|
### Feature Tests
|
|
|
|
Test E2E flows (UI → job → database):
|
|
|
|
```bash
|
|
./vendor/bin/sail artisan test tests/Feature/BulkDeletePoliciesTest.php
|
|
./vendor/bin/sail artisan test tests/Feature/BulkExportToBackupTest.php
|
|
./vendor/bin/sail artisan test tests/Feature/BulkProgressNotificationTest.php
|
|
./vendor/bin/sail artisan test tests/Feature/BulkTypeToConfirmTest.php
|
|
```
|
|
|
|
### All Tests
|
|
|
|
```bash
|
|
./vendor/bin/sail artisan test --filter=Bulk
|
|
```
|
|
|
|
### Browser Tests (Pest v4)
|
|
|
|
Test UI interactions:
|
|
|
|
```bash
|
|
./vendor/bin/sail artisan test tests/Browser/BulkOperationsTest.php
|
|
```
|
|
|
|
---
|
|
|
|
## Manual Testing Workflow
|
|
|
|
### Scenario 1: Bulk Delete Policies (< 20 items)
|
|
|
|
1. **Navigate**: Admin → Policies
|
|
2. **Select**: Check 10 policies
|
|
3. **Action**: Click "Delete" in bulk actions dropdown
|
|
4. **Confirm**: Modal appears: "Delete 10 policies?"
|
|
5. **Submit**: Click "Confirm"
|
|
6. **Verify**:
|
|
- Success notification: "Deleted 10 policies"
|
|
- Policies have `ignored_at` timestamp set
|
|
- Policies still exist in Intune (no Graph DELETE call)
|
|
- Audit log entry created
|
|
|
|
### Scenario 2: Bulk Delete Policies (≥ 20 items, queued)
|
|
|
|
1. **Navigate**: Admin → Policies
|
|
2. **Select**: Check 25 policies
|
|
3. **Action**: Click "Delete"
|
|
4. **Confirm**: Modal requires typing "DELETE"
|
|
5. **Type**: Enter "DELETE" (case-sensitive)
|
|
6. **Submit**: Click "Confirm"
|
|
7. **Verify**:
|
|
- Job dispatched to queue
|
|
- Progress notification: "Deleting policies... 0/25"
|
|
- Notification updates every 5s: "Deleting... 10/25", "20/25"
|
|
- Final notification: "Deleted 25 policies"
|
|
- `BulkOperationRun` record created with status `completed`
|
|
- Audit log entry
|
|
|
|
### Scenario 3: Bulk Export to Backup
|
|
|
|
1. **Navigate**: Admin → Policies
|
|
2. **Select**: Check 30 policies
|
|
3. **Action**: Click "Export to Backup"
|
|
4. **Form**:
|
|
- Backup Set Name: "Production Snapshot"
|
|
- Include Assignments: ☑ (if Feature 004 implemented)
|
|
5. **Submit**: Click "Confirm"
|
|
6. **Verify**:
|
|
- Job dispatched
|
|
- Progress: "Backing up... 10/30"
|
|
- Final: "Backup Set 'Production Snapshot' created (30 items)"
|
|
- New `BackupSet` record
|
|
- 30 `BackupItem` records
|
|
- Audit log entry
|
|
|
|
### Scenario 4: Bulk Prune Policy Versions
|
|
|
|
1. **Navigate**: Admin → Policy Versions
|
|
2. **Filter**: Show only non-current versions older than 90 days
|
|
3. **Select**: Check 15 versions
|
|
4. **Action**: Click "Delete"
|
|
5. **Confirm**: Type "DELETE"
|
|
6. **Submit**: Click "Confirm"
|
|
7. **Verify**:
|
|
- Eligibility check runs
|
|
- Eligible versions deleted (hard delete)
|
|
- Ineligible versions skipped
|
|
- Notification: "Deleted 12 policy versions (3 skipped)"
|
|
- Failures array shows skip reasons:
|
|
- "Referenced in Backup Set #5"
|
|
- "Current version"
|
|
- "Too recent (< 90 days)"
|
|
|
|
### Scenario 5: Circuit Breaker (abort on >50% fail)
|
|
|
|
1. **Setup**: Mock Graph API to fail for 60% of items
|
|
2. **Navigate**: Admin → Policies
|
|
3. **Select**: Check 100 policies
|
|
4. **Action**: Bulk Delete
|
|
5. **Verify**:
|
|
- Job processes ~50 items
|
|
- Detects >50% failure rate
|
|
- Aborts remaining items
|
|
- Notification: "Bulk operation aborted: 55/100 failures exceeded threshold"
|
|
- `BulkOperationRun.status` = `aborted`
|
|
|
|
---
|
|
|
|
## Debugging
|
|
|
|
### View Queue Jobs
|
|
|
|
```bash
|
|
# List failed jobs
|
|
./vendor/bin/sail artisan queue:failed
|
|
|
|
# Retry failed job
|
|
./vendor/bin/sail artisan queue:retry <job-id>
|
|
|
|
# Flush failed jobs
|
|
./vendor/bin/sail artisan queue:flush
|
|
```
|
|
|
|
### Inspect BulkOperationRun Records
|
|
|
|
```bash
|
|
./vendor/bin/sail tinker
|
|
```
|
|
|
|
```php
|
|
// Get recent runs
|
|
$runs = \App\Models\BulkOperationRun::recent()->limit(10)->get();
|
|
|
|
// View failures
|
|
$run = \App\Models\BulkOperationRun::find(1);
|
|
dd($run->failures);
|
|
|
|
// Check progress
|
|
echo "{$run->processed_items}/{$run->total_items} ({$run->progressPercentage()}%)";
|
|
```
|
|
|
|
### View Audit Logs
|
|
|
|
```bash
|
|
# Via Filament UI
|
|
# Navigate to: Admin → Audit Logs → Filter by event: "policies.bulk_deleted"
|
|
|
|
# Via Tinker
|
|
$logs = \App\Models\AuditLog::where('event', 'policies.bulk_deleted')->get();
|
|
```
|
|
|
|
### Database Queries
|
|
|
|
```bash
|
|
./vendor/bin/sail artisan tinker
|
|
```
|
|
|
|
```php
|
|
// Policies marked as ignored
|
|
$ignored = \App\Models\Policy::ignored()->count();
|
|
|
|
// Policy versions eligible for pruning
|
|
$eligible = \App\Models\PolicyVersion::pruneEligible(90)->count();
|
|
|
|
// Deletable restore runs
|
|
$deletable = \App\Models\RestoreRun::deletable()->count();
|
|
```
|
|
|
|
### Test Queue Job Manually
|
|
|
|
```bash
|
|
./vendor/bin/sail artisan tinker
|
|
```
|
|
|
|
```php
|
|
use App\Jobs\BulkPolicyDeleteJob;
|
|
use App\Models\BulkOperationRun;
|
|
|
|
$policyIds = [1, 2, 3];
|
|
$tenantId = 1;
|
|
$userId = 1;
|
|
|
|
$run = BulkOperationRun::create([
|
|
'tenant_id' => $tenantId,
|
|
'user_id' => $userId,
|
|
'resource' => 'policies',
|
|
'action' => 'delete',
|
|
'status' => 'pending',
|
|
'total_items' => count($policyIds),
|
|
'item_ids' => $policyIds,
|
|
]);
|
|
|
|
// Dispatch synchronously for debugging
|
|
BulkPolicyDeleteJob::dispatchSync($policyIds, $tenantId, $userId, $run->id);
|
|
|
|
// Check result
|
|
$run->refresh();
|
|
echo $run->summaryText();
|
|
```
|
|
|
|
---
|
|
|
|
## Common Issues & Solutions
|
|
|
|
### Issue 1: Type-to-confirm not working
|
|
|
|
**Symptom**: Confirm button remains enabled without typing "DELETE"
|
|
|
|
**Solution**: Check Filament form validation rule:
|
|
```php
|
|
->rule('in:DELETE') // Case-sensitive
|
|
```
|
|
|
|
### Issue 2: Progress notifications don't update
|
|
|
|
**Symptom**: Progress stuck at "0/100"
|
|
|
|
**Solution**:
|
|
- Ensure queue worker is running: `./vendor/bin/sail artisan queue:work`
|
|
- Check Livewire polling: `wire:poll.5s="refresh"`
|
|
- Verify BulkOperationRun is updated in job
|
|
|
|
### Issue 3: Policies reappear after deletion
|
|
|
|
**Symptom**: Deleted policies show up again after sync
|
|
|
|
**Solution**:
|
|
- Check `ignored_at` is set: `Policy::find($id)->ignored_at`
|
|
- Verify SyncPoliciesJob filters: `->whereNull('ignored_at')`
|
|
|
|
### Issue 4: Circuit breaker not aborting
|
|
|
|
**Symptom**: Job continues despite >50% failures
|
|
|
|
**Solution**: Check circuit breaker logic in job:
|
|
```php
|
|
if ($results['failed'] > count($this->policyIds) * 0.5) {
|
|
$run->update(['status' => 'aborted']);
|
|
throw new \Exception('Bulk operation aborted: >50% failure rate');
|
|
}
|
|
```
|
|
|
|
### Issue 5: Policy versions deleted despite references
|
|
|
|
**Symptom**: Referenced versions are deleted
|
|
|
|
**Solution**: Verify eligibility scope includes:
|
|
```php
|
|
->whereDoesntHave('backupItems')
|
|
->whereNotIn('id', function ($subquery) {
|
|
$subquery->select(DB::raw("CAST(metadata->>'policy_version_id' AS INTEGER)"))
|
|
->from('restore_runs')
|
|
->whereNotNull(DB::raw("metadata->>'policy_version_id'"));
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Performance Benchmarks
|
|
|
|
Expected performance (local Sail environment):
|
|
|
|
| Operation | Item Count | Duration | Notes |
|
|
|-----------|------------|----------|-------|
|
|
| Bulk Delete (sync) | 10 | <1s | Immediate feedback |
|
|
| Bulk Delete (queued) | 100 | <2min | Chunked, progress updates |
|
|
| Bulk Export | 50 | <3min | Includes Graph API calls |
|
|
| Bulk Prune | 30 | <30s | Eligibility checks |
|
|
| Progress Update | - | 5s | Polling interval |
|
|
|
|
---
|
|
|
|
## Code Formatting
|
|
|
|
Before committing:
|
|
|
|
```bash
|
|
./vendor/bin/sail composer pint
|
|
```
|
|
|
|
Formats all PHP files per PSR-12.
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. ✅ Complete Phase 2.1 (Foundation) tasks
|
|
2. ✅ Run all tests: `./vendor/bin/sail artisan test --filter=Bulk`
|
|
3. ✅ Manual QA: Follow scenarios above
|
|
4. ✅ Code review: Check PSR-12, permissions, audit logs
|
|
5. ✅ Load testing: Bulk delete 500 items
|
|
6. → Deploy to staging
|
|
7. → Manual QA on staging
|
|
8. → Deploy to production
|
|
|
|
---
|
|
|
|
## Useful Commands
|
|
|
|
```bash
|
|
# Watch queue jobs in real-time
|
|
./vendor/bin/sail artisan queue:work --verbose
|
|
|
|
# Monitor bulk operations
|
|
./vendor/bin/sail artisan tinker
|
|
>>> BulkOperationRun::inProgress()->get()
|
|
|
|
# Seed more test data
|
|
./vendor/bin/sail artisan db:seed --class=BulkOperationsTestSeeder
|
|
|
|
# Clear cache
|
|
./vendor/bin/sail artisan optimize:clear
|
|
|
|
# Restart queue workers (after code changes)
|
|
./vendor/bin/sail artisan queue:restart
|
|
```
|
|
|
|
---
|
|
|
|
## Resources
|
|
|
|
- [Laravel Queue Documentation](https://laravel.com/docs/12.x/queues)
|
|
- [Filament Bulk Actions](https://filamentphp.com/docs/4.x/tables/actions#bulk-actions)
|
|
- [Livewire Polling](https://livewire.laravel.com/docs/polling)
|
|
- [Pest Testing](https://pestphp.com/docs)
|
|
|
|
---
|
|
|
|
**Status**: Quickstart Complete
|
|
**Next Step**: Update agent context with new learnings
|