TenantAtlas/specs/185-settings-catalog-readable/plan.md
2025-12-14 20:23:18 +01:00

415 lines
13 KiB
Markdown

# Feature 185: Implementation Plan
## Tech Stack
- **Backend**: Laravel 12, PHP 8.4
- **Database**: PostgreSQL (JSONB for raw definition storage)
- **Frontend**: Filament 4, Livewire 3, Tailwind CSS
- **Graph Client**: Existing `GraphClientInterface`
- **JSON Viewer**: `pepperfm/filament-json` (installed)
## Architecture Overview
### Services Layer
```
app/Services/Intune/
├── SettingsCatalogDefinitionResolver.php (NEW)
├── PolicyNormalizer.php (EXTEND)
└── PolicySnapshotService.php (EXTEND)
```
### Database Layer
```
database/migrations/
└── xxxx_create_settings_catalog_definitions_table.php (NEW)
app/Models/
└── SettingsCatalogDefinition.php (NEW)
```
### UI Layer
```
app/Filament/Resources/
├── PolicyResource.php (EXTEND - infolist with tabs)
└── PolicyVersionResource.php (FUTURE - optional)
resources/views/filament/infolists/entries/
└── settings-catalog-grouped.blade.php (NEW - accordion view)
```
## Component Responsibilities
### 1. SettingsCatalogDefinitionResolver
**Purpose**: Fetch and cache setting definitions from Graph API
**Key Methods**:
- `resolve(array $definitionIds): array` - Batch resolve definitions
- `resolveOne(string $definitionId): ?array` - Single definition lookup
- `warmCache(array $definitionIds): void` - Pre-populate cache
- `clearCache(?string $definitionId = null): void` - Cache invalidation
**Dependencies**:
- `GraphClientInterface` - Graph API calls
- `SettingsCatalogDefinition` model - Database cache
- Laravel Cache - Memory-level cache
**Caching Strategy**:
1. Check memory cache (request-level)
2. Check database cache (30-day TTL)
3. Fetch from Graph API
4. Store in DB + memory
**Graph Endpoints**:
- `/deviceManagement/configurationSettings` (global catalog)
- `/deviceManagement/configurationPolicies/{id}/settings/{settingId}/settingDefinitions` (policy-specific)
### 2. PolicyNormalizer (Extension)
**Purpose**: Transform Settings Catalog snapshot into UI-ready structure
**New Method**: `normalizeSettingsCatalog(array $snapshot, array $definitions): array`
**Output Structure**:
```php
[
'type' => 'settings_catalog',
'groups' => [
[
'title' => 'Windows Hello for Business',
'description' => 'Configure biometric authentication settings',
'settings' => [
[
'label' => 'Use biometrics',
'value_display' => 'Enabled',
'value_raw' => true,
'help_text' => 'Allow users to sign in with fingerprint...',
'definition_id' => 'device_vendor_msft_passportforwork_biometrics_usebiometrics',
'instance_type' => 'ChoiceSettingInstance'
]
]
]
]
]
```
**Value Formatting Rules**:
- `ChoiceSettingInstance`: Extract choice label from `@odata.type` or value
- `SimpleSetting` (bool): "Enabled" / "Disabled"
- `SimpleSetting` (int): Number formatted with separators
- `SimpleSetting` (string): Truncate >100 chars, add "..."
- `GroupSettingCollectionInstance`: Flatten children recursively
**Grouping Strategy**:
- Group by `categoryId` from definition metadata
- Fallback: Group by first segment of definition ID (e.g., `device_vendor_msft_`)
- Sort groups alphabetically
### 3. PolicySnapshotService (Extension)
**Purpose**: Enrich snapshots with definition metadata after hydration
**Modified Flow**:
```
1. Hydrate settings from Graph (existing)
2. Extract all settingDefinitionId + children (NEW)
3. Call SettingsCatalogDefinitionResolver::warmCache() (NEW)
4. Add metadata to snapshot: definitions_cached, definition_count (NEW)
5. Save snapshot (existing)
```
**Non-Blocking**: Definition resolution should not block policy sync
- Use try/catch for Graph API calls
- Mark `definitions_cached: false` on failure
- Continue with snapshot save
### 4. PolicyResource (UI Extension)
**Purpose**: Render Settings Catalog policies with readable UI
**Changes**:
1. Add Tabs component to infolist:
- "Settings" tab (default)
- "JSON" tab (existing Feature 002 implementation)
2. Settings Tab Structure:
- Search/filter input (top)
- Accordion component (groups)
- Each group: Section with settings table
- Fallback: Show info message if no definitions cached
3. JSON Tab:
- Existing implementation from Feature 002
- Shows full snapshot with copy button
**Conditional Rendering**:
- Show tabs ONLY for `settingsCatalogPolicy` type
- For other policy types: Keep existing simple sections
## Database Schema
### Table: `settings_catalog_definitions`
```sql
CREATE TABLE settings_catalog_definitions (
id BIGSERIAL PRIMARY KEY,
definition_id VARCHAR(500) UNIQUE NOT NULL,
display_name VARCHAR(255) NOT NULL,
description TEXT,
help_text TEXT,
category_id VARCHAR(255),
ux_behavior VARCHAR(100),
raw JSONB NOT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE INDEX idx_definition_id ON settings_catalog_definitions(definition_id);
CREATE INDEX idx_category_id ON settings_catalog_definitions(category_id);
CREATE INDEX idx_raw_gin ON settings_catalog_definitions USING GIN(raw);
```
**Indexes**:
- `definition_id` - Primary lookup key
- `category_id` - Grouping queries
- `raw` (GIN) - JSONB queries if needed
## Graph API Integration
### Endpoints Used
1. **Global Catalog** (Preferred):
```
GET /deviceManagement/configurationSettings
GET /deviceManagement/configurationSettings/{settingDefinitionId}
```
2. **Policy-Specific** (Fallback):
```
GET /deviceManagement/configurationPolicies/{policyId}/settings/{settingId}/settingDefinitions
```
### Request Optimization
- Batch requests where possible
- Use `$select` to limit fields
- Use `$filter` for targeted lookups
- Respect rate limits (429 retry logic)
## UI/UX Flow
### Policy View Page Flow
1. User navigates to `/admin/policies/{id}`
2. Policy details loaded (existing)
3. Check policy type:
- If `settingsCatalogPolicy`: Show tabs
- Else: Show existing sections
4. Default to "Settings" tab
5. Load normalized settings from PolicyNormalizer
6. Render accordion with groups
7. User can search/filter settings
8. User can switch to "JSON" tab for raw view
### Settings Tab Layout
```
┌─────────────────────────────────────────────┐
│ [Search settings...] [🔍] │
├─────────────────────────────────────────────┤
│ ▼ Windows Hello for Business │
│ ├─ Use biometrics: Enabled │
│ ├─ Use facial recognition: Disabled │
│ └─ PIN minimum length: 6 │
├─────────────────────────────────────────────┤
│ ▼ Device Lock Settings │
│ ├─ Password expiration days: 90 │
│ └─ Password history: 5 │
└─────────────────────────────────────────────┘
```
### JSON Tab Layout
```
┌─────────────────────────────────────────────┐
│ Full Policy Configuration [Copy] │
├─────────────────────────────────────────────┤
│ { │
│ "@odata.type": "...", │
│ "name": "WHFB Settings", │
│ "settings": [...] │
│ } │
└─────────────────────────────────────────────┘
```
## Error Handling
### Definition Not Found
- **UI**: Show prettified definition ID
- **Label**: Convert `device_vendor_msft_policy_name` → "Device Vendor Msft Policy Name"
- **Icon**: Info icon with tooltip "Definition not cached"
### Graph API Failure
- **During Sync**: Mark `definitions_cached: false`, continue
- **During View**: Show cached data or fallback labels
- **Log**: Record Graph API errors for debugging
### Malformed Snapshot
- **Validation**: Check for required fields before normalization
- **Fallback**: Show raw JSON tab, hide Settings tab
- **Warning**: Display admin-friendly error message
## Performance Considerations
### Database Queries
- Eager load definitions for all settings in one query
- Use `whereIn()` for batch lookups
- Index on `definition_id` ensures fast lookups
### Memory Management
- Request-level cache using Laravel Cache
- Limit batch size to 100 definitions per request
- Clear memory cache after request
### UI Rendering
- Accordion lazy-loads groups (only render expanded)
- Pagination for policies with >50 groups
- Virtualized list for very large policies (future)
### Caching TTL
- Database: 30 days (definitions change rarely)
- Memory: Request duration only
- Background refresh: Optional scheduled job
## Security Considerations
### Graph API Permissions
- Existing `DeviceManagementConfiguration.Read.All` sufficient
- No new permissions required
### Data Sanitization
- Escape HTML in display names and descriptions
- Validate definition ID format before lookups
- Prevent XSS in value rendering
### Audit Logging
- Log definition cache misses
- Log Graph API failures
- Track definition cache updates
## Testing Strategy
### Unit Tests
**File**: `tests/Unit/SettingsCatalogDefinitionResolverTest.php`
- Test batch resolution
- Test caching behavior (memory + DB)
- Test fallback when definition not found
- Test Graph API error handling
**File**: `tests/Unit/PolicyNormalizerSettingsCatalogTest.php`
- Test grouping logic
- Test value formatting (bool, int, choice, string)
- Test fallback labels
- Test nested group flattening
### Feature Tests
**File**: `tests/Feature/Filament/PolicyViewSettingsCatalogReadableTest.php`
- Mock Graph API responses
- Assert tabs present for Settings Catalog policies
- Assert display names shown (not definition IDs)
- Assert values formatted correctly
- Assert search/filter works
- Assert JSON tab accessible
- Assert graceful degradation for missing definitions
### Manual QA Checklist
1. Open Policy View for Settings Catalog policy
2. Verify tabs present: "Settings" and "JSON"
3. Verify Settings tab shows groups with accordion
4. Verify display names shown (not raw IDs)
5. Verify values formatted (True/False, numbers, etc.)
6. Test search: Type setting name, verify filtering
7. Switch to JSON tab, verify snapshot shown
8. Test copy button in JSON tab
9. Test dark mode toggle
10. Test with policy missing definitions (fallback labels)
## Deployment Steps
### 1. Database Migration
```bash
./vendor/bin/sail artisan migrate
```
### 2. Cache Warming (Optional)
```bash
./vendor/bin/sail artisan tinker
>>> $resolver = app(\App\Services\Intune\SettingsCatalogDefinitionResolver::class);
>>> $resolver->warmCache([...definitionIds...]);
```
### 3. Clear Caches
```bash
./vendor/bin/sail artisan optimize:clear
```
### 4. Verify
- Navigate to Policy View
- Check browser console for errors
- Check Laravel logs for Graph API errors
## Rollback Plan
### If Critical Issues Found
1. Revert database migration:
```bash
./vendor/bin/sail artisan migrate:rollback
```
2. Revert code changes (Git):
```bash
git revert <commit-hash>
```
3. Clear caches:
```bash
./vendor/bin/sail artisan optimize:clear
```
### Partial Rollback
- Remove tabs, keep existing table view
- Disable definition resolver, show raw IDs
- Keep database table for future use
## Dependencies on Feature 002
**Shared**:
- `pepperfm/filament-json` package (installed)
- JSON viewer CSS assets (published)
- Tab component pattern (Filament Schemas)
**Independent**:
- Feature 185 can work without Feature 002 completed
- Feature 002 provided JSON tab foundation
- Feature 185 adds Settings tab with readable UI
## Timeline Estimate
- **Phase 1** (Foundation): 2-3 hours
- **Phase 2** (Snapshot): 1 hour
- **Phase 3** (Normalizer): 2-3 hours
- **Phase 4** (UI): 3-4 hours
- **Phase 5** (Testing): 2-3 hours
- **Total**: ~11-15 hours
## Success Metrics
1. **User Experience**:
- Admins can read policy settings without raw JSON
- Search finds settings in <200ms
- Accordion groups reduce scrolling
2. **Performance**:
- Definition resolution: <500ms for 50 definitions
- UI render: <2s for 200 settings
- Search response: <200ms
3. **Quality**:
- 100% test coverage for resolver
- Zero broken layouts for missing definitions
- Zero Graph API errors logged (with proper retry)
4. **Adoption**:
- Settings tab used >80% of time vs JSON tab
- Zero support tickets about "unreadable settings"