TenantAtlas/specs/001-rbac-onboarding/tasks.md
2025-12-14 19:56:02 +01:00

938 lines
52 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
description: "Task list for TenantPilot v1 implementation"
---
# Tasks: TenantPilot v1
**Input**: Design documents from `.specify/spec.md` and `.specify/plan.md`
**Prerequisites**: plan.md (complete), spec.md (complete)
---
## Constitution Evidence Ledger (Discovery + Verification)
> This section is the canonical evidence record to satisfy Constitution VII (Spec-Driven Development) and IV (Auditability).
> Each completed phase has: (a) discovery notes, (b) verification commands, (c) where to look in repo/UX.
### Evidence: Phases 16 (US1US4 core)
- **Discovery:** Verified existing Filament resources and services implement tenant scoping and Graph abstraction; restore flow includes preview + confirmation; versions stored immutable JSONB; audits written for critical operations.
- **Verification:**
- `./vendor/bin/pest tests/Feature/Filament/PolicyListingTest.php`
- `./vendor/bin/pest tests/Feature/Filament/BackupCreationTest.php`
- `./vendor/bin/pest tests/Feature/Filament/RestorePreviewTest.php tests/Feature/Filament/RestoreExecutionTest.php`
- `./vendor/bin/pest tests/Feature/Filament/PolicyVersionTest.php tests/Unit/VersionDiffTest.php`
- `./vendor/bin/pint --dirty`
- **Manual checks:** Filament UI: Policies list/filter, BackupSet detail + items, RestoreRun preview/execution, PolicyVersion view/diff.
### Evidence: Phase 13 (US1b settings display + safety gates)
- **Discovery:** Normalized settings display added; malformed snapshot warnings; @odata.type mismatch gates block restore execution.
- **Verification:**
- `./vendor/bin/pest tests/Unit/PolicyNormalizerTest.php`
- `./vendor/bin/pest tests/Feature/Filament/PolicySettingsDisplayTest.php`
- `./vendor/bin/pest tests/Feature/Filament/PolicyVersionSettingsTest.php`
- `./vendor/bin/pint --dirty`
### Evidence: Phase 14 (US7 RBAC wizard)
- **Discovery:** RBAC wizard stack present (TenantResource action, delegated auth controller, onboarding service, health panel, migrations, tests).
- **Verification:**
- `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`
- `./vendor/bin/pest tests/Unit/RbacOnboardingServiceTest.php`
- `./vendor/bin/pint --dirty`
### Evidence: Phase 15 (US8 Graph Contract Registry & Drift Guard)
- **Discovery:** Contract registry + fallback integrated in Graph client; drift-check command added; type-family tolerant @odata validation added.
- **Verification:**
- `./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php tests/Unit/GraphContractFallbackTest.php`
- `./vendor/bin/pest tests/Feature/Filament/ODataTypeMismatchTest.php`
- `./vendor/bin/pint --dirty`
### Evidence: Settings Catalog (settingsCatalogPolicy) extensions
- **Discovery:** Added first-class sync/type + restore hardening + hydration + normalized display improvements.
- **Verification:**
- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicySyncTest.php`
- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogRestoreTest.php`
- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php`
- `./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogTest.php`
- `./vendor/bin/pint --dirty`
---
## FR → Tasks Traceability Matrix (Explicit)
> This matrix makes FR coverage explicit (tooling/audits). Tasks also carry local Implements: tags where most useful.
- **FR-001 (Inventory)** → T008T011
- **FR-002 (Backups)** → T012T015, T131T132
- **FR-003 (Auditability baseline)** → T006, T015, T020, T025
- **FR-004 (Versions)** → T016T020
- **FR-005 (Diffs)** → T017, T019
- **FR-006FR-010 (Restore safety + preview + gating)** → T021T026, T144, T151
- **FR-011FR-018 (Tenant-aware + Graph abstraction + governance basics)** → T003T007, T035, T120T125
- **FR-019.1FR-019.2 (Settings normalization + edge cases)** → T140T153
- **FR-023FR-030 (RBAC onboarding wizard)** → T160T169
- **FR-031FR-034 (Contract registry + drift guard)** → T170T175
- **FR-035 (Rerun restore)** → T156
---
# Tasks: TenantPilot v1
## Measurable Thresholds (NFR/UX)
These thresholds make qualitative terms measurable and testable.
### Payload / Rendering Limits
- **Settings table max rows:** 1000 rows per rendered table block (truncate with notice).
- **Flatten recursion depth:** max depth 8; if exceeded, stop and warn.
- **Max value length:** 500 characters rendered inline; provide copy/full view for longer values.
- **Max JSON pretty print:** 1 MB rendered inline; above that show “download/copy only”.
### Graph Request Limits
- **Default Graph request timeout:** 30s per request.
- **Hydration pagination limit:** max 50 pages per subresource; if exceeded → warning + partial snapshot.
### Restore Safety
- **Dry-run is binary:** a restore run is either dry-run or execute; no “default dry-run=true” semantics.
- **Type mismatch gate:** `@odata.type` mismatch MUST block execution (preview may show).
### Retention / Housekeeping
- **Soft-deleted entities:** retained indefinitely unless explicitly force-deleted.
- **Audit logs:** retained indefinitely by default (configurable later).
### “Large payload” definition
- Any snapshot JSONB > 1 MB OR settings table > 1000 rows is considered **large** and triggers truncation rules above.
### FR-019 Settings Normalization & Display
FR-019.1 **Normalized Settings View**
- Admin can view a policy and policy version with settings rendered in a readable normalized format.
- The normalized output MUST hide Graph metadata keys unless explicitly requested.
FR-019.2 **Raw Snapshot + Copy**
- Admin can view raw JSON snapshot (pretty-printed where possible) and copy it.
FR-019.3 **Edge Handling**
- Malformed snapshots MUST show a warning banner and attempt partial rendering.
- `@odata.type` mismatch MUST show a warning; restore execution MUST be blocked.
FR-019.4 **Thresholds**
- Rendering and snapshot size limits MUST follow “Measurable Thresholds (NFR/UX)”.
- [x] T023 [US4] Implement restore service with preview/dry-run and selective item application in `app/Services/Intune/RestoreService.php`, integrating Graph adapter and conflict detection.
- Implements: FR-006, FR-008, FR-009
- Implements: FR-021
- Implements: FR-020
- Verified by: `./vendor/bin/pest tests/Feature/Filament/RestorePreviewTest.php tests/Feature/Filament/RestoreExecutionTest.php`
- [x] T145 [US1b] Create PolicyNormalizer service in `app/Services/Intune/PolicyNormalizer.php`.
- Implements: FR-019.1, FR-019.3
- Verified by: `./vendor/bin/pest tests/Unit/PolicyNormalizerTest.php`
- [x] T160 [US7] Add TenantResource ActionGroup entry “Setup Intune RBAC” …
- Implements: FR-023, FR-024
- Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`
- [x] T170 [US8] Add contract registry artifact …
- Implements: FR-031
- Verified by: `./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php` (See <attachments> above for file contents. You may not need to search or read the file again.)
## Phase 1: Setup (Shared Infrastructure)
- [x] T001 [P] [Shared] Confirm Sail/Env ready; ensure `.env` has PostgreSQL settings for Sail and Filament admin user seeded (if missing) in `database/seeders/`.
- [x] T002 [P] [Shared] Add baseline docs for local dev and staging promotion notes in `README.md` (Sail commands, staging-before-prod reminder).
## Phase 2: Foundational (Blocking Prerequisites)
- [x] T003 [Shared] Add tenant-aware migrations for `tenants`, `policies`, `policy_versions`, `backup_sets`, `backup_items`, `restore_runs`, `audit_logs` with JSONB payloads and FK/time indexes in `database/migrations/`.
- Implements: FR-013
- [x] T004 [Shared] Create models with relationships and guarded attributes for the above entities in `app/Models/`.
- [x] T005 [Shared] Implement Graph abstraction contracts (`GraphClientInterface`, error mapping, logging hooks) in `app/Services/Graph/` with a mockable adapter.
- [x] T006 [Shared] Add audit logging service/helper to capture actor, tenant, operation, resources, outcome in `app/Services/Intune/AuditLogger.php`.
- [x] T007 [Shared] Seed supported policy types/metadata for initial scope in `database/seeders/PoliciesSeeder.php` and ensure tenant scoping.
## Phase 3: User Story 1 - Policy inventory listing (Priority: P1)
### Tests for User Story 1
- [x] T008 [P] [US1] Feature test for Filament policy listing and filtering (tenant-scoped) in `tests/Feature/Filament/PolicyListingTest.php` using mocked Graph sync.
- [x] T176 [Scope][US1] Add Settings Catalog Policies as first-class type (`settingsCatalogPolicy`)
- **Goal**: Intune **Settings Catalog Policies** werden als **eigener Typ** synchronisiert, angezeigt und sind für Backup/Version/Diff/Preview/Restore (gemäß Matrix) korrekt routbar.
- **Why**: Settings Catalog Policies liegen in Graph unter `deviceManagement/configurationPolicies` (nicht unter `deviceManagement/deviceConfigurations`). Aktuell erscheinen sie daher nicht (oder nur unvollständig).
## Implementation
1) **Config: supported_types erweitern (Single Source of Truth)**
- In `config/tenantpilot.php` (oder eurem zentralen Type-Registry-File) neuen Typ hinzufügen:
- `key`: `settingsCatalogPolicy`
- `name`: `Settings Catalog Policy`
- `graph_resource`: `deviceManagement/configurationPolicies`
- `category`: `Configuration`
- `platform`: `windows` *(oder `all` + später per snapshot/@odata ableiten je nach eurer Modelllogik)*
- UI-Label so wählen, dass Admin sofort erkennt: **“Settings Catalog”** (z. B. Badge/Label).
2) **Restore-Matrix erweitern**
- In eurer Restore-Konfig (`scope.restore_matrix` bzw. config-driven Matrix):
- `settingsCatalogPolicy: backup: full, restore: enabled, risk: medium` *(optional `medium-high` falls ihr strenger sein wollt)*
- Restore-Warnungen/Badges müssen den neuen Typ korrekt anzeigen.
3) **Graph Contract Registry erweitern**
- In `config/graph_contracts.php` Contract für `settingsCatalogPolicy` hinzufügen:
- Resource paths (collection + single item)
- `allowed_select`/`allowed_expand` (konservativ starten)
- `type_family` / erlaubte `@odata.type` Werte für diesen Typ
- Create/Update routing (`POST`/`PATCH` wie bei euren anderen Typen)
- Sicherstellen, dass **capability fallback** (downgrade ohne `$select/$expand`) auch hier greift.
4) **PolicySyncService erweitern**
- Sync-Pipeline muss zusätzlich `deviceManagement/configurationPolicies` abfragen und upserten:
- `policies.type_key = settingsCatalogPolicy`
- `external_id = Graph id`
- `display_name`, `description`, `last_modified`, etc.
- Tenant-scoping beibehalten.
- **No duplicates**: gleiche `external_id` darf nicht in zwei Typen landen (Unique/Guard prüfen).
5) **Snapshots / Settings availability**
- Für die Spalte/Badge **“Settings”** (Available/Missing):
- Snapshot-Fetch muss für `settingsCatalogPolicy` über den neuen Endpoint laufen (single item fetch).
- Normalizer/Validator:
- `@odata.type` muss für diesen Typ als kompatibel erkannt werden (über Contract/type-family).
6) **UI (Filament)**
- `PolicyResource`:
- Type/Category Filter um `Settings Catalog Policy` erweitern
- Optional: Category bleibt `Configuration`, aber Typ klar `Settings Catalog`
- Detailseite:
- Normalized Settings anzeigen (wenn euer Normalizer Settings Catalog schon kann)
- sonst mind.: **Raw JSON + Hinweis** “Settings Catalog normalization pending” (kein silent fail).
7) **Permissions/Health**
- Verify/Permissions-Liste prüfen, ob für `deviceManagement/configurationPolicies` zusätzliche Graph-Permissions nötig sind.
- Falls ja:
- `config/intune_permissions.php` ergänzen
- Health Panel zeigt fehlende Permission sauber an.
## Tests (Pest)
- **Unit**:
- Contract Registry erkennt `settingsCatalogPolicy`
- type-family ok (derived `@odata.type` accepted)
- fallback ok (capability downgrade)
- **Feature**:
- Policy Sync importiert `configurationPolicies` als `settingsCatalogPolicy` und listet sie in der UI
- Settings badge wird **Available**, sobald Snapshot vorhanden ist
- **Regression**:
- `deviceConfiguration` Sync bleibt unverändert (keine Vermischung)
## Verification
- `./vendor/bin/pest tests/Feature/Filament/PolicyListingTest.php`
- ggf. neue Tests:
- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicySyncTest.php`
- Registry Tests erweitern:
- `./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php`
## Acceptance Criteria
- In der Policies-Liste erscheinen Intune **Settings Catalog Policies** als eigener Typ **Settings Catalog Policy**.
- Admin kann danach **Backup/Version/Preview/Restore** (gemäß Matrix) für diesen Typ nutzen.
- **Keine Duplikate/Überlappung** mit `deviceConfiguration`.
- [x] T177 [US4][Bugfix] Settings Catalog Restore: Graph-Fehlerdetails speichern + PATCH-Payload sanitizen (contract-driven)
- **Goal**: Restore von `settingsCatalogPolicy` soll nicht mehr als generisches `400 Graph apply failed` enden, sondern:
1) echte Graph-Fehlerdetails persistieren + im UI sichtbar machen
2) beim PATCH nur ein zulässiges Payload senden (read-only/meta Felder raus, whitelist/contract-driven)
- **Why**: `deviceManagement/configurationPolicies` akzeptiert beim PATCH i. d. R. keinen vollständigen Snapshot → read-only Felder führen zu 400.
**Implementation**
1) **RestoreRun Results verbessern (Fehlerdetails persistieren)**
- In `RestoreService` (oder zentralem Graph-Apply Catch):
- Bei Graph-Exception zusätzlich in `restore_run_item_results`/`results` JSON speichern:
- `graph_error_code`
- `graph_error_message`
- optional (falls vorhanden): `graph_request_id`, `graph_client_request_id`, `graph_date`
- UI (RestoreRun Detail) soll bei failed Items neben `code/reason` auch `graph_error_message` anzeigen (kurz) + “Details” (expand/collapsible) für request ids.
2) **Contract Registry: update sanitizer für settingsCatalogPolicy**
- In `config/graph_contracts.php` bei `settingsCatalogPolicy` ergänzen:
- entweder `update_whitelist` (preferred) **oder** `update_strip_keys`
- `update_whitelist` konservativ starten (nur Felder, die PATCH typischerweise akzeptiert), z. B.:
- `name`, `description`, `settings`, `technologies`, `platforms`, `roleScopeTagIds`
- `assignments` **nur** wenn Restore wirklich Assignments patcht (sonst weglassen)
- In `GraphContractRegistry` (oder äquivalent) Methode bereitstellen:
- `sanitizeUpdatePayload(string $typeKey, array $snapshot): array`
- Entfernt immer: `id`, `createdDateTime`, `lastModifiedDateTime`, `@odata.*`, `version`, `roleScopeTagIds@odata.*`, sowie unbekannte Keys
- In `RestoreService` beim UPDATE/PATCH:
- für `settingsCatalogPolicy` vor dem Graph PATCH immer `sanitizeUpdatePayload()` verwenden.
3) **Graph apply: bessere Diagnose im Audit**
- Audit-Event (z. B. `restore.item.failed`) soll zusätzlich `graph_error_code` + `graph_request_id` enthalten (keine Tokens/payloads).
**Tests (Pest)**
- Unit: `GraphContractRegistry` sanitizer
- Given snapshot mit read-only/meta Feldern → sanitized payload enthält nur whitelist
- Feature: Restore execution für settingsCatalogPolicy mit “bad payload”
- Mock Graph 400 mit error body → RestoreRun result speichert `graph_error_message` + IDs
- UI assertion: Fehlermeldung sichtbar (kurz) + Details optional
**Verification**
- `./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php`
- `./vendor/bin/pest tests/Feature/Filament/RestoreExecutionTest.php` (oder neues `SettingsCatalogPolicyRestoreTest.php`)
- Manuell: RestoreRun detail zeigt bei 400 die echte Graph-Fehlermeldung + request-id; kein generisches “apply failed” ohne Details.
**Acceptance Criteria**
- Restore von `settingsCatalogPolicy` nutzt PATCH mit sanitiziertem Payload.
- Bei Fehlern ist im RestoreRun klar ersichtlich *warum* (Graph error message), inkl. request ids für Support.
- [x] T178 [US4][Bugfix] Settings Catalog Restore: PATCH strikt auf {name, description, settings} begrenzen + Property-Mapping (displayName→name) + case-insensitive strip
- **Problem**:
- Restore von `settingsCatalogPolicy` schlägt mit 400 fehl:
- “Invalid patch, attempting to patch property Platforms is not allowed. Valid properties are Name, Description, and Settings.”
- Sanitizer lässt `platforms/Platforms` noch durch und/oder es wird `displayName` statt `name` gepatcht.
- **Implementation**:
1) **Contract fix** (`config/graph_contracts.php`)
- Für `settingsCatalogPolicy` `update_whitelist` auf exakt:
- `name`, `description`, `settings`
- Optional: `update_map` definieren:
- `displayName``name`
- (und ggf. `Description`/`Settings` casing normalisieren)
2) **Sanitizer hardening** (`app/Services/Graph/GraphContractRegistry.php`)
- Whitelist/Strip **case-insensitive** anwenden (z. B. `Platforms`, `platforms`, `PlatformS` immer entfernen).
- Vor dem Final-Payload:
- Mapping anwenden (displayName→name)
- Blocklist zusätzlich hart erzwingen: `platforms`, `technologies`, `templateReference`, `id`, `@odata.*`, `createdDateTime`, `lastModifiedDateTime`
- Ergebnis-Payload für update muss **nur** `name/description/settings` enthalten.
3) **RestoreService** (`app/Services/Intune/RestoreService.php`)
- Sicherstellen, dass für `settingsCatalogPolicy` Update-Payload aus Sanitizer kommt (kein “merge back” später).
- Bei leerem Payload: als `noop`/`skipped` behandeln statt PATCH.
- **Tests (Pest)**:
- Unit: Sanitizer entfernt `platforms/Platforms` zuverlässig + mapping `displayName→name`:
- `tests/Unit/GraphContractRegistryTest.php` (erweitern)
- Feature: Restore Settings Catalog erzeugt PATCH ohne platforms und läuft durch (Graph mocked):
- `tests/Feature/Filament/SettingsCatalogRestoreTest.php` (happy-path ergänzen)
- **Verification**:
- `./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php tests/Feature/Filament/SettingsCatalogRestoreTest.php`
- `./vendor/bin/pint --dirty`
- **Acceptance**:
- Restore von `settingsCatalogPolicy` scheitert nicht mehr an `Platforms`.
- Results zeigen bei Fehlern weiterhin request-id/client-request-id (bleibt wie T177).
- [ ] T179 [US1b][Scope][settingsCatalogPolicy] Hydrate Settings Catalog “Configuration settings” for all snapshots (versions, backups, previews) and ensure normalized display.
- **Goal:** For `settingsCatalogPolicy`, the **Configuration settings** (as seen in the Intune Portal under *Configuration settings*) must be visible throughout the system. This includes:
- being part of the raw JSON in **Policy Versions** and **Backup Snapshots**.
- being displayed in the **Normalized settings** section in a readable list/table format.
- ensuring that **Diff, Preview, and Restore** operations are based on these detailed settings, not just on general metadata.
- **Why:** The base entity for `deviceManagement/configurationPolicies` often only provides metadata (`name`, `platforms`, `settingCount`, etc.). The actual settings reside in a subresource (e.g., `.../configurationPolicies/{id}/settings`). Without hydrating this data, TenantPilot cannot display or work with the most relevant policy details like PIN length or biometric settings.
---
## Implementation
### 1) Centralize Snapshot Hydration
- In the service responsible for capturing snapshots (e.g., a central `PolicySnapshotService`, or within `VersionService` and `BackupService`), implement a method to hydrate `settingsCatalogPolicy` data.
- When the `type_key` is `settingsCatalogPolicy`:
1. `GET deviceManagement/configurationPolicies/{id}` (Base entity).
2. `GET deviceManagement/configurationPolicies/{id}/settings` (with proper paging).
3. Merge the retrieved settings into the snapshot under a consistent key (e.g., `snapshot['settings'] = [...]`).
- This hydration logic MUST be used for creating **policy versions**, **backup items**, and **restore previews**.
### 2) Enhance PolicyNormalizer
- In `app/Services/Intune/PolicyNormalizer.php`, ensure the normalizer can interpret and display the `snapshot['settings']` data for `settingsCatalogPolicy`.
- It should render a readable table/list of the settings, not just metadata.
### 3) Update UI Components
- Ensure the **Policy Detail** and **Policy Version Detail** pages use the hydrated snapshots to display the settings.
- The "Settings available" badge for `settingsCatalogPolicy` should only show "Available" if the snapshot contains the hydrated `settings`.
### 4) Testing
- **Feature Test:** Create a `SettingsCatalogPolicyHydrationTest.php` that:
- Mocks the Graph API for both the base entity and the `/settings` subresource.
- Triggers both a **Version Capture** and a **Backup**.
- Asserts that the resulting `PolicyVersion` and `BackupItem` snapshots contain the hydrated `settings`.
- Asserts that the Policy Detail and Version Detail pages display the normalized settings correctly.
- **Unit Test:** `PolicyNormalizerSettingsCatalogTest.php` should be updated to verify the rendering of a hydrated snapshot.
### Verification
- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php`
- `./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogTest.php`
- `./vendor/bin/pint --dirty`
- [x] T180 - DUPLICATE of T179. Merged into T179.
- [x] T182 [US1b][settingsCatalogPolicy][UX] Dynamic normalization of Settings Catalog “settings” (generic flatten + readable labels)
- **Goal:** `settingsCatalogPolicy` soll im **Normalized settings** Bereich nicht mehr nur “setting -” anzeigen, sondern die hydrierten `settings[]` **generisch** (ohne hartes Mapping pro Setting) als verständliche Liste/Tabelle darstellen:
- pro Setting: **SettingDefinitionId**, **Instance Type**, **Value** (und ggf. Choice-Value)
- nested `children` / group collections werden **rekursiv geflattet**
- optional: einfache Gruppierung (z. B. nach Prefix der definitionId oder “group root”)
- **Why:** Microsoft hat unzählige Settings. Wir brauchen eine **dynamische** Darstellung, die immer funktioniert auch für neue Settings, ohne dass wir jedes Setting kennen.
---
## Implementation
### 1) PolicyNormalizer: settingsCatalogPolicy → generic flatten
- In `app/Services/Intune/PolicyNormalizer.php`:
- Bei `policyType === settingsCatalogPolicy`:
- Wenn `snapshot['settings']` existiert:
- Erzeuge eine Normalizer-Sektion `Settings` als Tabelle/Repeatable:
- `definitionId` (string)
- `instanceType` (string, aus `settingInstance['@odata.type']`)
- `value` (string/number/bool/json; aus `simpleSettingValue.value` oder `choiceSettingValue.value`)
- `path` (optional): zusammengesetzter Pfad zur Einordnung (z. B. parentDefinitionId > childDefinitionId)
- Implementiere `flattenSettingsCatalogSettingInstances(array $settings): array`:
- Iteriere `settings[]` Einträge
- Extrahiere `settingInstance`
- Unterstütze generisch (mindestens):
- `deviceManagementConfigurationSimpleSettingInstance``simpleSettingValue.value`
- `deviceManagementConfigurationChoiceSettingInstance``choiceSettingValue.value`
- `deviceManagementConfigurationGroupSettingCollectionInstance`:
- iteriere `groupSettingCollectionValue[]`
- rekursiv `children[]`
- Fallback: wenn unbekannt → `value = json_encode(settingInstance)` (kurz/gekürzt)
- Für Rekursion: maximal Depth (z. B. 8) + Schutz gegen Zyklen/zu große Payloads.
- Optional: wenn Value ein “enum-like” String ist, zusätzlich `displayValue` = letzter Token nach `_` (nur für bessere Lesbarkeit, ohne Semantik zu behaupten).
- Wenn `settings` fehlt:
- Zeige Banner “Settings not hydrated” (oder “Partial snapshot”) und nur Metadaten.
### 2) Filament View: bessere Darstellung (Table statt “setting -”)
- In `PolicyResource/ViewPolicy` und `PolicyVersionResource/ViewPolicyVersion`:
- Stelle sicher, dass die Normalizer-Ausgabe für `Settings` als Tabelle angezeigt wird:
- Spalten: `Definition`, `Type`, `Value`, optional `Path`
- Lange Values: truncated mit “copy” möglich (oder expand/collapse).
### 3) Diff: Fokus auf echte Settings (optional, aber empfohlen)
- In der diff-summary Logik (falls vorhanden):
- Wenn `policyType=settingsCatalogPolicy` und `settings` vorhanden:
- Summary soll zumindest sagen: “X setting values changed/added/removed”
- (Die JSON diff bleibt weiterhin verfügbar.)
### 4) Performance & Safety
- Guardrails:
- max rows (z. B. 1000) → danach “truncated”
- value length max (z. B. 500 chars) → danach “truncated”
- depth limit
- Keine Secrets loggen; Normalizer arbeitet nur auf Snapshot JSONB.
---
## Tests (Pest)
### Unit: `tests/Unit/PolicyNormalizerSettingsCatalogFlattenTest.php`
- Input Snapshot mit:
- simpleSettingInstance (int)
- choiceSettingInstance (string)
- groupSettingCollectionInstance mit children (mix)
- Assert:
- Normalizer liefert `Settings` Sektion mit mehreren Zeilen
- jede Zeile hat `definitionId`, `instanceType`, `value`
- rekursive children werden als eigene Zeilen enthalten
- Unknown instance type fällt auf fallback (json string) ohne crash
### Feature: `tests/Feature/Filament/SettingsCatalogPolicyNormalizedDisplayTest.php`
- Erzeuge PolicyVersion (settingsCatalogPolicy) mit Snapshot inkl. `settings[]`
- Öffne Version-Detail und Policy-Detail
- Assert:
- In Normalized settings existiert Sektion “Settings”
- Tabelle enthält erwartete definitionIds und Werte (z. B. minimumpinlength=12, usebiometrics=true)
- Keine “setting -” Platzhalter mehr für diesen Snapshot
### Regression
- Bestehende Normalizer-Ausgaben für andere Typen bleiben unverändert.
---
## Verification
- `./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogFlattenTest.php`
- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicyNormalizedDisplayTest.php`
- `./vendor/bin/pint --dirty`
---
## Acceptance Criteria
- Settings Catalog Policy zeigt im **Normalized settings** Bereich eine verständliche **Settings-Tabelle** (DefinitionId/Type/Value) statt generischem “setting -”.
- Rekursive Group/Children-Settings werden sichtbar (nicht verloren).
- Darstellung ist **dynamisch** (kein hardcoded mapping pro Setting).
- Guardrails verhindern UI/Memory Explosion bei sehr großen Policies.
- [x] T183 [US1b][UX] Make Policy Version detail readable (Tabs + scroll-safe tables)
- **Goal:** Policy Version Detail (und optional Policy Detail) soll für Admins **lesbar** sein:
- **Normalized Settings** ist Default/primär sichtbar
- **Raw JSON** ist weiterhin verfügbar, aber UI zerbricht nicht durch riesige Payloads
- Settings Catalog Tabellen/Paths/IDs werden sauber dargestellt (kein “Textsalat”)
- **Why:** Aktuell verdrängt Raw JSON + lange SettingDefinitionIds/Paths die gesamte Seite. Admins sehen nicht mehr “was geändert wurde”, sondern nur Datenmüll.
---
## Implementation
### 1) UI Layout: Tabs (Normalized default, Raw JSON secondary)
- In `app/Filament/Resources/PolicyVersionResource/Pages/ViewPolicyVersion.php` (und optional `ViewPolicy.php`)
- ersetze die aktuelle Darstellung durch **Tabs**:
- Tab 1: **Normalized settings** (Default)
- Tab 2: **Raw JSON** (mit Copy Button)
- optional Tab 3: **Diff** (falls vorhanden)
- Falls Filament Infolist-Komponenten keine Tabs erlauben:
- nutze eine `ViewEntry` und rendere Tabs in Blade via `x-filament::tabs`.
### 2) Raw JSON: Max height + scroll + monospace
- In der Raw JSON Blade-View (z.B. `resources/views/filament/infolists/entries/raw-json.blade.php` oder bestehende View)
- Wrap `<pre>` mit:
- `class="max-h-[520px] overflow-auto rounded-lg border bg-gray-50 p-3 text-xs font-mono leading-relaxed"`
- optional: “Expand” action (modal/slideOver) für Vollbildansicht.
### 3) Normalized settings tables: horizontal scroll + readable columns
- In `resources/views/filament/infolists/entries/normalized-settings.blade.php`
- Table container:
- `class="overflow-x-auto rounded-lg border"`
- Table:
- `class="min-w-[900px] table-fixed"`
- Cells:
- Definition/Path: `font-mono text-xs break-all whitespace-normal`
- Value: `break-words whitespace-normal`
- Column widths:
- Definition: `w-[35%]`, Type: `w-[20%]`, Value: `w-[25%]`, Path: `w-[20%]`
- Long values: clamp (optional):
- `line-clamp-2` + “Show more” (details/modal)
### 4) Optional: Search within Settings (nice-to-have)
- Add a small client-side filter input (Alpine) above settings table:
- filters rows by DefinitionId/Value/Path
- Keep it optional if you want minimal change in v1.
---
## Tests (Pest)
### Feature: `tests/Feature/Filament/PolicyVersionReadableLayoutTest.php`
- Given a `settingsCatalogPolicy` version with long `settings` payload
- Assert:
- Tabs render (Normalized + Raw JSON)
- Raw JSON container has max-height/overflow classes
- Normalized table wrapper uses overflow-x
- Page does not contain extremely long unbroken lines without wrappers (basic assertion on classes)
---
## Verification
- `./vendor/bin/pest tests/Feature/Filament/PolicyVersionReadableLayoutTest.php`
- `./vendor/bin/pint --dirty`
---
## Acceptance Criteria
- Policy Version page is readable on normal screen widths:
- Normalized settings are immediately visible without scrolling past raw JSON
- Raw JSON is accessible in second tab and scrolls inside its container
- Settings table does not break layout; long IDs/paths wrap/scroll cleanly
- No regressions for other policy types (deviceConfiguration/compliance/scripts).
- [x] T184 [US1b][UX] Use Filament Tables for Settings Catalog settings (Policy + Version) with responsive layout + SlideOver details
- **Goal:** `settingsCatalogPolicy` Settings sollen **lesbar, scannbar und bedienbar** sein:
- als echte **Filament Table** (nicht “pseudo table” im Infolist-Blade)
- mit **truncate + tooltip**, horizontal scroll, sticky header
- mit **Details** (SlideOver) + Copy pro Row
- identisch nutzbar in **Policy Detail** und **Policy Version Detail**
- **Why:** Die aktuelle Darstellung bricht Layout/Spaltenbreiten (Definition/Type/Value laufen ineinander). Filament Tables lösen genau diese Probleme (fixed layout, responsive, actions, search).
---
## Implementation
### 1) Introduce reusable Livewire component for Settings Catalog settings table
- New: `app/Livewire/SettingsCatalogSettingsTable.php`
- Props:
- `array $settingsRows` (aus PolicyNormalizer Output oder direkt aus Snapshot `settings`)
- `string $context` (`policy|version`) optional
- Intern: build a Filament `Table` with columns:
- **Definition** (`TextColumn::make('definition')`)
- `wrap(false)`, `searchable()`, `tooltip(fn($state) => $state)`, `limit(60)`
- **Type** (`TextColumn::make('type')`)
- `wrap(false)`, `toggleable()`, `limit(50)`, `tooltip(...)`
- **Value** (`TextColumn::make('value')`)
- `wrap(false)`, `limit(60)`, `tooltip(...)`
- render `(group)` badge for group rows
- **Path** (`TextColumn::make('path')`)
- `toggleable(isToggledHiddenByDefault: true)`, `limit(80)`, `tooltip(...)`
- Table config:
- `paginated([25, 50, 100])` (default 25)
- `searchPlaceholder('Search definition/value…')`
- `striped()`, `deferLoading()`
- Row Action:
- `Action::make('details')->label('Details')->icon('heroicon-m-eye')`
- opens **SlideOver**
- shows full Definition/Type/Value/Path + optional raw setting JSON (pretty)
- Copy buttons for Definition/Value
### 2) Embed component via ViewEntry in Policy + PolicyVersion detail
- Policy detail (`app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php`)
- For `settingsCatalogPolicy`:
- render `SettingsCatalogSettingsTable` (instead of current table block)
- pass rows from Normalizer (`normalize()` should expose a stable rows array)
- PolicyVersion detail (`app/Filament/Resources/PolicyVersionResource/Pages/ViewPolicyVersion.php`)
- same embedding for `settingsCatalogPolicy`
> Rule: Nur für `settingsCatalogPolicy` auf Table UI umstellen. Andere Typen bleiben Infolist/KeyValue.
### 3) Tailwind/Filament styling guardrails (no layout break)
- Ensure table container is responsive:
- wrap table in `div class="overflow-x-auto"`
- set columns non-wrapping by default (truncate)
- Sticky header:
- enable sticky header in table (Filament supports sticky header via table wrapper CSS; if needed add a small CSS utility class in your Filament theme)
### 4) Normalizer output contract (stable)
- Ensure `PolicyNormalizer` returns for settingsCatalogPolicy:
- `['settings_table' => ['columns' => [...], 'rows' => [...]]]`
- rows fields: `definition`, `type`, `value`, `path`, `raw` (optional)
- Table uses **rows**, not parsing raw snapshot again (single source).
---
## Tests (Pest)
### Feature: `tests/Feature/Filament/SettingsCatalogSettingsTableRenderTest.php` (new)
- Create a policy + version with hydrated `settings`
- Visit Policy detail and PolicyVersion detail
- Assert:
- table headers visible (Definition/Type/Value)
- at least one known definition appears
- “Details” action exists
### Unit (optional): `tests/Unit/PolicyNormalizerSettingsCatalogRowsTest.php`
- Given snapshot with nested settings instances
- Assert normalizer returns rows with `definition/type/value/path`
---
## Verification
- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogSettingsTableRenderTest.php`
- `./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogRowsTest.php`
- `./vendor/bin/pint --dirty`
---
## Acceptance Criteria
- In **Policy Detail** und **Policy Version Detail** sind Settings Catalog Settings als **Filament Table** sichtbar (lesbar, nicht überlappend).
- Lange Werte sind **truncated** aber per Tooltip/Details vollständig erreichbar.
- Pro Row gibt es **Details SlideOver** + Copy.
- Kein Layout-Bruch auf typischen Screenbreiten (Laptop/FullHD).
- [ ]T185 [UX][US1b][settingsCatalogPolicy] Make Settings Catalog settings readable (label/value parsing + table ergonomics)
- **Goal:** Settings Catalog Policies sollen im Policy/Version Detail **für Admins lesbar** sein, ohne dass wir “alle Settings kennen müssen”.
- Tabelle zeigt **sprechende Bezeichnung** + **kompakte Werte**
- Lange IDs bleiben verfügbar (Tooltip/Copy/Details), aber dominieren nicht die UI
- **Why:** Aktuell sind `definitionId` und `choiceSettingValue.value` so lang, dass sie in der Tabelle abgeschnitten werden und der Admin weder Setting noch Wert versteht.
---
## Implementation
### 1) Presentation layer: generate human-friendly labels (no registry needed)
- Add helper in `PolicyNormalizer` (oder kleiner `SettingsCatalogPresenter`):
- `labelFromDefinitionId(string $definitionId): string`
- remove common prefixes: `device_vendor_msft_`, `user_vendor_msft_`, `policy_config_`, `admx_`
- replace `_` with spaces
- keep last 24 segments if string is huge
- replace `{tenantid}` with `{tenant}`
- Output example:
- `user_vendor_msft_policy_config_admx_desktop_nomydocumentsico...``Desktop: No My Documents Icon` (heuristic), fallback: last segments nicely spaced
> Heuristic only. If no good split possible, fallback to “last segments” label.
### 2) Parse values into a short “effective value”
- Implement `valuePreview(array $settingInstance): string`:
- For `SimpleSettingValue`: return scalar (`12`, `0`, `true/false`)
- For `ChoiceSettingValue.value`: return last token after last `_` OR map known boolean patterns:
- suffix `_true`/`_false` → `True`/`False`
- suffix `_0`/`_1` for allowed/blocked → show `0`/`1` but also tag `Allowed/Blocked` if detectable
- For group instances: show `(group)` and put children into details view only
### 3) Improve table ergonomics (Filament Table / Livewire)
- In `SettingsCatalogSettingsTable`:
- Columns:
1) **Setting** (human label) + small muted secondary line showing truncated definitionId
2) **Value** (valuePreview)
3) **Type** (badge: Choice/Simple/Group)
4) Optional: **Path** (toggleable, hidden by default)
- Add:
- `->searchable()` should search both label + raw definitionId + raw value
- `->wrap()` / `->limit()` for long strings
- tooltips showing full definitionId/value on hover
- “Copy” icon action in row details (SlideOver) for Definition + Raw JSON
- Ensure horizontal scroll only inside table container:
- wrapper `div` with `overflow-x-auto` + `max-w-full`
- table layout fixed where possible (`table-fixed`) to prevent column blowouts
### 4) Keep Raw JSON accessible but not primary
- In PolicyVersion view:
- Put Raw JSON into collapsible section or separate tab.
- Normalized Settings tab becomes default for settingsCatalogPolicy.
---
## Tests (Pest)
### Unit: `tests/Unit/SettingsCatalogPresenterTest.php`
- labelFromDefinitionId() produces readable output and stable fallback
- valuePreview() returns expected previews for:
- choice true/false, numeric, group
### Feature: `tests/Feature/Filament/SettingsCatalogSettingsTableUsabilityTest.php`
- Render policy detail with one very long definition + long choice value
- Assert:
- label column shows shortened readable label (not the full raw string)
- value column shows preview (e.g., `True`, `12`, `Never`)
- details slide-over contains full raw definition/value + copy UI
---
## Verification
- `./vendor/bin/pest tests/Unit/SettingsCatalogPresenterTest.php tests/Feature/Filament/SettingsCatalogSettingsTableUsabilityTest.php`
- `./vendor/bin/pint --dirty`
---
## Acceptance Criteria
- In Policy Detail, Settings table shows:
- **Readable Setting name** (not a cut-off vendor string)
- **Readable Value preview** (True/False/12/etc.)
- Full raw definitionId and raw value remain accessible via tooltip and SlideOver + copy button.
- No layout overflow/broken columns on common laptop viewport widths.
### Implementation for User Story 1
- [x] T009 [US1] Implement policy sync/import orchestrator using Graph abstraction in `app/Services/Intune/PolicySyncService.php` (no direct Graph in UI).
- Implements: FR-001
- [x] T010 [US1] Create Filament resource/table for policies with filters and metadata columns in `app/Filament/Resources/PolicyResource.php`.
- Implements: FR-001
- [x] T011 [US1] Add command/job to sync policies (queues-ready) in `app/Console/Commands/SyncPolicies.php` and queue job under `app/Jobs/`.
## Phase 4: User Story 2 - Backup creation and browsing (Priority: P1)
### Tests for User Story 2
- [x] T012 [P] [US2] Feature test for creating backup sets with multiple policies and verifying immutable JSONB snapshots + audit log in `tests/Feature/Filament/BackupCreationTest.php`.
### Implementation for User Story 2
- [x] T013 [US2] Implement backup domain service to assemble snapshots from policies with Graph payload retrieval in `app/Services/Intune/BackupService.php`.
- Implements: FR-002
- Implements: FR-020
- [x] T014 [US2] Add Filament resource/pages for backup sets and items (list/detail) in `app/Filament/Resources/BackupSetResource.php`.
- Implements: FR-002
- [x] T131 [UX] [US2] Refactor BackupSet policy selection to RelationManager:
- Remove the multi-select policy picker from the BackupSet **Create** form (keep Create minimal: name/description).
- After create, redirect to BackupSet **Edit/View** where items can be managed.
- Add `BackupItemsRelationManager` to `BackupSetResource` showing a table with columns: Policy Name, Type (badge), Restore (badge), Risk (badge).
- Add header action “Policies hinzufügen” (searchable, multiple) that adds items/attaches policies **tenant-scoped** and prevents duplicates per BackupSet.
- Provide a remove action (detach/soft-delete as per domain rules).
- [x] T132 [P] [US2] Update/extend `tests/Feature/Filament/BackupCreationTest.php` to cover the new UX flow:
- Create BackupSet without policies.
- Add multiple policies via RelationManager action.
- Verify immutable JSONB snapshots + audit log behavior remains correct.
- [x] T015 [US2] Wire audit logging for backup creation events in `app/Services/Intune/BackupService.php` using `AuditLogger`.
- Implements: FR-003
## Phase 5: User Story 3 - Version history and diff (Priority: P1)
### Tests for User Story 3
- [x] T016 [P] [US3] Feature test for version capture and timeline display in `tests/Feature/Filament/PolicyVersionTest.php`.
- [x] T017 [P] [US3] Unit test for diff generation (human summary + JSON diff) in `tests/Unit/VersionDiffTest.php`.
### Implementation for User Story 3
- [x] T018 [US3] Implement version capture service with immutable JSONB writes in `app/Services/Intune/VersionService.php`.
- Implements: FR-004
- [x] T019 [US3] Create diff helper (summary + structured JSON) in `app/Services/Intune/VersionDiff.php` and surface in Filament version compare view in `app/Filament/Resources/PolicyVersionResource.php`.
- Implements: FR-005
- [x] T020 [US3] Hook version capture into relevant flows (manual trigger + backup/restore hooks) ensuring audit logging.
## Phase 6: User Story 4 - Restore with preview and confirmation (Priority: P1)
### Tests for User Story 4
- [x] T021 [P] [US4] Feature test for restore preview (change summary, conflicts, selective items) in `tests/Feature/Filament/RestorePreviewTest.php`.
- [x] T022 [P] [US4] Feature test for confirmed restore execution capturing audit logs and per-item outcomes in `tests/Feature/Filament/RestoreExecutionTest.php`.
### Implementation for User Story 4
- [x] T023 [US4] Implement restore service with preview/dry-run and selective item application in `app/Services/Intune/RestoreService.php`, integrating Graph adapter and conflict detection.
- [x] T024 [US4] Add Filament restore UI (wizard or pages) showing preview, warnings, and confirmation gate in `app/Filament/Resources/RestoreRunResource.php`.
- Implements: FR-022
- [x] T025 [US4] Record restore run lifecycle (start, per-item result, completion) and audit events in `restore_runs` and `audit_logs`.
- Implements: FR-007
- [ ] T156 [US4][UX] Add “Rerun” action to RestoreRun row actions (ActionGroup): creates a new RestoreRun cloned from selected run (same backup_set_id, same selected items, same dry_run flag), enforces same safety gates/confirmations as original execution path, writes audit event restore_run.rerun_created with source_restore_run_id.
- Implements: FR-035
- Implements: FR-035
## Phase 7: User Story 5 - Operational readiness and environments (Priority: P2)
- [x] T026 [US5] Document Dokploy staging→production promotion steps, required env vars, queue/worker expectations, and migration safety notes in `README.md` or `docs/deploy.md`.
- Implements: FR-010
- Implements: FR-011
- Implements: FR-012
- [x] T027 [US5] Add quick Sail commands and test invocation notes to `README.md` (e.g., `./vendor/bin/sail artisan test`) and ensure sample env entries for Graph credentials.
## Phase 8: Tenant Management (Tenant hinzufügen, App-Setup, Verify) (Priority: P1)
> Hinweis: Diese Phase ist “Tenant Management” und **nicht** US6, damit US6 sauber “Permissions/Health” bleibt.
- [x] T030 [TENANT] Migration für `tenants` ergänzen/prüfen (name, tenant_id, domain, app_client_id, app_status, app_notes, timestamps).
- Implements: FR-011
- Implements: FR-014
- Implements: FR-015
- Implements: FR-016
- Implements: FR-017
- Implements: FR-018
- [x] T031 [TENANT] Eloquent Model `Tenant` (Beziehungen, tenant-aware scopes).
- [x] T032 [TENANT] Filament `TenantResource` (list/create/edit/detail; Actions: Open in Entra, Copy consent URL optional).
- [x] T033 [TENANT] `TenantConfigService` / Graph connectivity check.
- [x] T034 [TENANT] Action „Verify configuration“ + Audit (`tenant.config.verified`).
- [x] T035 [TENANT] Tenant-Kontext in Policy/Backup/Restore/Audit Services (tenant_id überall setzen).
- Implements: FR-009
- [x] T036 [TENANT] Feature-Test `TenantSetupTest` (ok/error + Audit).
- [x] T037 [TENANT] Admin-Consent Callback Route (state → tenant mapping, status update, UI page).
## Phase 9: User Story 6 - Berechtigungsübersicht & Health-Status (Priority: P1)
- [x] T040 [P] [US6] Zentrale Permissions-Liste `config/intune_permissions.php` (+ optional `docs/permissions.md`).
- Implements: FR-006
- Implements: FR-008
- [x] T041 [US6] Datenmodell Tenant-Berechtigungen (JSONB `granted_permissions` oder `tenant_permissions` Tabelle; status ok/missing/error).
- [x] T042 [US6] `TenantPermissionService` (required, granted, compare DTO).
- [x] T043 [US6] Tenant-Detail UI Panel „Permissions“ (required list + status).
- [x] T044 [US6] Verify erweitern: compare + persist + Audit `tenant.permissions.checked`.
- Implements: FR-006
- Implements: FR-008
- [x] T045 [US6] Tests: Unit compare + Feature Tenant detail status + Verify updates.
## Phase 9b: Scope-Ausrichtung auf neue Objekttypen
- [x] T028 [Scope] `config/tenantpilot.php` auf `scope.supported_types` erweitern; single source for sync/backup/restore.
- [x] T029 [Scope] Filament-UI an neue Typen anpassen (Filter/Grouping + Restore-Level Hinweise).
## Phase 10: Housekeeping Delete-Funktionen für Backups & Versions
- [x] T060 [HK] BackupSets soft deletable + Guard gegen RestoreRuns.
- [x] T061 [HK] Filament Delete BackupSets + Confirmation + Audit (`backup.deleted`) + Guard.
- [x] T062 [HK] PolicyVersions soft deletable + Queries/Resources default non-deleted.
- [x] T063 [HK] Filament Delete PolicyVersions + Audit (`policy_version.deleted`).
- [x] T064 [HK] Tests Housekeeping (BackupSet delete ok/block + PolicyVersion delete).
## Phase 11: Housekeeping Tenant löschen/deaktivieren
- [x] T070 [HK] Tenants soft deletable (optional status active/archived).
- [x] T071 [HK] Tenant deactivate/archive action + Audit (`tenant.archived`).
- [x] T072 [HK] Block operations for deactivated tenants (sync/backup/restore early fail).
- [x] T073 [HK] RestoreRuns soft deletable (optional) + Audit (`restore_run.deleted`).
- [x] T074 [HK] Tests Tenant delete/deactivate behavior + clear errors, no Graph calls.
## Phase 12: Housekeeping Hard Deletes (Force Delete)
- [x] T075 [HK] Force-Delete-Actions (only in trashed; guards; audit before delete) + tests.
## Phase 12b: Single current tenant ("Highlander")
- [x] T120 [TENANT] Migration add `is_current` + partial unique index.
- [x] T121 [TENANT] Tenant::current() + makeCurrent() + remove implicit defaults.
- [x] T122 [TENANT] Data cleanup (mark one current; archive local-tenant).
- [x] T123 [TENANT] Filament UI badge + “Make current” action.
- [x] T124 [TENANT] Consumers refactor to `Tenant::current()` or explicit tenant.
- [x] T125 [TENANT] Tests for current selection + “Make current”.
- [x] T130 [UX] Tabellen-Aktionen in Dropdown bündeln (ActionGroup) in TenantResource (+ optional others).
## Phase 13: Settings Normalization & Display (Priority: P1)
- [x] T140 [P] [US1b] Unit test PolicyNormalizer.
- [x] T141 [P] [US1b] Feature test Policy Settings section.
- [x] T142 [P] [US1b] Feature test Version detail pretty JSON + normalized.
- [x] T143 [P] [Edge] Feature test malformed snapshot warning.
- [x] T144 [P] [Edge] Feature test @odata.type mismatch flag + restore exec block.
- [x] T145 [US1b] PolicyNormalizer service.
- Implements: FR-019
- [x] T146 [US1b] Settings infolist in PolicyResource.
- [x] T147 [US1b] PolicyVersion view pretty JSON + normalized.
- [x] T148 [US1b] Integrations (list badge, optional diff enhancements, tenant scoping).
- [x] T149 [Edge] SnapshotValidator helper.
- [x] T150 [Edge] @odata.type validator (policy/backup/restore gates).
- [x] T151 [Edge] UI warnings + restore execution gating (preview may show).
- [x] T152 [US1b] README docs for settings display.
- [x] T153 [US1b] Inline docs in PolicyNormalizer.
## Phase 14: User Story 7 Intune RBAC Onboarding Wizard (Delegated, Synchronous)
**Scope**: FR-023 to FR-030; delegated login and grant run synchronously in Filament (no queue for grant). Optional jobs/CLI only for CHECK/REPORT (no grant).
- [x] T160 [US7] Add TenantResource ActionGroup entry “Setup Intune RBAC”: visible for active tenants with `app_client_id`; sits alongside Admin consent/Verify; guarded by Highlander rules. Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
- Implements: FR-023
- Implements: FR-025
- Implements: FR-026
- Implements: FR-027
- Implements: FR-028
- Implements: FR-029
- [x] T161 [US7] Wizard UI (Filament): Step 1 preconditions/summary with inputs for Role, Scope, Group mode; least-privilege warnings; review screen of planned changes. Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
- [x] T162 [US7] Delegated auth step: initiate delegated login; stop with clear error + audit on failure; token not persisted. Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
- [x] T163 [US7] Execution service (sync) with audit per step: resolve SP by `app_client_id`; ensure/create security group (`securityEnabled=true`); add SP as member (idempotent); ensure/create/update Intune role assignment; persist IDs on tenant for idempotency. Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
- Implements: FR-030
- [x] T164 [US7] Post-check (mandatory): clear app token cache / force fresh token acquisition and run canary reads:
- `GET /deviceManagement/deviceConfigurations?$top=1`
- `GET /deviceManagement/deviceCompliancePolicies?$top=1`
- optional CA canary only if CA features enabled
- update tenant health + audit verify outcome.
Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
- [x] T165 [US7] Tests (Pest, mocked Graph): happy path; rerun idempotent; missing permissions error mapping; scope-limited warning; delegated login failure path; non-security-enabled group failure. Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
- [x] T166 [US7] Documentation: README note for wizard behavior (delegated, sync), least-privilege defaults, audit expectations, rerun safety. Verified by: manual review of README.md update.
- [ ] T167 [US7-Optional] CLI/Job for CHECK/REPORT only (no grant), explicitly exclude async grant.
- [x] T168 [US7] Extend Verify configuration / Health panel to include “Intune RBAC status” (OK/Missing/Error) + CTA “Run Setup Intune RBAC”, persist last_checked_at + reason; Audit `tenant.rbac.checked`. Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
- Implements: FR-024
- [x] T169 [US7] Persist RBAC artifacts on Tenant for idempotency:
- migration add nullable columns: `rbac_group_id`, `rbac_group_name`, `rbac_role_assignment_id`, `rbac_role_key`, `rbac_scope_mode`, `rbac_scope_id`
- prefer stored IDs on reruns; discovery fallback.
Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
## Phase 15: User Story 8 Graph Contract Registry & Drift Guard
**Scope**: FR-031 to FR-034; contract registry per type, type-family handling, capability fallbacks, drift checks.
- [x] T170 [US8] Add contract registry artifact (e.g., `config/graph_contracts.php`) capturing per supported type: resource paths, allowed `$select`/`$expand`, allowed @odata.type family, create/update methods, id field, hydration strategy. Verified by: manual review.
- [x] T171 [US8] Implement registry service + integration in Graph client to enforce allowed capabilities and downgrade on capability errors (retry without expand/select), logging warnings/audit entries. Verified by: `./vendor/bin/pest tests/Unit/GraphContractFallbackTest.php`.
- Implements: FR-032
- [x] T172 [US8] Implement type-family handling so derived @odata.type within a family routes correctly for preview/restore (no odata_mismatch) while still blocking unknown types. Verified by: `./vendor/bin/pest tests/Feature/Filament/ODataTypeMismatchTest.php`.
- Implements: FR-033
- [x] T173 [US8] Add verification command `php artisan graph:contract:check` (staging/CI) to probe endpoints, detect drift, and emit actionable diff/log output; make prod opt-in/guarded. Verified by: manual review.
- Implements: FR-034
- [x] T174 [US8] Tests (Pest/unit/integration): registry lookups, fallback selection on capability errors, derived type acceptance, drift-check command behavior. Verified by: `./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php tests/Unit/GraphContractFallbackTest.php`.
- [x] T175 [US8] Documentation: describe registry format/update process, fallback behavior, and how/when to run `graph:contract:check`. Verified by: manual review of README update.