feat/004-assignments-scope-tags #4

Merged
ahmido merged 41 commits from feat/004-assignments-scope-tags into dev 2025-12-23 21:49:59 +00:00
Showing only changes of commit bd4608551b - Show all commits

View File

@ -0,0 +1,889 @@
# Feature 004: Assignments & Scope Tags - Task Breakdown
## Overview
This document breaks down the implementation plan into granular, actionable tasks organized by phase and user story.
**Total Estimated Tasks**: 62 tasks
**MVP Scope**: Tasks marked with ⭐ (24 tasks, ~16-22 hours)
**Full Implementation**: All tasks (~30-40 hours)
---
## Phase 1: Setup & Database (Foundation)
**Duration**: 2-3 hours
**Dependencies**: None
**Parallelizable**: No (sequential setup)
### Tasks
**1.1** ⭐ Create migration: `add_assignments_to_backup_items`
- File: `database/migrations/xxxx_add_assignments_to_backup_items.php`
- Add `assignments` JSONB column after `metadata`
- Make nullable
- Write reversible `down()` method
- Test: `php artisan migrate` and `migrate:rollback`
**1.2** ⭐ Create migration: `add_group_mapping_to_restore_runs`
- File: `database/migrations/xxxx_add_group_mapping_to_restore_runs.php`
- Add `group_mapping` JSONB column after `results`
- Make nullable
- Write reversible `down()` method
- Test: `php artisan migrate` and `migrate:rollback`
**1.3** ⭐ Update `BackupItem` model with assignments cast
- Add `'assignments' => 'array'` to `$casts`
- Add `assignments` to `$fillable`
- Test: Create BackupItem with assignments, verify cast works
**1.4** ⭐ Add `BackupItem` assignment accessor methods
- `getAssignmentCountAttribute(): int`
- `hasAssignments(): bool`
- `getGroupIdsAttribute(): array`
- `getScopeTagIdsAttribute(): array`
- `getScopeTagNamesAttribute(): array`
- `hasOrphanedAssignments(): bool`
- `assignmentsFetchFailed(): bool`
**1.5** ⭐ Add `BackupItem` scope: `scopeWithAssignments()`
- Filter policies with non-null assignments
- Use `whereNotNull('assignments')` and `whereRaw('json_array_length(assignments) > 0')`
**1.6** ⭐ Update `RestoreRun` model with group_mapping cast
- Add `'group_mapping' => 'array'` to `$casts`
- Add `group_mapping` to `$fillable`
- Test: Create RestoreRun with group_mapping, verify cast works
**1.7** ⭐ Add `RestoreRun` group mapping helper methods
- `hasGroupMapping(): bool`
- `getMappedGroupId(string $sourceGroupId): ?string`
- `isGroupSkipped(string $sourceGroupId): bool`
- `getUnmappedGroupIds(array $sourceGroupIds): array`
- `addGroupMapping(string $sourceGroupId, string $targetGroupId): void`
**1.8** ⭐ Add `RestoreRun` assignment outcome methods
- `getAssignmentRestoreOutcomes(): array`
- `getSuccessfulAssignmentsCount(): int`
- `getFailedAssignmentsCount(): int`
- `getSkippedAssignmentsCount(): int`
**1.9** ⭐ Update `config/graph_contracts.php` with assignments endpoints
- Add `assignments_list_path` (GET)
- Add `assignments_create_path` (POST)
- Add `assignments_delete_path` (DELETE)
- Add `supports_scope_tags: true`
- Add `scope_tag_field: 'roleScopeTagIds'`
**1.10** ⭐ Write unit tests: `BackupItemTest`
- Test assignment accessors
- Test scope `withAssignments()`
- Test metadata helpers (scope tags, orphaned flags)
- Expected: 100% coverage for new methods
**1.11** ⭐ Write unit tests: `RestoreRunTest`
- Test group mapping helpers
- Test assignment outcome methods
- Test `addGroupMapping()` persistence
- Expected: 100% coverage for new methods
**1.12** Run Pint: Format all new code
- `./vendor/bin/pint database/migrations/`
- `./vendor/bin/pint app/Models/BackupItem.php`
- `./vendor/bin/pint app/Models/RestoreRun.php`
**1.13** Verify tests pass
- Run: `php artisan test --filter=BackupItem`
- Run: `php artisan test --filter=RestoreRun`
- Expected: All green
---
## Phase 2: Graph API Integration (Core Services)
**Duration**: 4-6 hours
**Dependencies**: Phase 1 complete
**Parallelizable**: Yes (services independent)
### Tasks
**2.1** ⭐ Create service: `AssignmentFetcher`
- File: `app/Services/Graph/AssignmentFetcher.php`
- Method: `fetch(string $tenantId, string $policyId): array`
- Implement primary endpoint: GET `/assignments`
- Implement fallback: GET with `$expand=assignments`
- Return empty array on failure (fail-soft)
- Log warnings with request IDs
**2.2** ⭐ Add error handling to `AssignmentFetcher`
- Catch `GraphException`
- Log: tenant_id, policy_id, error message, request_id
- Return empty array (don't throw)
- Set flag for caller: `assignments_fetch_failed`
**2.3** ⭐ Write unit test: `AssignmentFetcherTest::primary_endpoint_success`
- Mock Graph response with assignments
- Assert returned array matches response
- Assert no fallback called
**2.4** ⭐ Write unit test: `AssignmentFetcherTest::fallback_on_empty_response`
- Mock primary returning empty array
- Mock fallback returning assignments
- Assert fallback called
- Assert assignments returned
**2.5** ⭐ Write unit test: `AssignmentFetcherTest::fail_soft_on_error`
- Mock both endpoints throwing `GraphException`
- Assert empty array returned
- Assert warning logged
**2.6** Create service: `GroupResolver`
- File: `app/Services/Graph/GroupResolver.php`
- Method: `resolveGroupIds(array $groupIds, string $tenantId): array`
- Implement: POST `/directoryObjects/getByIds`
- Return keyed array: `['group-id' => ['id', 'displayName', 'orphaned']]`
- Handle orphaned IDs (not in response)
**2.7** Add caching to `GroupResolver`
- Cache key: `"groups:{$tenantId}:" . md5(implode(',', $groupIds))`
- TTL: 5 minutes
- Use `Cache::remember()`
**2.8** Write unit test: `GroupResolverTest::resolves_all_groups`
- Mock Graph response with all group IDs
- Assert all resolved with names
- Assert `orphaned: false`
**2.9** Write unit test: `GroupResolverTest::handles_orphaned_ids`
- Mock Graph response missing some IDs
- Assert orphaned IDs have `displayName: null`
- Assert `orphaned: true`
**2.10** Write unit test: `GroupResolverTest::caches_results`
- Call resolver twice with same IDs
- Assert Graph API called only once
- Assert cache hit on second call
**2.11** Create service: `ScopeTagResolver`
- File: `app/Services/Graph/ScopeTagResolver.php`
- Method: `resolve(array $scopeTagIds): array`
- Implement: GET `/deviceManagement/roleScopeTags?$select=id,displayName`
- Return array of scope tag objects
- Cache results (1 hour TTL)
**2.12** Add cache to `ScopeTagResolver`
- Cache key: `"scope_tags:all"`
- TTL: 1 hour
- Fetch all scope tags once, filter in memory
**2.13** Write unit test: `ScopeTagResolverTest::resolves_scope_tags`
- Mock Graph response
- Assert correct scope tags returned
- Assert filtered to requested IDs only
**2.14** Write unit test: `ScopeTagResolverTest::caches_results`
- Call resolver twice
- Assert Graph API called only once
- Assert cache hit on second call
**2.15** Run Pint: Format service classes
- `./vendor/bin/pint app/Services/Graph/`
**2.16** Verify service tests pass
- Run: `php artisan test --filter=AssignmentFetcher`
- Run: `php artisan test --filter=GroupResolver`
- Run: `php artisan test --filter=ScopeTagResolver`
- Expected: All green, 90%+ coverage
---
## Phase 3: US1 - Backup with Assignments (MVP Core)
**Duration**: 4-5 hours
**Dependencies**: Phase 2 complete
**Parallelizable**: Partially (service and UI separate)
### Tasks
**3.1** ⭐ Add checkbox to Backup creation form
- File: `app/Filament/Resources/BackupResource/Pages/CreateBackup.php`
- Component: `Checkbox::make('include_assignments')`
- Label: "Include Assignments & Scope Tags"
- Help text: "Captures group/user targeting and RBAC scope. Adds ~2-5 KB per policy."
- Default: `false`
- Show only for Settings Catalog policies
**3.2** ⭐ Create service: `AssignmentBackupService`
- File: `app/Services/AssignmentBackupService.php`
- Method: `backup(int $tenantId, string $policyId, bool $includeAssignments): BackupItem`
- Call `AssignmentFetcher` if `includeAssignments` true
- Call `ScopeTagResolver` for scope tags
- Update `backup_items.assignments` and `metadata`
**3.3** ⭐ Implement assignment fetch in `AssignmentBackupService`
- Check `includeAssignments` flag
- Call `$this->assignmentFetcher->fetch($tenantId, $policyId)`
- Store result in `$backupItem->assignments`
- If empty or failed, set `metadata['assignments_fetch_failed'] = true`
**3.4** ⭐ Implement scope tag resolution in `AssignmentBackupService`
- Extract `roleScopeTagIds` from policy payload
- Call `$this->scopeTagResolver->resolve($scopeTagIds)`
- Store in `metadata['scope_tag_ids']` and `metadata['scope_tag_names']`
**3.5** ⭐ Update `metadata` with assignment summary
- Set `metadata['assignment_count']`
- Set `metadata['has_orphaned_assignments']` (detect via GroupResolver)
- Set `metadata['assignments_fetch_failed']` on error
**3.6** Create job: `FetchAssignmentsJob`
- File: `app/Jobs/FetchAssignmentsJob.php`
- Dispatch async after backup creation if checkbox enabled
- Call `AssignmentBackupService` in job
- Handle failures gracefully (log, don't retry)
**3.7** Add audit log entry: `backup.assignments.included`
- Create entry when checkbox enabled
- Metadata: tenant_id, backup_set_id, policy_count, assignment_count
**3.8** ⭐ Write feature test: `BackupWithAssignmentsTest::creates_backup_with_assignments`
- Mock Graph API responses (assignments, scope tags)
- Create backup with checkbox enabled
- Assert `backup_items.assignments` populated
- Assert `metadata['assignment_count']` correct
- Assert audit log entry created
**3.9** ⭐ Write feature test: `BackupWithAssignmentsTest::creates_backup_without_assignments`
- Create backup with checkbox disabled
- Assert `backup_items.assignments` is null
- Assert `metadata['assignment_count']` is 0 or not set
**3.10** ⭐ Write feature test: `BackupWithAssignmentsTest::handles_fetch_failure`
- Mock Graph API throwing exception
- Create backup with checkbox enabled
- Assert backup completes (fail-soft)
- Assert `metadata['assignments_fetch_failed'] = true`
- Assert warning logged
**3.11** Run Pint: Format new code
- `./vendor/bin/pint app/Services/AssignmentBackupService.php`
- `./vendor/bin/pint app/Jobs/FetchAssignmentsJob.php`
**3.12** Verify feature tests pass
- Run: `php artisan test --filter=BackupWithAssignments`
- Expected: All green
---
## Phase 4: US2 - Policy View with Assignments Tab
**Duration**: 3-4 hours
**Dependencies**: Phase 3 complete
**Parallelizable**: Yes (independent of Phase 5)
### Tasks
**4.1** ⭐ Add "Assignments" tab to Policy view
- File: `app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php`
- Use Filament `Tabs` component
- Add tab: `Tab::make('Assignments')`
- Show only for Settings Catalog policies with assignments
**4.2** ⭐ Create assignments table in tab
- Use Filament `Table` component
- Columns: Type (group/user/device), Name, Mode (Include/Exclude), ID
- Data source: `$this->record->assignments` (from BackupItem via Policy relationship)
**4.3** ⭐ Handle orphaned group IDs in table
- Check if `displayName` is null
- Render: "Unknown Group (ID: {id})" with warning icon
- Use Filament `Badge` component with color `warning`
**4.4** Create Scope Tags section
- Below assignments table
- Use Filament `Infolist` component
- Display scope tag names with IDs: "Default (ID: 0), HR-Admins (ID: abc-123)"
**4.5** Handle empty state
- Show message: "No assignments found"
- Use Filament `EmptyState` component
- Icon: `heroicon-o-user-group`
**4.6** ⭐ Write feature test: `PolicyViewAssignmentsTabTest::displays_assignments_table`
- Create policy with assignments
- Visit policy view page
- Assert "Assignments" tab visible
- Assert table contains assignment data
**4.7** ⭐ Write feature test: `PolicyViewAssignmentsTabTest::displays_orphaned_ids_with_warning`
- Create policy with orphaned group ID (no name in metadata)
- Visit assignments tab
- Assert "Unknown Group" text visible
- Assert warning badge present
**4.8** ⭐ Write feature test: `PolicyViewAssignmentsTabTest::displays_scope_tags`
- Create policy with scope tags
- Visit assignments tab
- Assert scope tag names + IDs displayed
**4.9** ⭐ Write feature test: `PolicyViewAssignmentsTabTest::shows_empty_state`
- Create policy without assignments
- Visit assignments tab
- Assert "No assignments found" message visible
**4.10** Run Pint: Format view code
- `./vendor/bin/pint app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php`
**4.11** Verify feature tests pass
- Run: `php artisan test --filter=PolicyViewAssignmentsTab`
- Expected: All green
---
## Phase 5: US3 - Restore with Group Mapping (Core Restore)
**Duration**: 6-8 hours
**Dependencies**: Phase 4 complete
**Parallelizable**: No (complex, sequential)
### Tasks
**5.1** Detect unresolved groups in restore preview
- File: `app/Filament/Resources/RestoreResource/Pages/RestorePreview.php`
- Extract group IDs from source assignments
- Call `POST /directoryObjects/getByIds` for target tenant
- Compare: missing IDs = unresolved
**5.2** Create Filament wizard component: `GroupMappingStep`
- File: `app/Filament/Forms/Components/GroupMappingStep.php`
- Add as step 2 in restore wizard (between Preview and Confirm)
- Show only if unresolved groups exist
**5.3** Build group mapping table in wizard step
- Use Filament `Repeater` or custom table
- Columns: Source Group (name or ID), Target Group (dropdown), Skip (checkbox)
- Populate source groups from backup metadata
**5.4** Implement target group dropdown
- Use `Select::make('target_group_id')`
- `->searchable()`
- `->getSearchResultsUsing()` to query target tenant groups
- `->debounce(500)` for responsive search
- `->lazy()` to load options on demand
**5.5** Add "Skip" checkbox functionality
- When checked, set mapping value to `"SKIP"`
- Disable target group dropdown when skip checked
**5.6** Cache target tenant groups
- In wizard `mount()`, call `GroupResolver->getAllForTenant($targetTenantId)`
- Cache for 5 minutes
- Pre-warm dropdown options
**5.7** Persist group mapping to `RestoreRun`
- On wizard step 2 submit, save mapping to `$restoreRun->group_mapping`
- Use `RestoreRun->addGroupMapping()` helper
- Validate: all unresolved groups either mapped or skipped
**5.8** Create service: `AssignmentRestoreService`
- File: `app/Services/AssignmentRestoreService.php`
- Method: `restore(string $policyId, array $assignments, array $groupMapping): array`
- Implement DELETE-then-CREATE pattern
**5.9** Implement DELETE existing assignments
- Step 1: GET `/assignments` for target policy
- Step 2: Loop and DELETE each assignment
- Handle 204 No Content (success)
- Log warnings on failure, continue
**5.10** Implement CREATE new assignments with mapping
- Step 3: Loop through source assignments
- Apply group mapping: replace source group IDs with target IDs
- Skip assignments marked `"SKIP"` in mapping
- POST each assignment to `/assignments`
- Handle 201 Created (success)
- Log per-assignment outcome
**5.11** Add rate limit protection
- Add 100ms delay between sequential POST calls: `usleep(100000)`
- Log request IDs for failed calls
**5.12** Handle per-assignment failures gracefully
- Don't throw on failure, collect outcomes
- Outcomes array: `['status' => 'success|failed|skipped', 'assignment' => ..., 'error' => ...]`
- Continue with remaining assignments (fail-soft)
**5.13** Store outcomes in `RestoreRun->results`
- Add `assignment_outcomes` key to results JSON
- Include successful, failed, skipped counts
- Store error details for failed assignments
**5.14** Add audit log entries
- `restore.group_mapping.applied`: When mapping saved
- `restore.assignment.created`: Per successful assignment
- `restore.assignment.failed`: Per failed assignment
- `restore.assignment.skipped`: Per skipped assignment
**5.15** Create job: `RestoreAssignmentsJob`
- File: `app/Jobs/RestoreAssignmentsJob.php`
- Dispatch async after restore initiated
- Call `AssignmentRestoreService`
- Handle job failures (log, mark RestoreRun as failed)
**5.16** Write feature test: `RestoreGroupMappingTest::detects_unresolved_groups`
- Create backup with group IDs
- Target tenant has different groups
- Start restore wizard
- Assert group mapping step appears
- Assert unresolved groups listed
**5.17** Write feature test: `RestoreGroupMappingTest::persists_group_mapping`
- Fill out group mapping form
- Submit wizard
- Assert `RestoreRun->group_mapping` populated
- Assert audit log entry created
**5.18** Write feature test: `RestoreGroupMappingTest::skips_mapped_groups`
- Map some groups, skip others
- Complete restore
- Assert skipped groups have `"SKIP"` value
- Assert skipped assignments not created
**5.19** Write feature test: `RestoreAssignmentApplicationTest::applies_assignments_successfully`
- Mock Graph API (DELETE 204, POST 201)
- Restore with mapped groups
- Assert assignments created in target tenant
- Assert outcomes logged
**5.20** Write feature test: `RestoreAssignmentApplicationTest::handles_failures_gracefully`
- Mock Graph API: some POSTs fail (400 or 500)
- Restore with mapped groups
- Assert successful assignments still created
- Assert failed assignments logged with errors
- Assert RestoreRun status reflects partial success
**5.21** Run Pint: Format restore code
- `./vendor/bin/pint app/Services/AssignmentRestoreService.php`
- `./vendor/bin/pint app/Jobs/RestoreAssignmentsJob.php`
- `./vendor/bin/pint app/Filament/Forms/Components/GroupMappingStep.php`
**5.22** Verify feature tests pass
- Run: `php artisan test --filter=RestoreGroupMapping`
- Run: `php artisan test --filter=RestoreAssignmentApplication`
- Expected: All green
---
## Phase 6: US4 - Restore Preview with Assignment Diff
**Duration**: 2-3 hours
**Dependencies**: Phase 5 complete
**Parallelizable**: Yes (extends Phase 5, doesn't block)
### Tasks
**6.1** Fetch target policy's current assignments
- In restore preview, call `AssignmentFetcher` for target policy
- Cache for duration of wizard session
**6.2** Implement diff algorithm
- Compare source assignments with target assignments
- Group by change type:
- **Added**: In source, not in target
- **Removed**: In target, not in source
- **Unchanged**: In both (match by group ID + intent)
**6.3** Display diff in Restore Preview
- Use Filament `Infolist` with color coding:
- Green (success): Added assignments
- Red (danger): Removed assignments
- Gray (secondary): Unchanged assignments
- Show counts: "3 added, 1 removed, 2 unchanged"
**6.4** Add Scope Tag diff
- Compare source scope tag IDs with target tenant scope tags
- Display: "Scope Tags: 2 matched, 1 not found in target"
- Show warning icon for missing scope tags
**6.5** Update feature test: `RestoreAssignmentApplicationTest::displays_assignment_diff`
- Visit restore preview
- Assert diff sections visible
- Assert color coding correct
- Assert counts accurate
**6.6** Run Pint: Format diff code
- `./vendor/bin/pint app/Filament/Resources/RestoreResource/Pages/RestorePreview.php`
**6.7** Verify test passes
- Run: `php artisan test --filter=displays_assignment_diff`
- Expected: Green
---
## Phase 7: Scope Tags (Full Support)
**Duration**: 2-3 hours
**Dependencies**: Phase 6 complete
**Parallelizable**: Yes (extends existing code)
### Tasks
**7.1** Extract scope tag IDs during backup
- In `AssignmentBackupService`, read `roleScopeTagIds` from policy payload
- Store in `metadata['scope_tag_ids']`
**7.2** Resolve scope tag names during backup
- Call `ScopeTagResolver->resolve($scopeTagIds)`
- Store in `metadata['scope_tag_names']`
**7.3** Validate scope tags during restore
- In `AssignmentRestoreService`, check if scope tag IDs exist in target tenant
- Call `POST /directoryObjects/getByIds` with type `['scopeTag']` (if supported) or GET all scope tags
**7.4** Log warnings for missing scope tags
- Log: "Scope Tag '{id}' not found in target tenant, restore will proceed"
- Don't block restore (Graph API handles missing scope tags gracefully)
**7.5** Update unit test: `ScopeTagResolverTest::handles_missing_scope_tags`
- Mock Graph response with some scope tags missing
- Assert warnings logged
- Assert restore proceeds
**7.6** Update feature test: `RestoreAssignmentApplicationTest::handles_missing_scope_tags`
- Restore with scope tag IDs not in target tenant
- Assert warnings logged
- Assert policy created successfully
**7.7** Run Pint: Format scope tag code
**7.8** Verify tests pass
- Run: `php artisan test --filter=ScopeTag`
- Expected: Green
---
## Phase 8: Polish & Performance
**Duration**: 3-4 hours
**Dependencies**: Phase 7 complete
**Parallelizable**: Partially (UI polish vs performance tuning)
### Tasks
**8.1** Add loading indicators to group mapping dropdown
- Use `wire:loading` on Select component
- Show spinner during search
- Disable dropdown while loading
**8.2** Add debouncing to group search
- Set `->debounce(500)` on Select component
- Test: Type quickly, verify search only fires after 500ms pause
**8.3** Optimize Graph API calls: batch group resolution
- In `GroupResolver`, batch max 100 IDs per POST
- If > 100 groups, split into chunks: `collect($groupIds)->chunk(100)`
- Merge results
**8.4** Add cache warming for target tenant groups
- In wizard `mount()`, pre-fetch all target tenant groups
- Cache for 5 minutes
- Display loading message while warming
**8.5** Add tooltips to UI elements
- Backup checkbox: "Captures group/user targeting and RBAC scope. Adds ~2-5 KB per policy."
- Group mapping: "Map source groups to target groups for cross-tenant migrations."
- Skip checkbox: "Don't restore assignments targeting this group."
**8.6** Update README: Add "Assignments & Scope Tags" section
- Overview of feature
- How to use backup checkbox
- How to use group mapping wizard
- Troubleshooting tips
**8.7** Create performance test: Large restore
- Restore 50 policies with 10 assignments each
- Measure duration
- Target: < 5 minutes
- Log: Graph API call count, cache hits
**8.8** Run performance test
- Execute test
- Record results in `quickstart.md`
- Optimize if needed (e.g., increase cache TTL)
**8.9** Run Pint: Format all code
**8.10** Final test run
- Run: `php artisan test`
- Expected: All tests green, 85%+ coverage
---
## Phase 9: Testing & QA
**Duration**: 2-3 hours
**Dependencies**: Phase 8 complete
**Parallelizable**: No (QA sequential)
### Tasks
**9.1** Manual QA: Backup with assignments (same tenant)
- Navigate to backup creation
- Enable checkbox
- Verify backup completes
- Verify assignments stored in DB
- Verify audit log entry
**9.2** Manual QA: Backup without assignments
- Navigate to backup creation
- Disable checkbox
- Verify backup completes
- Verify assignments column is null
**9.3** Manual QA: Policy view with assignments tab
- Navigate to policy with assignments
- Click "Assignments" tab
- Verify table renders
- Verify orphaned IDs show warning
- Verify scope tags section
**9.4** Manual QA: Restore to same tenant (auto-match)
- Restore backup to original tenant
- Verify no group mapping step appears
- Verify assignments restored correctly
**9.5** Manual QA: Restore to different tenant (group mapping)
- Restore backup to different tenant
- Verify group mapping step appears
- Fill out mapping (map some, skip others)
- Verify assignments restored with mapped IDs
- Verify skipped assignments not created
**9.6** Manual QA: Handle orphaned group IDs
- Create backup with group ID that doesn't exist in target
- Restore to target tenant
- Verify warning displayed
- Verify orphaned group rendered as "Unknown Group"
**9.7** Manual QA: Handle Graph API failures
- Simulate API failure (disable network or use Http::fake with 500 response)
- Attempt backup with checkbox
- Verify fail-soft behavior
- Verify warning logged
**9.8** Browser test: `GroupMappingWizardTest`
- Use Pest browser testing
- Navigate restore wizard
- Fill out group mapping
- Submit
- Verify mapping persisted
**9.9** Load testing: 100+ policies
- Create 100 policies with 20 assignments each
- Restore to target tenant
- Measure duration
- Verify < 5 minutes
**9.10** Staging deployment
- Deploy to staging via Dokploy
- Run manual QA scenarios on staging
- Verify no issues
**9.11** Document QA results
- Update `quickstart.md` with QA checklist results
- Note any issues or edge cases discovered
---
## Phase 10: Deployment & Documentation
**Duration**: 1-2 hours
**Dependencies**: Phase 9 complete
**Parallelizable**: No (sequential deployment)
### Tasks
**10.1** Review deployment checklist
- Migrations ready? ✅
- Rollback tested? ✅
- Env vars needed? (None for this feature)
- Queue workers running? ✅
**10.2** Run migrations on staging
- SSH to staging server
- Run: `php artisan migrate`
- Verify no errors
- Check backup_items and restore_runs tables
**10.3** Smoke test on staging
- Create backup with assignments
- Restore to same tenant
- Verify success
**10.4** Update spec.md with implementation notes
- Add "Implementation Status" section
- Note any deviations from original spec
- Document known limitations (e.g., Settings Catalog only in Phase 1)
**10.5** Create migration guide
- Document: "Existing backups will NOT have assignments (not retroactive)"
- Document: "Re-create backups with checkbox to capture assignments"
**10.6** Add monitoring alerts
- Alert: Assignment fetch failure rate > 10%
- Alert: Group resolution failure rate > 5%
- Use Laravel logs or external monitoring (e.g., Sentry)
**10.7** Production deployment
- Deploy to production via Dokploy
- Run migrations
- Monitor logs for 24 hours
- Check for errors
**10.8** Verify no rate limit issues
- Monitor Graph API headers: `X-RateLimit-Remaining`
- If rate limiting detected, increase delays (currently 100ms)
**10.9** Final documentation update
- Update README with feature status
- Link to quickstart.md for developers
- Add to feature list
**10.10** Close tasks in tasks.md
- Mark all tasks complete ✅
- Archive planning docs (optional)
---
## Task Summary
### MVP Scope (⭐ Tasks)
**Total**: 24 tasks
**Estimated**: 16-22 hours
**Breakdown**:
- Phase 1 (Setup): 11 tasks (2-3h)
- Phase 2 (Graph Services): 5 tasks (subset, ~2-3h)
- Phase 3 (Backup): 6 tasks (3-4h)
- Phase 4 (Policy View): 6 tasks (3-4h)
- Phase 5 (Restore, basic): Partial (subset for same-tenant restore)
**MVP Scope Definition**:
- ✅ Backup with assignments checkbox (US1)
- ✅ Policy view with assignments tab (US2)
- ✅ Basic restore (same tenant, auto-match) (US3 partial)
- ❌ Group mapping wizard (defer to post-MVP)
- ❌ Restore preview diff (defer to post-MVP)
- ❌ Scope tags full support (defer to post-MVP)
---
### Full Implementation
**Total**: 62 tasks
**Estimated**: 30-40 hours
**Breakdown**:
- Phase 1: 13 tasks (2-3h)
- Phase 2: 16 tasks (4-6h)
- Phase 3: 12 tasks (4-5h)
- Phase 4: 11 tasks (3-4h)
- Phase 5: 22 tasks (6-8h)
- Phase 6: 7 tasks (2-3h)
- Phase 7: 8 tasks (2-3h)
- Phase 8: 10 tasks (3-4h)
- Phase 9: 11 tasks (2-3h)
- Phase 10: 10 tasks (1-2h)
---
## Parallel Development Opportunities
### Track 1: Backend Services (Dev A)
- Phase 1: Database setup (13 tasks)
- Phase 2: Graph API services (16 tasks)
- Phase 3: Backup service (12 tasks)
- Phase 5: Restore service (22 tasks)
**Total**: ~16-22 hours
---
### Track 2: Frontend/UI (Dev B)
- Phase 4: Policy view tab (11 tasks)
- Phase 5: Group mapping wizard (subset of Phase 5, ~10 tasks)
- Phase 6: Restore preview diff (7 tasks)
- Phase 8: UI polish (subset, ~5 tasks)
**Total**: ~12-16 hours
---
### Track 3: Testing & QA (Dev C or shared)
- Phase 2: Unit tests for services (subset)
- Phase 3: Feature tests for backup (subset)
- Phase 4: Feature tests for policy view (subset)
- Phase 9: Manual QA + browser tests (11 tasks)
**Total**: ~8-12 hours
**Note**: Testing can be done incrementally as phases complete.
---
## Dependencies Graph
```
Phase 1 (Setup)
Phase 2 (Graph Services)
Phase 3 (Backup) → Phase 4 (Policy View)
↓ ↓
Phase 5 (Restore) ←------+
Phase 6 (Preview Diff)
Phase 7 (Scope Tags)
Phase 8 (Polish)
Phase 9 (Testing & QA)
Phase 10 (Deployment)
```
**Parallel Phases**: Phase 3 and Phase 4 can run in parallel after Phase 2.
---
## Risk Mitigation Tasks
### Risk: Graph API Rate Limiting
- **Task 2.11**: Add retry logic with exponential backoff
- **Task 5.11**: Add 100ms delay between POSTs
- **Task 8.3**: Batch group resolution (max 100 per request)
### Risk: Large Group Counts (500+)
- **Task 5.4**: Implement searchable dropdown with debounce
- **Task 5.6**: Cache target tenant groups
- **Task 8.4**: Pre-warm cache on wizard mount
### Risk: Assignment Restore Failures
- **Task 5.12**: Fail-soft per-assignment error handling
- **Task 5.13**: Store outcomes in RestoreRun results
- **Task 5.14**: Audit log all outcomes
---
## Next Actions
1. **Review**: Team review of tasks.md, adjust estimates if needed
2. **Assign**: Assign tasks to developers (Track 1/2/3)
3. **Start**: Begin with Phase 1 (Setup & Database)
4. **Track Progress**: Update task status as work progresses
5. **Iterate**: Adjust plan based on discoveries during implementation
---
**Status**: Task Breakdown Complete
**Ready for Implementation**: ✅
**Estimated Duration**: 30-40 hours (MVP: 16-22 hours)