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

52 KiB
Raw Blame History

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)

  • T001 [P] [Shared] Confirm Sail/Env ready; ensure .env has PostgreSQL settings for Sail and Filament admin user seeded (if missing) in database/seeders/.
  • 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)

  • 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/.
  • T004 [Shared] Create models with relationships and guarded attributes for the above entities in app/Models/.
  • T005 [Shared] Implement Graph abstraction contracts (GraphClientInterface, error mapping, logging hooks) in app/Services/Graph/ with a mockable adapter.
  • T006 [Shared] Add audit logging service/helper to capture actor, tenant, operation, resources, outcome in app/Services/Intune/AuditLogger.php.
  • 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

  • T008 [P] [US1] Feature test for Filament policy listing and filtering (tenant-scoped) in tests/Feature/Filament/PolicyListingTest.php using mocked Graph sync.

  • 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.
  • 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.
  • 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:
          • displayNamename
          • (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.
  1. 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).
  1. 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)
  1. 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.

  • 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):
            • deviceManagementConfigurationSimpleSettingInstancesimpleSettingValue.value
            • deviceManagementConfigurationChoiceSettingInstancechoiceSettingValue.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.

  • 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).

  • 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/_falseTrue/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

  • T009 [US1] Implement policy sync/import orchestrator using Graph abstraction in app/Services/Intune/PolicySyncService.php (no direct Graph in UI).
  • T010 [US1] Create Filament resource/table for policies with filters and metadata columns in app/Filament/Resources/PolicyResource.php.
  • 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

  • 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

  • T013 [US2] Implement backup domain service to assemble snapshots from policies with Graph payload retrieval in app/Services/Intune/BackupService.php.

  • T014 [US2] Add Filament resource/pages for backup sets and items (list/detail) in app/Filament/Resources/BackupSetResource.php.

  • 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).
  • 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.
  • 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

  • T016 [P] [US3] Feature test for version capture and timeline display in tests/Feature/Filament/PolicyVersionTest.php.
  • T017 [P] [US3] Unit test for diff generation (human summary + JSON diff) in tests/Unit/VersionDiffTest.php.

Implementation for User Story 3

  • T018 [US3] Implement version capture service with immutable JSONB writes in app/Services/Intune/VersionService.php.
  • 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.
  • 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

  • T021 [P] [US4] Feature test for restore preview (change summary, conflicts, selective items) in tests/Feature/Filament/RestorePreviewTest.php.
  • 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

  • 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.
  • T024 [US4] Add Filament restore UI (wizard or pages) showing preview, warnings, and confirmation gate in app/Filament/Resources/RestoreRunResource.php.
  • 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)

  • 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.
  • 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.

  • T030 [TENANT] Migration für tenants ergänzen/prüfen (name, tenant_id, domain, app_client_id, app_status, app_notes, timestamps).
  • T031 [TENANT] Eloquent Model Tenant (Beziehungen, tenant-aware scopes).
  • T032 [TENANT] Filament TenantResource (list/create/edit/detail; Actions: Open in Entra, Copy consent URL optional).
  • T033 [TENANT] TenantConfigService / Graph connectivity check.
  • T034 [TENANT] Action „Verify configuration“ + Audit (tenant.config.verified).
  • T035 [TENANT] Tenant-Kontext in Policy/Backup/Restore/Audit Services (tenant_id überall setzen).
  • T036 [TENANT] Feature-Test TenantSetupTest (ok/error + Audit).
  • T037 [TENANT] Admin-Consent Callback Route (state → tenant mapping, status update, UI page).

Phase 9: User Story 6 - Berechtigungsübersicht & Health-Status (Priority: P1)

  • T040 [P] [US6] Zentrale Permissions-Liste config/intune_permissions.php (+ optional docs/permissions.md).
  • T041 [US6] Datenmodell Tenant-Berechtigungen (JSONB granted_permissions oder tenant_permissions Tabelle; status ok/missing/error).
  • T042 [US6] TenantPermissionService (required, granted, compare DTO).
  • T043 [US6] Tenant-Detail UI Panel „Permissions“ (required list + status).
  • T044 [US6] Verify erweitern: compare + persist + Audit tenant.permissions.checked.
  • T045 [US6] Tests: Unit compare + Feature Tenant detail status + Verify updates.

Phase 9b: Scope-Ausrichtung auf neue Objekttypen

  • T028 [Scope] config/tenantpilot.php auf scope.supported_types erweitern; single source for sync/backup/restore.
  • T029 [Scope] Filament-UI an neue Typen anpassen (Filter/Grouping + Restore-Level Hinweise).

Phase 10: Housekeeping Delete-Funktionen für Backups & Versions

  • T060 [HK] BackupSets soft deletable + Guard gegen RestoreRuns.
  • T061 [HK] Filament Delete BackupSets + Confirmation + Audit (backup.deleted) + Guard.
  • T062 [HK] PolicyVersions soft deletable + Queries/Resources default non-deleted.
  • T063 [HK] Filament Delete PolicyVersions + Audit (policy_version.deleted).
  • T064 [HK] Tests Housekeeping (BackupSet delete ok/block + PolicyVersion delete).

Phase 11: Housekeeping Tenant löschen/deaktivieren

  • T070 [HK] Tenants soft deletable (optional status active/archived).
  • T071 [HK] Tenant deactivate/archive action + Audit (tenant.archived).
  • T072 [HK] Block operations for deactivated tenants (sync/backup/restore early fail).
  • T073 [HK] RestoreRuns soft deletable (optional) + Audit (restore_run.deleted).
  • T074 [HK] Tests Tenant delete/deactivate behavior + clear errors, no Graph calls.

Phase 12: Housekeeping Hard Deletes (Force Delete)

  • T075 [HK] Force-Delete-Actions (only in trashed; guards; audit before delete) + tests.

Phase 12b: Single current tenant ("Highlander")

  • T120 [TENANT] Migration add is_current + partial unique index.

  • T121 [TENANT] Tenant::current() + makeCurrent() + remove implicit defaults.

  • T122 [TENANT] Data cleanup (mark one current; archive local-tenant).

  • T123 [TENANT] Filament UI badge + “Make current” action.

  • T124 [TENANT] Consumers refactor to Tenant::current() or explicit tenant.

  • T125 [TENANT] Tests for current selection + “Make current”.

  • T130 [UX] Tabellen-Aktionen in Dropdown bündeln (ActionGroup) in TenantResource (+ optional others).

Phase 13: Settings Normalization & Display (Priority: P1)

  • T140 [P] [US1b] Unit test PolicyNormalizer.

  • T141 [P] [US1b] Feature test Policy Settings section.

  • T142 [P] [US1b] Feature test Version detail pretty JSON + normalized.

  • T143 [P] [Edge] Feature test malformed snapshot warning.

  • T144 [P] [Edge] Feature test @odata.type mismatch flag + restore exec block.

  • T145 [US1b] PolicyNormalizer service.

  • T146 [US1b] Settings infolist in PolicyResource.

  • T147 [US1b] PolicyVersion view pretty JSON + normalized.

  • T148 [US1b] Integrations (list badge, optional diff enhancements, tenant scoping).

  • T149 [Edge] SnapshotValidator helper.

  • T150 [Edge] @odata.type validator (policy/backup/restore gates).

  • T151 [Edge] UI warnings + restore execution gating (preview may show).

  • T152 [US1b] README docs for settings display.

  • 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).

  • 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.

  • 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.

  • 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.

  • 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.

  • 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.
  • 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.

  • 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.

  • 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.

  • 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.

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.