diff --git a/specs/004-assignments-scope-tags/tasks.md b/specs/004-assignments-scope-tags/tasks.md new file mode 100644 index 0000000..a892b4c --- /dev/null +++ b/specs/004-assignments-scope-tags/tasks.md @@ -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)