- Add assignments section to PolicyResource ViewPolicy page
- Display assignment count, scope tags, and assignment details
- Show orphaned groups with warning emoji
- Support markdown rendering for assignment lists
- Phase 3 complete: all backup with assignments features done
- BackupWithAssignmentsTest: Added missing PolicySnapshotService mock for orphaned groups test
- BackupCreationTest: Added PolicySnapshotService mock with twice() expectation, added last_synced_at to test policies, fixed payload assertion (id instead of policyId)
- PolicyListingTest: Added last_synced_at to test policies
Root cause: Policies without last_synced_at within 7 days are filtered out by BackupItemsRelationManager (Feature 005 workaround for deleted policies)
All tests now passing: 155 passed ✅, 5 skipped
- Test resolves scope tag IDs to objects with id and displayName
- Test caching behavior (1 hour TTL)
- Test empty input handling
- Test 403 Forbidden error handling gracefully
- Test filtering to requested IDs preserving array keys
- Updated BackupWithAssignmentsTest ScopeTagResolver mocks to match actual method signature
- Fixed TenantSetupTest to expect 'granted' instead of 'ok' status
- Added ScopeTagResolver mock to BackupCreationTest
All Phase 3 (Scope Tags) functionality complete and tested.
Moved DeviceManagementRBAC.Read.All and Group.Read.All from
'required' to 'granted' section after adding them in Azure AD.
These permissions are now active and will resolve:
- Scope tag IDs to display names
- Group IDs to group names for assignments
Next step: Create new backup to verify scope tag name resolution works.
Improved visual clarity for all status fields:
- Tenant status: active=green, inactive=gray, suspended=warning, error=red
- App status: ok/configured=green, pending=warning, error=red, requires_consent=warning
- RBAC status: ok/configured=green, manual_assignment_required=warning, error/failed=red
- Permission status: granted=green, missing=orange, error=red (from previous commit)
- Canaries: ok=green badge, error=red badge, pending=yellow badge
All status badges now use consistent color coding across the application
for better UX and faster status recognition.
Changes:
- Status labels: 'ok' → 'granted' (clearer meaning)
- Badge colors: granted=green, missing=orange, error=red
- Updated tests to match new status values
This makes the permission status more intuitive and visually
distinguishable on the Tenant detail page (/admin/tenants/1).
Added two new required permissions for Feature 004:
- DeviceManagementRBAC.Read.All: Resolve scope tag IDs to names
- Group.Read.All: Resolve group IDs for assignments
These permissions will be displayed on the Tenant detail page
(/admin/tenants/1) as 'missing' until added in Azure AD.
Steps to complete setup:
1. Add permissions in Azure AD App Registration
2. Grant admin consent
3. Move permissions from 'Required' to 'Tatsächlich granted' in this config
4. Clear cache: php artisan cache:clear
5. Verify on Tenant detail page
- Created docs/PERMISSIONS.md with complete permission requirements
- Added logging for 403 errors in ScopeTagResolver
- Updated README with link to permissions documentation
Issue: Scope tags show 'Unknown (ID: 0)' due to missing permission
Required: DeviceManagementRBAC.Read.All with admin consent
User must:
1. Go to Azure Portal → App Registration
2. Add DeviceManagementRBAC.Read.All permission
3. Grant admin consent
4. Wait 5-10 min for propagation
5. Clear cache: php artisan cache:clear
Prevents selection of policies that haven't been synced in the last 7 days
(likely deleted in Intune). Aligns with PolicyResource table filter.
This is a workaround until Feature 005 (Policy Lifecycle) is implemented
with proper soft delete detection during sync.
Fixes bug where removed backup items could not be re-added via UI.
🐛 Problem:
- When a BackupItem is soft-deleted (removed from BackupSet), it disappears from UI
- User tries to re-add the same policy → receives 'added successfully' notification
- Policy doesn't actually get added → BackupService filtered it out as already existing
- Confusing UX: notification says success but nothing changes
🔍 Root Cause:
BackupService checked for existence of policies including soft-deleted ones:
$existingPolicyIds = $backupSet->items()->withTrashed()->pluck('policy_id')
$policyIds = array_diff($policyIds, $existingPolicyIds) // ❌ Filters out soft-deleted
This prevented re-adding policies that were previously removed.
✅ Solution:
When a policy is re-added that already exists as soft-deleted:
1. Restore the soft-deleted BackupItem instead of ignoring it
2. Only create new items for truly new policies
3. Show restored policies in the UI dropdown (removed withTrashed() from RelationManager)
📝 Changes:
- BackupService::addPoliciesToSet():
* Separate soft-deleted items from new policies
* Restore soft-deleted items automatically
* Track restored_count in audit logs
- BackupItemsRelationManager: Removed withTrashed() so soft-deleted items appear in dropdown again
- BackupItemReaddTest: Updated to expect restore behavior instead of ignore
✅ Tests: 3 passed (11 assertions)
Impact:
- ✅ Removed policies can now be re-added via UI
- ✅ Restores existing backup data instead of creating duplicates
- ✅ Proper audit trail with restored_count
- 62 tasks across 10 phases
- MVP scope: 24 tasks (16-22 hours)
- Full implementation: 62 tasks (30-40 hours)
- Organized by user story (US1-US4)
- Parallel development tracks defined
- Risk mitigation tasks included
Changes:
- Replace POST /assign with standard CRUD operations
- Restore strategy: DELETE existing + POST new assignments
- Graph Contract: assignments_create/update/delete_path + methods
- Handle 201 Created (POST) and 204 No Content (DELETE)
- Fail-soft: continue if individual assignment fails
Based on: Microsoft Learn Graph API docs + real-world usage patterns