TenantAtlas/.specify/tasks.md
2025-12-14 20:23:18 +01:00

907 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)
**Status snapshot**
- Done: Phases 113 (US1US4, Settings normalization/display, Highlander, US6 permissions/health, housekeeping/UX, ops)
- Next up: Phase 14 (US7) delegated Intune RBAC onboarding wizard (synchronous)
- Upcoming: Phase 15 (US8) Graph Contract Registry & Drift Guard
---
## 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/`.
- [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 snapshots + normalized display
- **Goal:** Für `settingsCatalogPolicy` sollen die **Configuration settings** (wie im Intune Portal unter *Configuration settings*) im System sichtbar sein:
- in **Policy Version Raw JSON** enthalten
- im **Normalized settings** Abschnitt als verständliche Liste/Tabelle dargestellt
- damit Diff/Preview/Restore auf den relevanten Settings basiert (nicht nur “General” Metadaten).
- **Why:** `deviceManagement/configurationPolicies` liefert im Base-Entity oft nur Metadaten (`name`, `platforms`, `technologies`, `settingCount`, `templateReference` …). Die eigentlichen Settings liegen typischerweise in einem **Subresource** (z. B. `.../configurationPolicies/{id}/settings`). Aktuell zeigt TenantPilot daher nicht die relevanten Werte (PIN length, biometrics, etc.).
---
## Implementation
### 1) Graph Contract Registry erweitern (Hydration Strategy)
- In `config/graph_contracts.php` beim Contract für `settingsCatalogPolicy` ergänzen:
- `member_hydration_strategy: 'subresource_settings'`
- `subresources`:
- `settings`:
- `path`: `deviceManagement/configurationPolicies/{id}/settings`
- `collection`: true
- `paging`: true
- `allowed_select`: konservativ (oder leer → fallback)
- `allowed_expand`: leer
> Erwartung: Registry definiert, wie der Snapshot “vollständig” gemacht wird.
### 2) Snapshot Capture für settingsCatalogPolicy hydrieren
- In dem Service, der Snapshots für **Version/Backup/Restore-Preview** lädt (z. B. `BackupService`, `VersionService`, `RestoreService` oder zentraler “PolicySnapshotService” falls vorhanden):
- Wenn `type_key === settingsCatalogPolicy`:
1. `GET deviceManagement/configurationPolicies/{id}` (Base entity)
2. `GET deviceManagement/configurationPolicies/{id}/settings` (paged)
3. Im Snapshot speichern als **entweder**:
- `snapshot['settingsCatalog'] = ['settings' => [...]]`
- **oder** `snapshot['settings'] = [...]` (wenn konsistenter mit Normalizer)
- Wichtig: **Keine Secrets** loggen, Payload bleibt JSONB.
- Pagination: `$top` + `@odata.nextLink` sauber abarbeiten.
### 3) PolicyNormalizer erweitern (Settings Catalog wirklich anzeigen)
- In `app/Services/Intune/PolicyNormalizer.php`:
- Für `settingsCatalogPolicy` nicht nur Metadaten anzeigen, sondern:
- `settings` / `settingsCatalog.settings` interpretieren
- pro Setting mindestens:
- Setting-Name/Display (wenn vorhanden)
- Setting-Path/DefinitionId (wenn vorhanden)
- Value (aktueller Wert)
- Ausgabe als Tabelle/RepeatableEntry (“Key/Value”), gruppiert nach Kategorie (z. B. “Windows Hello for Business”), wenn ableitbar.
### 4) “Settings available” Badge korrekt setzen
- Stelle sicher, dass die Logik “Settings available” bei `settingsCatalogPolicy` erst dann **Available** zeigt, wenn der Snapshot **Settings** enthält (nicht nur Base entity).
- Optional: Status “Partial”, wenn Base ok ist, aber Settings fetch fehlgeschlagen.
### 5) Diff & Restore Preview profitieren lassen (Ziel, kein Muss)
- Diff/Preview soll aus dem hydrierten Snapshot arbeiten → Änderungen an Settings werden sichtbar.
- Falls Diff aktuell nur top-level vergleicht: sicherstellen, dass `settings` Teil des Snapshot-Diffs ist.
### 6) Permissions/Health prüfen
- Prüfen, ob der `.../settings` Endpoint zusätzliche Permissions braucht.
- Falls ja: `config/intune_permissions.php` ergänzen + Verify/Health zeigt Missing sauber an.
---
## Tests (Pest)
### Feature: `tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php`
- Seed Policy vom Typ `settingsCatalogPolicy`
- Mock Graph:
- Base entity call liefert Metadaten
- `/settings` liefert 23 Settings Objekte (mit Value)
- Trigger Snapshot Capture (Version oder Backup)
- Assert:
- Snapshot enthält `settings` / `settingsCatalog.settings`
- Policy Version Detail zeigt Normalized settings mit diesen Einträgen
### Unit: `tests/Unit/PolicyNormalizerSettingsCatalogTest.php`
- Input: Snapshot mit `settings` Array
- Assert: Normalizer liefert strukturierte Key/Value Ausgabe, nicht nur Metadaten
### Regression
- `deviceConfiguration` / `deviceCompliancePolicy` etc. bleiben unverändert.
---
## Verification
- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php`
- `./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogTest.php`
- `./vendor/bin/pint --dirty`
- [ ] T180 [US1b][Bug][settingsCatalogPolicy] Hydrate Settings Catalog settings in Version capture + Policy detail uses hydrated snapshot
- **Goal:** `settingsCatalogPolicy` soll die *Configuration settings* nicht nur in Backups, sondern auch in **Policy Versions** enthalten, damit **Policy Detail**, Diff/Preview/Restore auf den echten Settings basieren.
- **Why:** Aktuell hydriert nur `BackupService`, aber Policy Detail/Versions zeigen weiterhin nur Base-Metadaten.
### Implementation
1) **VersionService (oder zentraler SnapshotFetcher) erweitern**
- Beim Version-Capture für `settingsCatalogPolicy`:
- `GET deviceManagement/configurationPolicies/{id}`
- `GET deviceManagement/configurationPolicies/{id}/settings` (paging + nextLink)
- Merge in Snapshot unter **dem Key den Normalizer nutzt**:
- bevorzugt: `snapshot['settings'] = [...];`
- Keine Secrets loggen; nur IDs/Status im Audit.
2) **Policy Detail nutzt latest Version Snapshot**
- Sicherstellen, dass `PolicyResource -> ViewPolicy` / Normalizer den **latest policy_version snapshot** nimmt (nicht nur Policy-Metadaten).
- Falls bereits so: nur sicherstellen, dass der Snapshot-Key konsistent ist (`settings`).
3) **PolicyNormalizer: settingsCatalogPolicy Rendering**
- Falls bereits vorhanden: Normalizer liest `snapshot['settings']`.
- Falls nicht: ergänzen, damit in der UI eine Tabelle/Liste entsteht:
- Setting name / definitionId / value (mindestens)
4) **Settings Badge Logik**
- Badge “Settings available” soll bei settingsCatalogPolicy nur **Available** sein, wenn `snapshot['settings']` **nicht leer** ist.
- Optional: “Partial”, wenn Base ok aber settings fetch fehlgeschlagen.
### Tests (Pest)
- **Feature:** `tests/Feature/Filament/SettingsCatalogPolicyVersionHydrationTest.php`
- Mock Graph base entity + `/settings`
- Trigger **Version capture**
- Assert: Version Raw JSON enthält `settings`
- Assert: Policy Detail “Normalized settings” zeigt konkrete Settings (z. B. PIN length / biometrics)
- **Unit:** erweitere `PolicyNormalizerSettingsCatalogTest.php` falls nötig (Key + rendering)
### Verification
- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicyVersionHydrationTest.php`
- `./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogTest.php`
- `./vendor/bin/pint --dirty`
### Acceptance Criteria
- Policy Version Raw JSON enthält Settings (nicht nur Metadaten).
- Policy Detail zeigt konkrete Settings (z. B. “Minimum PIN Length: 12”, “Allow biometrics: True”).
- Settings Badge ist **Available**, sobald hydrierte Settings im Snapshot vorhanden sind.
- Keine Regression bei bestehenden Typen.
---
## Acceptance Criteria
- Settings Catalog Policies zeigen im **Policy Version Raw JSON** die **Settings** (nicht nur Metadaten).
- Im **Normalized settings** Bereich erscheinen konkrete Werte (z. B. “Minimum PIN Length: 12”, “Allow biometrics: True”).
- “Settings” Badge ist **Available**, sobald hydrierte Settings im Snapshot vorhanden sind.
- Keine Änderungen/Regressions bei bestehenden Typen.
- [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.
### Discovery → Decision
- Checked available Graph paths and contract registry: `configurationPolicies` exposes a subresource at `deviceManagement/configurationPolicies/{id}/settings` which is the supported method to add/update settings for Settings Catalog policies. There is no special action; the supported mechanism is a POST to the settings subresource (collection) or the collection resource when creating a new policy. Therefore the restore flow will:
- PATCH top-level metadata (`name`, `description`) via the policy resource
- POST settings to `deviceManagement/configurationPolicies/{id}/settings` when present
- If the tenant/API rejects the settings POST (NotSupported/ModelValidationFailure), the restore item will be marked `manual_required` with Graph request IDs and a clear admin message.
---
## 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.)
- [ ] T186 [US4][Bugfix][settingsCatalogPolicy] Fix settings_apply payload typing (@odata.type) + body shape for configurationPolicies/{id}/settings
**Goal:** Restore für `settingsCatalogPolicy` soll Settings zuverlässig anwenden können, ohne ModelValidationFailure wegen fehlender/entfernter `@odata.type`.
**Why:** Aktuell schlägt `settings_apply` fehl mit „choiceSettingValue does not exist on type …SettingInstance“ → typischerweise fehlt `@odata.type` in `settingInstance` (oder in nested children) nach Sanitizing/Mapping.
### Implementation
1. **Contract:** Ensure `settings_apply` schema is explicit in `config/graph_contracts.php` (method = `POST`, path = `deviceManagement/configurationPolicies/{id}/settings`, `body_shape` = `collection`).
2. **Sanitizer:** In `GraphContractRegistry` allow and preserve `@odata.type` inside `settingInstance` and nested children (recursively); continue to strip read-only/meta fields and `id`.
3. **RestoreService:** Build `settingsPayload = sanitizeSettingsApplyPayload(snapshot['settings'])` and `POST` to the contract path; on failure mark item `manual_required` and persist Graph meta (`request_id`, `client_request_id`, error message).
4. **UI:** RestoreRun Results view shows clear admin message when `manual_required` due to settings_apply, including request ids.
### Tests (Pest)
- Unit: `tests/Unit/GraphContractRegistrySettingsApplySanitizerTest.php` (preserve `@odata.type`, strip ids)
- Feature: `tests/Feature/Filament/SettingsCatalogRestoreApplySettingsTest.php` (mock Graph, assert POST body includes `@odata.type` and success/failure flows)
### Verification
- `./vendor/bin/pest tests/Unit/GraphContractRegistrySettingsApplySanitizerTest.php`
- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogRestoreApplySettingsTest.php`
- `./vendor/bin/pint --dirty`
### Acceptance Criteria
- RestoreRun for `settingsCatalogPolicy` no longer fails with `choiceSettingValue does not exist …` when Graph supports settings POST.
- POST `.../settings` includes `settingInstance.@odata.type` (recursive) and is accepted by Graph, or the restore item is marked `manual_required` with request IDs visible.
- No regressions for other restore types.
- 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).
- [x] T010 [US1] Create Filament resource/table for policies with filters and metadata columns in `app/Filament/Resources/PolicyResource.php`.
- [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`.
- [x] T014 [US2] Add Filament resource/pages for backup sets and items (list/detail) in `app/Filament/Resources/BackupSetResource.php`.
- [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`.
## 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`.
- [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`.
- [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`.
- [x] T025 [US4] Record restore run lifecycle (start, per-item result, completion) and audit events in `restore_runs` and `audit_logs`.
- [ ] 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.
## 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`.
- [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).
- [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).
- [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`).
- [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`.
- [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.
- [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`.
- [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`.
- [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`.
- [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`.
- [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`.
- [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.
- [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.