From 79636c13c53e5ce141dd6c2e9d23562f906777b6 Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Sat, 13 Dec 2025 19:12:32 +0100 Subject: [PATCH 01/18] =?UTF-8?q?docs(speckit):=20add=20constitution=20evi?= =?UTF-8?q?dence=20ledger,=20FR=E2=86=92Task=20traceability,=20and=20measu?= =?UTF-8?q?rable=20NFR=20thresholds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Constitution Evidence Ledger with discovery + verification for Phases 1-15 - Add FR → Tasks Traceability Matrix (FR-001 to FR-035 → Task IDs) - Add Measurable Thresholds (NFR/UX): rendering limits, Graph timeouts, retention policies - Annotate tasks with explicit Implements: FR-XXX tags (100% FR coverage: 35/35) - Consolidate spec artifacts into specs/001-rbac-onboarding/ per speckit workflow - Add FR-019 Settings Normalization sub-requirements (FR-019.1 to FR-019.4) Constitution VII (Spec-Driven Development) compliance achieved: - Discovery notes present for all completed phases - Verification commands documented per phase - Explicit FR→Task mapping for traceability - No unmapped FRs; no placeholders (TODO/TBD) Ready for /speckit.implement or further iteration. --- .specify/memory/constitution.md | 195 +---- .specify/plan.md | 75 +- .specify/spec.md | 50 +- .specify/tasks.md | 1131 ++++++++++++++++++---------- specs/001-rbac-onboarding/plan.md | 102 +++ specs/001-rbac-onboarding/spec.md | 709 +++++++++++++++++ specs/001-rbac-onboarding/tasks.md | 937 +++++++++++++++++++++++ specs/002-filament-json/plan.md | 107 +++ specs/002-filament-json/spec.md | 118 +++ specs/002-filament-json/tasks.md | 250 ++++++ 10 files changed, 3111 insertions(+), 563 deletions(-) create mode 100644 specs/001-rbac-onboarding/plan.md create mode 100644 specs/001-rbac-onboarding/spec.md create mode 100644 specs/001-rbac-onboarding/tasks.md create mode 100644 specs/002-filament-json/plan.md create mode 100644 specs/002-filament-json/spec.md create mode 100644 specs/002-filament-json/tasks.md diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index a4438da..a4670ff 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -1,175 +1,50 @@ - - -# TenantPilot Constitution +# [PROJECT_NAME] Constitution + ## Core Principles -### I. Safety-First Operations +### [PRINCIPLE_1_NAME] + +[PRINCIPLE_1_DESCRIPTION] + -Every destructive or high-impact action involving Intune configurations MUST implement safety mechanisms: -- **Explicit confirmation UI** for restore, rollback, and destructive operations -- **Dry-run/preview modes** where technically feasible, showing clear change summaries before execution -- **Validation gates** detecting conflicts, incompatible states, or invalid inputs -- **Audit log entries** for all critical operations (backup creation, restore execution, policy rollback) +### [PRINCIPLE_2_NAME] + +[PRINCIPLE_2_DESCRIPTION] + -**Rationale**: Intune configurations are critical production assets. A single misconfigured policy can affect thousands of devices. Safety gates prevent operational accidents and provide recovery visibility. +### [PRINCIPLE_3_NAME] + +[PRINCIPLE_3_DESCRIPTION] + -### II. Immutable Versioning +### [PRINCIPLE_4_NAME] + +[PRINCIPLE_4_DESCRIPTION] + -Policy versions MUST be stored as immutable snapshots: -- **Full payload capture** in JSONB with metadata (who, when, source tenant, policy type) -- **No in-place modifications** to version records after creation -- **Queryable history** by policy, time range, and actor -- **Diff capabilities** between any two versions (human-readable summary + structured JSON where feasible) +### [PRINCIPLE_5_NAME] + +[PRINCIPLE_5_DESCRIPTION] + -**Rationale**: Immutable versions provide reliable rollback targets, accurate audit trails, and trustworthy diff outputs. Mutable history undermines auditability and introduces rollback risks. +## [SECTION_2_NAME] + -### III. Defensive Restore +[SECTION_2_CONTENT] + -Restore operations MUST be defensive and transparent: -- **Preview/dry-run mode** showing what changes will be applied before execution -- **Selective restore** allowing granular control over which items to restore -- **Conflict detection** flagging existing resources that may be overwritten or incompatible -- **Pre-execution summary** clearly communicating scope, risks, and affected resources -- **Explicit confirmation** required before executing any restore +## [SECTION_3_NAME] + -**Rationale**: Restore operations can overwrite production configurations. Defensive workflows reduce risk of unintended changes and provide administrators with informed control. - -### IV. Auditability - -All critical operations MUST produce comprehensive audit logs: -- **Scope**: backup creation, restore execution/attempts, policy rollback, policy version creation -- **Content**: actor, timestamp, operation type, affected resources, outcome (success/failure/partial) -- **Tenant-scoped**: logs MUST be queryable per tenant for multi-tenant future support -- **RBAC-respecting**: log access follows same permission model as operational access -- **High-level Graph logging**: log Graph API calls without exposing secrets or sensitive payloads - -**Rationale**: Audit trails enable compliance verification, incident investigation, and operational transparency. They are non-negotiable for enterprise configuration management. - -### V. Tenant-Aware Architecture - -Data models and business logic MUST be tenant-scoped from day one: -- **Tenant entity** present in schema even if v1 deploys single-tenant -- **Foreign key relationships** reference tenant_id where applicable -- **Service layer logic** accepts tenant context explicitly -- **Isolation enforcement** ensures no cross-tenant data leakage in queries or operations - -**Rationale**: TenantPilot v1 is single-tenant per deployment, but the architecture must support multi-tenant evolution. Retrofitting tenant isolation later is expensive and error-prone. - -### VI. Graph Abstraction - -Microsoft Graph integration MUST be isolated behind a dedicated abstraction layer: -- **Domain services** depend on `GraphClient` interface, not raw Graph SDK -- **Responsibilities**: auth token handling, rate-limit-friendly batching, standardized error mapping -- **No direct Graph calls** in controllers, Filament resources, or domain logic -- **Testability**: Graph layer must be mockable for integration tests - -**Rationale**: Graph API complexity (auth, rate limits, versioning, error handling) should not bleed into domain logic. Abstraction enables cleaner testing, easier SDK upgrades, and centralized rate limit management. - -### VII. Spec-Driven Development - -All features MUST follow the Spec Kit workflow: -1. **Read** `.specify/constitution.md` (this document) -2. **Create/update** `.specify/spec.md` defining user stories, acceptance criteria, and requirements -3. **Produce** `.specify/plan.md` with technical design, structure decisions, and constitution check -4. **Break down** into `.specify/tasks.md` organized by independently testable user stories -5. **Implement** in small, reviewable PRs aligned with tasks - -**Non-negotiable constraints**: -- **Constitution check** in plan.md MUST pass before implementation -- **User stories** in spec.md MUST be prioritized and independently testable -- **Requirements changes** during implementation MUST update spec/plan before continuing -- **Tasks** MUST be organized by user story to enable incremental delivery - -**Rationale**: Spec Kit enforces thoughtful design, prevents scope drift, and maintains alignment between requirements, design, and implementation. It provides checkpoints for validation before costly implementation work. - -## Security & Permissions - -- **Least privilege**: Graph scopes and app permissions MUST request only necessary access -- **Role-based access control**: Admin vs. read-only auditor roles (v1 baseline) -- **No secrets in code**: Use Laravel encrypted storage or environment-based secret management -- **Tenant identifier validation**: All Graph operations MUST validate tenant context -- **Encrypted storage**: Sensitive fields (tokens, credentials) MUST use Laravel encryption where stored - -## Technology Stack - -**Core Stack**: -- Backend: **Laravel** (latest stable) -- Admin UI: **Filament** (v3+) -- Database: **PostgreSQL** (via Sail locally, managed service in production) -- Auth: **Microsoft Identity** (Entra ID/Azure AD integration) -- External API: **Microsoft Graph** (Intune endpoints) - -**Development Environment**: -- Local: **Laravel Sail** (Docker-based, PostgreSQL container) -- Tooling: **Drizzle** for local PostgreSQL workflows (if configured) -- Testing: **Pest** (PHPUnit-based) - -**Deployment**: -- Repository: **Gitea** (self-hosted) -- Deployment: **Dokploy** on VPS (container-based) -- Environments: **Staging** (mandatory validation gate) → **Production** - -**Constraints**: -- PHP: **PSR-12** conventions -- Migrations: **Reversible**, validated on Staging before Production -- JSONB storage: Use for raw Graph payloads, policy snapshots, backup items -- Indexing: **GIN indexes** on JSONB fields requiring search/filter +[SECTION_3_CONTENT] + ## Governance + -This constitution supersedes all other development practices and guidelines. It defines the non-negotiable principles for TenantPilot development. +[GOVERNANCE_RULES] + -**Amendment Process**: -- Amendments require documentation in this file with version bump rationale -- Version follows **semantic versioning**: - - **MAJOR**: Backward-incompatible governance changes (principle removal/redefinition) - - **MINOR**: New principles added or materially expanded guidance - - **PATCH**: Clarifications, wording improvements, non-semantic refinements -- Constitution changes MUST propagate to affected templates and guidance files -- All templates in `.specify/templates/` MUST align with active principles - -**Compliance Enforcement**: -- All specs MUST include a "Constitution Check" section in plan.md -- All PRs MUST verify compliance with relevant principles -- Violations MUST be justified in plan.md Complexity Tracking section -- Runtime development guidance lives in `Agents.md`, which MUST align with this constitution - -**Ratification**: This constitution was established to formalize TenantPilot's architectural and operational principles based on v1 specification requirements and the Spec Kit workflow. - -**Version**: 1.0.0 | **Ratified**: 2025-12-10 | **Last Amended**: 2025-12-10 +**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE] + diff --git a/.specify/plan.md b/.specify/plan.md index 4c5bd0b..77109ae 100644 --- a/.specify/plan.md +++ b/.specify/plan.md @@ -5,11 +5,12 @@ # Implementation Plan: TenantPilot v1 **Spec Source**: `.specify/spec.md` (scope/restore matrix unchanged) ## Summary -TenantPilot v1 already delivers tenant-scoped Intune inventory, immutable backups, version history with diffs, defensive restore flows, tenant setup/permissions health, settings normalization/display, and Highlander enforcement. Remaining priority work is the delegated Intune RBAC onboarding wizard. All Graph calls stay behind the abstraction with audit logging; snapshots remain JSONB with safety gates for high-risk types (preview-only). +TenantPilot v1 already delivers tenant-scoped Intune inventory, immutable backups, version history with diffs, defensive restore flows, tenant setup, permissions/health, settings normalization/display, and Highlander enforcement. Remaining priority work is the delegated Intune RBAC onboarding wizard (US7) and afterwards the Graph Contract Registry & Drift Guard (US8). All Graph calls stay behind the abstraction with audit logging; snapshots remain JSONB with safety gates (preview-only for high-risk types). ## Status Snapshot (tasks.md is source of truth) -- **Done**: US1 inventory, US2 backups, US3 versions/diffs, US4 restore preview/exec, scope config, soft-deletes/housekeeping, Highlander single current tenant, tenant setup & verify (US6), permissions/health overview (US7), table ActionGroup UX, settings normalization/display (US1b), Dokploy/Sail runbooks. -- **Next up**: US8 (formerly labeled “User Story 7” in spec) Intune RBAC onboarding wizard (delegated, synchronous Filament flow). +- **Done**: US1 inventory, US2 backups, US3 versions/diffs, US4 restore preview/exec, scope config, soft-deletes/housekeeping, Highlander single current tenant, tenant setup & verify (US6), permissions/health overview (US6), table ActionGroup UX, settings normalization/display (US1b), Dokploy/Sail runbooks. +- **Next up**: **US7** Intune RBAC onboarding wizard (delegated, synchronous Filament flow). +- **Upcoming**: **US8** Graph Contract Registry & Drift Guard (contract registry, type-family handling, verification command, fallback strategies). ## Technical Baseline - Laravel 12, Filament 4, PHP 8.4; Sail-first with PostgreSQL. @@ -25,29 +26,63 @@ ## Completed Workstreams (no new action needed) - **US3 Versions/Diffs (Phase 5)**: Version capture, timelines, human+JSON diffs, soft-deletes with audit. - **US4 Restore (Phase 6)**: Preview, selective execution, conflict warnings, per-type restore level (enabled vs preview-only), PowerShell decode/encode respected, audit of outcomes. - **US6 Tenant Setup & Highlander (Phases 8 & 12)**: Tenant CRUD/verify, INTUNE_TENANT_ID override, `is_current` unique enforcement, “Make current” action, block deactivated tenants. -- **US7 Permissions/Health (Phase 9)**: Required permissions list, compare/check service, Verify action updates status and audit, permissions panel in Tenant detail. -- **US1b Settings Display (Phase 13)**: PolicyNormalizer + SnapshotValidator, warnings for malformed/@odata mismatches, normalized settings and pretty JSON on policy/version detail, list badges, README section. +- **US6 Permissions/Health (Phase 9)**: Required permissions list, compare/check service, Verify action updates status and audit, permissions panel in Tenant detail. +- **US1b Settings Display (Phase 13)**: PolicyNormalizer + SnapshotValidator, warnings for malformed snapshots, normalized settings and pretty JSON on policy/version detail, list badges, README section. - **Housekeeping/UX (Phases 10–12)**: Soft/force deletes for tenants/backups/versions/restore runs with guards; table actions in ActionGroup per UX guideline. - **Ops (Phase 7)**: Sail runbook and Dokploy staging→prod guidance captured. -## Next Up: US8 Intune RBAC Onboarding Wizard (delegated, synchronous) -- Entry: Tenant detail ActionGroup “Setup Intune RBAC”; gated to active tenants with `app_client_id`. -- Flow: explain/preconditions (role/scope/group mode, least-privilege warning), delegated login, synchronous execution in Filament (no queue for grant), post-check via Verify + canary reads. - - Canary reads (read-only): `GET /deviceManagement/deviceConfigurations?$top=1` and `GET /deviceManagement/deviceCompliancePolicies?$top=1` (and `GET /identity/conditionalAccess/policies?$top=1` only if CA is enabled for the tenant/scope). -- Execution steps (idempotent): resolve service principal; ensure/create security group; add SP member; create/update role assignment with chosen scope; log audit for start/login/group/member/assignment/verify. -- Optional jobs/CLI limited to CHECK/REPORT only (no grant). -- Tests: happy path, rerun idempotent, missing permissions error mapping, scope-limited warning. -- Documentation: add wizard behavior, audit expectations, and least-privilege guidance once implemented. -- Operational note: After admin-consent or RBAC changes, force a fresh token acquisition (e.g., clear app token cache) before re-trying sync/backup/restore; Verify should run with a non-stale token. -- Note: This is **Intune RBAC** for the **Enterprise App (service principal)**. No “App roles” need to be added in the App Registration; Graph API permissions + Intune role assignment are separate concerns. +## Execution Plan: US7 Intune RBAC Onboarding Wizard (Phase 14) + +- Objectives: deliver delegated, tenant-scoped wizard that safely converges the Intune RBAC state for the configured service principal; fully audited, idempotent, least-privilege by default. +- Scope alignment: FR-023–FR-030, constitution (Safety-First, Auditability, Tenant-Aware, Graph Abstraction). No secret/token persistence; delegated tokens stay request-local and are not stored in DB/cache. +- Design decisions: + - Service: `RbacOnboardingService` orchestrates steps using `GraphClientInterface`; reuse `RbacHealthService` for verification; all calls through abstraction with error mapping. +- Data: use existing tenant RBAC columns (`rbac_group_id`, `rbac_group_name`, `rbac_role_assignment_id`, `rbac_role_key`, `rbac_scope_mode`, `rbac_scope_id`, status fields). No new entities; ensure casts + guards. +- Audit: log start, delegated login outcome, group ensure, membership ensure, role assignment ensure/update, verify results. No payload logging; only IDs/status codes. +- Wizard flow (Filament, Tenant detail ActionGroup): + 1) Preconditions/config step with review screen: show tenant/app info, required permissions, least-privilege warning; inputs for role (default Policy/Profile Manager; Intune Administrator shows warning), scope (global default; optional group picker), group mode (create default `TenantPilot-Intune-RBAC` vs pick existing security-enabled group). Summarize planned changes before proceeding. + 2) Delegated auth step: initiate login; on failure stop with actionable message + audit; do not store token beyond request. + 3) Execute (synchronous): resolve service principal by `app_client_id`; on missing SP stop with consent-required hint + audit reason `sp_not_found`; ensure/create security group (validate `securityEnabled=true`); ensure SP membership (idempotent “already exists” OK); ensure/create/patch Intune role assignment for chosen role/scope; persist discovered IDs on tenant for idempotency. + 4) Post-verify: force fresh token acquisition; run canary reads (deviceConfigurations, deviceCompliancePolicies, conditionalAccess if enabled); update RBAC/permission health; surface warnings if scope-limited; audit verify result. + 5) Summary: show IDs (group, role assignment), role/scope used, verify status, CTA to retry policy sync. +- UX rules: action only for active tenants with `app_client_id`; keep in ActionGroup with Admin consent/Verify; show badge/hint if RBAC missing; warnings on selecting Intune Administrator role; block execution if tenant inactive or missing consent/SP. +- Safety/idempotency: handle “already exists” as success; no self-heal jobs; retry-safe writes; no queue usage to avoid token expiry; timeouts surfaced clearly; no delegated token persistence. +- Tests: happy path, rerun idempotent, SP missing, insufficient privileges, non-security-enabled group failure, scope-limited warning, delegated auth failure path; Filament wizard visibility + summary rendering; health prompts to run wizard when RBAC missing. +- Documentation: add wizard behavior, least-privilege defaults, audit expectations, “no token storage”, and how to rerun safely; note CTA to retry policy sync. +- Operational note: After admin-consent or RBAC changes, force a fresh token acquisition (e.g., clear app token cache) before re-trying sync/backup/restore; Verify should run with a non-stale token. Optional CHECK/REPORT jobs only (no grant) remain out-of-scope for this phase. +- Testing plan (Pest): + - Service unit tests: happy path, rerun idempotent, SP missing, insufficient privileges, scope-limited warning, group exists/not security-enabled failure. + - Filament feature: wizard visibility gating, delegated failure path, successful run shows summary and updates health, warnings rendered. + - Health integration: Verify reflects RBAC status and prompts to run wizard when missing. +- Deployment/ops: no new env vars; ensure migrations for tenant RBAC columns are applied; run targeted tests `php artisan test tests/Unit/RbacOnboardingServiceTest.php tests/Feature/Filament/TenantRbacWizardTest.php`; Pint on touched files. + +## Upcoming: US8 Graph Contract Registry & Drift Guard (Phase 15) + +- Objectives: centralize Graph contract assumptions per supported type/endpoint and provide drift detection + safe fallbacks so preview/restore remain stable on Graph shape/capability changes. +- Scope alignment: FR-031–FR-034 (spec), constitution (Safety-First, Auditability, Graph Abstraction, Tenant-Aware). +- Approach: + - Artifact: `config/graph_contracts.php` (or similar) with per-type contract data: + - resource paths (collection + single item) + - allowed `$select` / allowed `$expand` + - **type families / allowed `@odata.type` values** + - create/update methods, id field + - hydration strategy (member expansion vs follow-up fetch vs unavailable) + - Service: registry + checker; integrate with Graph client to enforce allowed capabilities and downgrade on capability errors (retry without expands/selects), recording warnings/audit entries. + - Type families: treat derived `@odata.type` values **within a declared family** as compatible (no `odata_mismatch`) for routing preview/restore. + - Verification: `php artisan graph:contract:check` (staging/CI) to probe endpoints and surface actionable diffs when Graph changes; opt-in/guarded for prod. + - Docs: explain registry format and update process when Graph changes. +- Testing outline: unit for registry lookups/type-family matching/fallback selection; integration/Pest to simulate capability errors and ensure downgrade path + correct routing for derived types. ## Testing & Quality Gates -- Continue using targeted Pest runs per change set; add/extend tests for US8 accordingly. +- Continue using targeted Pest runs per change set; add/extend tests for US7 wizard now, and for US8 contracts when implemented. - Run Pint on touched files before finalizing. -- Maintain tenant isolation, audit logging, and restore safety gates; validate @odata.type and malformed snapshots prior to restore execution. -- Safety gate: `@odata.type` mismatches MUST block restore execution (preview may still show details + warnings), to prevent applying payloads to the wrong policy type/platform. +- Maintain tenant isolation, audit logging, and restore safety gates; validate snapshot shape and type-family compatibility prior to restore execution. + +### Restore Safety Gate +- Restore execution MUST be blocked if a snapshot’s `@odata.type` is **outside** the declared **type family** for the target policy type (prevent cross-type/platform restores). +- Restore preview MAY still render details + warnings for out-of-family snapshots, but MUST NOT offer an apply action. ## Coordination -- Update `.specify/tasks.md` to reflect progress on remaining US8 tasks; no new entities or scope changes introduced here. +- Update `.specify/tasks.md` to reflect progress on US7 wizard and future US8 contract tasks; no new entities or scope changes introduced here. - Stage validation required before production for any migration or restore-impacting change. -- Keep Graph integration behind abstraction; no secrets in logs; follow existing UX patterns (ActionGroup, warnings for risky ops). +- Keep Graph integration behind abstraction; no secrets in logs; follow existing UX patterns (ActionGroup, warnings for risky ops). \ No newline at end of file diff --git a/.specify/spec.md b/.specify/spec.md index f07ed90..3b86243 100644 --- a/.specify/spec.md +++ b/.specify/spec.md @@ -57,6 +57,11 @@ ## Scope name: "Applications (Metadata only)" graph_resource: "deviceAppManagement/mobileApps" notes: "Backup nur von Metadaten/Zuweisungen (kein Binary-Download in v1)." + + - key: settingsCatalogPolicy + name: "Settings Catalog Policy" + graph_resource: "deviceManagement/configurationPolicies" + notes: "Intune Settings Catalog Policies liegen NICHT unter deviceConfigurations, sondern unter configurationPolicies. v1 behandelt sie als eigenen Typ." restore_matrix: deviceConfiguration: @@ -112,6 +117,12 @@ ## Scope restore: enabled risk: high notes: "Security-relevante Einstellungen (z. B. Credential Guard); Preview + klare Konflikt-Warnungen nötig." + + settingsCatalogPolicy: + backup: full + restore: enableds + risk: medium + notes: "Settings Catalog Policies sind Standard-Config-Policies (Settings Catalog). Preview/Audit Pflicht; Restore automatisierbar." mobileApp: backup: metadata-only @@ -134,7 +145,7 @@ ### User Story 1 - Policy inventory listing (Priority: P1) 1. **Given** an authenticated admin, **When** they open the Policies list, **Then** they see supported object types with identifiers, type/category, platform, and last-updated metadata. 2. **Given** filtering by type/category, **When** the admin selects a type, **Then** only matching objects appear and the view remains tenant-scoped. - +3. **Given** Settings Catalog Policies exist in Intune, **When** the admin opens the Policies list and syncs, **Then** Settings Catalog Policies are listed as type `Settings Catalog Policy` (settingsCatalogPolicy) and are not mixed into `Device Configuration`. --- ### User Story 1b - Policy detail shows readable settings (Priority: P1) @@ -264,6 +275,43 @@ ### User Story 6 - Berechtigungsübersicht & Health-Status (Priority: P1) 1. **Given** a fresh checkout, **When** Sail commands run (`./vendor/bin/sail up -d`, `./vendor/bin/sail artisan migrate`), **Then** the app boots with PostgreSQL and Filament admin available. 2. **Given** a pending release, **When** migrations and restore flows are validated on staging, **Then** production deployment proceeds with documented steps and environment parity. + ### User Story 8 – Graph Contract Registry & Drift Guard (Priority: P1) +Admin soll sich darauf verlassen können, dass Backup/Restore/Preview nicht wegen Graph-Shape-Details (derived @odata.type, verbotene $expand/$select, Property-Abweichungen) “random” bricht. + +Acceptance Scenarios: + 1. Given ein Backup enthält @odata.type = #microsoft.graph.windows10CompliancePolicy, +When Preview/Restore läuft, +Then wird das als gültiger deviceCompliancePolicy-Family Typ behandelt (kein odata_mismatch), und der korrekte Endpoint/Method wird genutzt. + 2. Given ein Endpoint erlaubt bestimmte Expands/Selects nicht, +When TenantPilot Requests baut, +Then werden nur “allowed capabilities” verwendet (kein 400 durch OData parsing). + 3. Given Microsoft/Intune ändert Shape/Capabilities, +When graph:contract:check läuft, +Then schlägt der Check kontrolliert fehl und zeigt welcher Contract angepasst werden muss (statt dass Prod-Flows brechen). + +2) Neue Functional Requirements (FR-03x) ergänzen + +Beispiel, passend zu deinem Stil: + • FR-031: System MUST maintain a central Graph Contract Registry per supported type/endpoint (resource path, allowed $select, allowed $expand, “type family” / allowed @odata.type values, create/update methods). + • FR-032: Restore/Preview MUST treat derived @odata.type values as compatible within a declared type family (e.g. compliance policy family), and MUST NOT hard-fail on base-vs-derived mismatches. + • FR-033: System MUST provide a verification command (e.g. php artisan graph:contract:check) that validates registry assumptions against live Graph behavior (at least via canary calls / lightweight probes), logging actionable diffs. + • FR-034: When Graph returns capability errors (OData select/expand, unsupported features), system MUST downgrade to a safe fallback strategy (e.g. “no expand, extra fetches”) and MUST record a warning/audit entry. + +(Du kannst FR-033 “live check” auch optional machen für prod, aber mindestens in CI/Staging wertvoll.) + +3) Implementation Notes / Data Artefacts ergänzen + +Ein kleines, versioniertes Artefakt einführen, z. B.: + • config/graph_contracts.php oder .specify/contracts/graph.yaml + +Darin pro Objekt-Typ: + • resource (collection + single-item path) + • type_family (Liste erlaubter @odata.type) + • allowed_select / allowed_expand + • member_hydration_strategy (z. B. “property array” vs “subresource” vs “not available”) + • create_method / update_method / id_field + +Das verhindert “Wissens-Leaks” quer durch Services. ### Edge Cases - Graph permissions missing or expired, causing policy fetch/restore failures with clear error mapping and audit entries. diff --git a/.specify/tasks.md b/.specify/tasks.md index c66173b..f539b55 100644 --- a/.specify/tasks.md +++ b/.specify/tasks.md @@ -1,5 +1,4 @@ --- - description: "Task list for TenantPilot v1 implementation" --- @@ -9,8 +8,11 @@ # Tasks: TenantPilot v1 **Prerequisites**: plan.md (complete), spec.md (complete) **Status snapshot** -- Done: Phases 1–13 (US1–US4, Settings normalization/display, Highlander, permissions/health, housekeeping/UX, ops) -- Next up: Phase 14 (US8) delegated Intune RBAC onboarding wizard (synchronous) +- Done: Phases 1–13 (US1–US4, 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) @@ -30,6 +32,669 @@ ## 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 2–3 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 `
` mit:
+    - `class="max-h-[520px] overflow-auto rounded-lg border bg-gray-50 p-3 text-xs font-mono leading-relaxed"`
+  - optional: “Expand” action (modal/slideOver) für Vollbildansicht.
+
+### 3) Normalized settings tables: horizontal scroll + readable columns
+- In `resources/views/filament/infolists/entries/normalized-settings.blade.php`
+  - Table container:
+    - `class="overflow-x-auto rounded-lg border"`
+  - Table:
+    - `class="min-w-[900px] table-fixed"`
+  - Cells:
+    - Definition/Path: `font-mono text-xs break-all whitespace-normal`
+    - Value: `break-words whitespace-normal`
+  - Column widths:
+    - Definition: `w-[35%]`, Type: `w-[20%]`, Value: `w-[25%]`, Path: `w-[20%]`
+  - Long values: clamp (optional):
+    - `line-clamp-2` + “Show more” (details/modal)
+
+### 4) Optional: Search within Settings (nice-to-have)
+- Add a small client-side filter input (Alpine) above settings table:
+  - filters rows by DefinitionId/Value/Path
+- Keep it optional if you want minimal change in v1.
+
+---
+
+## Tests (Pest)
+
+### Feature: `tests/Feature/Filament/PolicyVersionReadableLayoutTest.php`
+- Given a `settingsCatalogPolicy` version with long `settings` payload
+- Assert:
+  - Tabs render (Normalized + Raw JSON)
+  - Raw JSON container has max-height/overflow classes
+  - Normalized table wrapper uses overflow-x
+  - Page does not contain extremely long unbroken lines without wrappers (basic assertion on classes)
+
+---
+
+## Verification
+
+- `./vendor/bin/pest tests/Feature/Filament/PolicyVersionReadableLayoutTest.php`
+- `./vendor/bin/pint --dirty`
+
+---
+
+## Acceptance Criteria
+
+- Policy Version page is readable on normal screen widths:
+  - Normalized settings are immediately visible without scrolling past raw JSON
+  - Raw JSON is accessible in second tab and scrolls inside its container
+  - Settings table does not break layout; long IDs/paths wrap/scroll cleanly
+- No regressions for other policy types (deviceConfiguration/compliance/scripts).
+
+
+
+- [x] T184 [US1b][UX] Use Filament Tables for Settings Catalog settings (Policy + Version) with responsive layout + SlideOver details
+
+- **Goal:** `settingsCatalogPolicy` Settings sollen **lesbar, scannbar und bedienbar** sein:
+  - als echte **Filament Table** (nicht “pseudo table” im Infolist-Blade)
+  - mit **truncate + tooltip**, horizontal scroll, sticky header
+  - mit **Details** (SlideOver) + Copy pro Row
+  - identisch nutzbar in **Policy Detail** und **Policy Version Detail**
+
+- **Why:** Die aktuelle Darstellung bricht Layout/Spaltenbreiten (Definition/Type/Value laufen ineinander). Filament Tables lösen genau diese Probleme (fixed layout, responsive, actions, search).
+
+---
+
+## Implementation
+
+### 1) Introduce reusable Livewire component for Settings Catalog settings table
+- New: `app/Livewire/SettingsCatalogSettingsTable.php`
+  - Props:
+    - `array $settingsRows` (aus PolicyNormalizer Output oder direkt aus Snapshot `settings`)
+    - `string $context` (`policy|version`) optional
+  - Intern: build a Filament `Table` with columns:
+    - **Definition** (`TextColumn::make('definition')`)
+      - `wrap(false)`, `searchable()`, `tooltip(fn($state) => $state)`, `limit(60)`
+    - **Type** (`TextColumn::make('type')`)
+      - `wrap(false)`, `toggleable()`, `limit(50)`, `tooltip(...)`
+    - **Value** (`TextColumn::make('value')`)
+      - `wrap(false)`, `limit(60)`, `tooltip(...)`
+      - render `(group)` badge for group rows
+    - **Path** (`TextColumn::make('path')`)
+      - `toggleable(isToggledHiddenByDefault: true)`, `limit(80)`, `tooltip(...)`
+  - Table config:
+    - `paginated([25, 50, 100])` (default 25)
+    - `searchPlaceholder('Search definition/value…')`
+    - `striped()`, `deferLoading()`
+  - Row Action:
+    - `Action::make('details')->label('Details')->icon('heroicon-m-eye')`
+    - opens **SlideOver**
+      - shows full Definition/Type/Value/Path + optional raw setting JSON (pretty)
+      - Copy buttons for Definition/Value
+
+### 2) Embed component via ViewEntry in Policy + PolicyVersion detail
+- Policy detail (`app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php`)
+  - For `settingsCatalogPolicy`:
+    - render `SettingsCatalogSettingsTable` (instead of current table block)
+    - pass rows from Normalizer (`normalize()` should expose a stable rows array)
+- PolicyVersion detail (`app/Filament/Resources/PolicyVersionResource/Pages/ViewPolicyVersion.php`)
+  - same embedding for `settingsCatalogPolicy`
+
+> Rule: Nur für `settingsCatalogPolicy` auf Table UI umstellen. Andere Typen bleiben Infolist/KeyValue.
+
+### 3) Tailwind/Filament styling guardrails (no layout break)
+- Ensure table container is responsive:
+  - wrap table in `div class="overflow-x-auto"`
+  - set columns non-wrapping by default (truncate)
+- Sticky header:
+  - enable sticky header in table (Filament supports sticky header via table wrapper CSS; if needed add a small CSS utility class in your Filament theme)
+
+### 4) Normalizer output contract (stable)
+- Ensure `PolicyNormalizer` returns for settingsCatalogPolicy:
+  - `['settings_table' => ['columns' => [...], 'rows' => [...]]]`
+  - rows fields: `definition`, `type`, `value`, `path`, `raw` (optional)
+- Table uses **rows**, not parsing raw snapshot again (single source).
+
+---
+
+## Tests (Pest)
+
+### Feature: `tests/Feature/Filament/SettingsCatalogSettingsTableRenderTest.php` (new)
+- Create a policy + version with hydrated `settings`
+- Visit Policy detail and PolicyVersion detail
+- Assert:
+  - table headers visible (Definition/Type/Value)
+  - at least one known definition appears
+  - “Details” action exists
+
+### Unit (optional): `tests/Unit/PolicyNormalizerSettingsCatalogRowsTest.php`
+- Given snapshot with nested settings instances
+- Assert normalizer returns rows with `definition/type/value/path`
+
+---
+
+## Verification
+
+- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogSettingsTableRenderTest.php`
+- `./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogRowsTest.php`
+- `./vendor/bin/pint --dirty`
+
+---
+
+## Acceptance Criteria
+
+- In **Policy Detail** und **Policy Version Detail** sind Settings Catalog Settings als **Filament Table** sichtbar (lesbar, nicht überlappend).
+- Lange Werte sind **truncated** aber per Tooltip/Details vollständig erreichbar.
+- Pro Row gibt es **Details SlideOver** + Copy.
+- Kein Layout-Bruch auf typischen Screenbreiten (Laptop/FullHD).
+
+
+
+- [ ]T185 [UX][US1b][settingsCatalogPolicy] Make Settings Catalog settings readable (label/value parsing + table ergonomics)
+
+- **Goal:** Settings Catalog Policies sollen im Policy/Version Detail **für Admins lesbar** sein, ohne dass wir “alle Settings kennen müssen”.
+  - Tabelle zeigt **sprechende Bezeichnung** + **kompakte Werte**
+  - Lange IDs bleiben verfügbar (Tooltip/Copy/Details), aber dominieren nicht die UI
+
+- **Why:** Aktuell sind `definitionId` und `choiceSettingValue.value` so lang, dass sie in der Tabelle abgeschnitten werden und der Admin weder Setting noch Wert versteht.
+
+---
+
+## Implementation
+
+### 1) Presentation layer: generate human-friendly labels (no registry needed)
+- Add helper in `PolicyNormalizer` (oder kleiner `SettingsCatalogPresenter`):
+  - `labelFromDefinitionId(string $definitionId): string`
+    - remove common prefixes: `device_vendor_msft_`, `user_vendor_msft_`, `policy_config_`, `admx_`
+    - replace `_` with spaces
+    - keep last 2–4 segments if string is huge
+    - replace `{tenantid}` with `{tenant}`
+  - Output example:
+    - `user_vendor_msft_policy_config_admx_desktop_nomydocumentsico...` → `Desktop: No My Documents Icon` (heuristic), fallback: last segments nicely spaced
+
+> Heuristic only. If no good split possible, fallback to “last segments” label.
+
+### 2) Parse values into a short “effective value”
+- Implement `valuePreview(array $settingInstance): string`:
+  - For `SimpleSettingValue`: return scalar (`12`, `0`, `true/false`)
+  - For `ChoiceSettingValue.value`: return last token after last `_` OR map known boolean patterns:
+    - suffix `_true`/`_false` → `True`/`False`
+    - suffix `_0`/`_1` for allowed/blocked → show `0`/`1` but also tag `Allowed/Blocked` if detectable
+  - For group instances: show `(group)` and put children into details view only
+
+### 3) Improve table ergonomics (Filament Table / Livewire)
+- In `SettingsCatalogSettingsTable`:
+  - Columns:
+    1) **Setting** (human label) + small muted secondary line showing truncated definitionId
+    2) **Value** (valuePreview)
+    3) **Type** (badge: Choice/Simple/Group)
+    4) Optional: **Path** (toggleable, hidden by default)
+  - Add:
+    - `->searchable()` should search both label + raw definitionId + raw value
+    - `->wrap()` / `->limit()` for long strings
+    - tooltips showing full definitionId/value on hover
+    - “Copy” icon action in row details (SlideOver) for Definition + Raw JSON
+  - Ensure horizontal scroll only inside table container:
+    - wrapper `div` with `overflow-x-auto` + `max-w-full`
+    - table layout fixed where possible (`table-fixed`) to prevent column blowouts
+
+### 4) Keep Raw JSON accessible but not primary
+- In PolicyVersion view:
+  - Put Raw JSON into collapsible section or separate tab.
+  - Normalized Settings tab becomes default for settingsCatalogPolicy.
+
+---
+
+## Tests (Pest)
+
+### Unit: `tests/Unit/SettingsCatalogPresenterTest.php`
+- labelFromDefinitionId() produces readable output and stable fallback
+- valuePreview() returns expected previews for:
+  - choice true/false, numeric, group
+
+### Feature: `tests/Feature/Filament/SettingsCatalogSettingsTableUsabilityTest.php`
+- Render policy detail with one very long definition + long choice value
+- Assert:
+  - label column shows shortened readable label (not the full raw string)
+  - value column shows preview (e.g., `True`, `12`, `Never`)
+  - details slide-over contains full raw definition/value + copy UI
+
+---
+
+## Verification
+- `./vendor/bin/pest tests/Unit/SettingsCatalogPresenterTest.php tests/Feature/Filament/SettingsCatalogSettingsTableUsabilityTest.php`
+- `./vendor/bin/pint --dirty`
+
+---
+
+## Acceptance Criteria
+- In Policy Detail, Settings table shows:
+  - **Readable Setting name** (not a cut-off vendor string)
+  - **Readable Value preview** (True/False/12/etc.)
+- Full raw definitionId and raw value remain accessible via tooltip and SlideOver + copy button.
+- No layout overflow/broken columns on common laptop viewport widths.
+
+
 
 ### Implementation for User Story 1
 
@@ -47,6 +712,7 @@ ### 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.
@@ -58,6 +724,7 @@ ### Implementation for User Story 2
   - 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)
@@ -85,422 +752,122 @@ ### 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)
 
-### Implementation for User Story 5
-
 - [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: User Story 6 - Tenant hinzufügen & Entra ID App-Setup (Priority: P1)
 
-- [x] T030 [US6] Migration für `tenants` ergänzen/prüfen:
-  - Felder: `name`, `tenant_id` (GUID), `domain`, `app_client_id`, `app_status`, `app_notes`,
-    `created_at`, `updated_at`.
-  - Optional: Felder für Secret/Certificate-Config (verschlüsselt), falls benötigt.
+## Phase 8: Tenant Management (Tenant hinzufügen, App-Setup, Verify) (Priority: P1)
 
-- [x] T031 [US6] Eloquent Model `Tenant`:
-  - Beziehungen zu `policies`, `backup_sets`, `restore_runs`, `policy_versions`, `audit_logs`
-    über `tenant_id`.
-  - Tenant-aware Scopes, falls vorhanden (z. B. `forTenant()`).
+> Hinweis: Diese Phase ist “Tenant Management” und **nicht** US6, damit US6 sauber “Permissions/Health” bleibt.
 
-- [x] T032 [US6] Filament-Resource `TenantResource`:
-  - Listenansicht: Name, Tenant ID, Domain, App-Status, erstellt/am.
-  - Create/Edit-Form: Name, Tenant ID, Domain, App-Client-ID, optionale Notizen.
-  - Detailseite mit Actions:
-    - „Open in Entra“ (Link zur App/Tenant im Entra-Portal),
-    - optional: „Copy Admin Consent URL“.
+- [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).
 
-- [x] T033 [US6] `TenantConfigService` (oder Erweiterung des Graph-Clients):
-  - Methode `testConnectivity(Tenant $tenant)`: führt einen einfachen Graph-Call aus
-    (z. B. `/organization` oder ähnliches) mit den App-Daten des Tenants.
-  - Rückgabe: DTO/Array mit `success`, `error_message` (falls vorhanden).
+## Phase 9: User Story 6 - Berechtigungsübersicht & Health-Status (Priority: P1)
 
-- [x] T034 [US6] Action „Verify configuration“ in `TenantResource`:
-  - Ruft `testConnectivity()` auf,
-  - setzt `app_status` auf z. B. `ok`, `error` oder `consent_required`,
-  - zeigt eine Filament-Notification mit dem Ergebnis,
-  - schreibt einen Audit-Log-Eintrag (`tenant.config.verified`).
-
-- [x] T035 [US6] Tenant-Kontext in bestehende Services integrieren:
-  - `PolicySyncService`, `BackupService`, `RestoreService` so anpassen,
-    dass sie einen `Tenant` oder `tenant_id` übergeben bekommen
-    und den Graph-Client mit diesem Kontext verwenden.
-  - Sicherstellen, dass alle policy/backup/restore/audit-Datensätze `tenant_id` setzen.
-
-- [x] T036 [US6] Feature-Test `TenantSetupTest`:
-  - Erstellen eines Tenants via Filament (Create-Form).
-  - Aufruf der Action „Verify configuration“ mit gemocktem Graph-Client:
-    - einmal mit erfolgreichem Call → `app_status = ok`,
-    - einmal mit Fehler → `app_status = error` + passende Notification.
-  - Prüfen, dass Audit-Logs geschrieben werden.
-
-- [x] T037 [US6] Admin-Consent Callback Route
-  - Route/Controller, der als `redirect_uri` der Entra-ID-App dient.
-  - Liest `tenant` / `error` / `admin_consent` aus der Query.
-  - Ordnet das dem richtigen `Tenant` zu (z. B. via `state`).
-  - Aktualisiert `app_status` (z. B. `ok`, `error`, `consent_denied`).
-  - Zeigt eine Bestätigungs-/Fehlerseite für den Admin.
----
-
-## Phase 9: User Story 7 - Berechtigungsübersicht & Health-Status (Priority: P1)
-
-- [x] T040 [US7] Zentrale Permissions-Liste anlegen:
-  - `config/intune_permissions.php` mit allen aktuell benötigten Graph-Berechtigungen:
-    - technischer Name (z. B. `DeviceManagementConfiguration.ReadWrite.All`),
-    - Typ: `application` / `delegated`,
-    - kurze Beschreibung,
-    - Feature-Tags (z. B. `["policy-sync", "backup"]`).
-  - Optional: `docs/permissions.md` mit einer Tabelle Feature ↔ Permission als
-    menschlich lesbare Referenz.
-
-- [x] T041 [US7] Datenmodell für Tenant-Berechtigungen:
-  - Variante A (einfach): JSONB-Feld `granted_permissions` in `tenants` (Liste von Permission-Keys).
-  - Variante B (feiner): Tabelle `tenant_permissions` mit
-    `(tenant_id, permission_key, status, last_checked_at)`.
-  - `status` mindestens: `ok`, `missing`, `error`.
-
-- [x] T042 [US7] Service `TenantPermissionService`:
-  - `getRequiredPermissions(): array` – liest aus `config/intune_permissions.php`.
-  - `getGrantedPermissions(Tenant $tenant): array` – liest aus Graph oder aus
-    `tenant_permissions`/`granted_permissions`.
-  - `compare(Tenant $tenant): TenantPermissionStatusDTO` – liefert pro Permission
-    den Status (ok/missing/error) + Gesamthealth.
-
-- [x] T043 [US7] Integration in Tenant-Detail-UI:
-  - Auf der `TenantResource`-Detailseite ein Panel/Section „Permissions“:
-    - Liste aller **required permissions**,
-    - pro Zeile: Name, Typ, Feature-Tags, Status (Icon + Label: OK/fehlt/Fehler).
-  - Optional: Link zu Doku oder Entra-Darstellung (z. B. „How to grant these permissions“).
-
-- [x] T044 [US7] Action „Verify configuration“ erweitern:
-  - Zusätzlich zu `testConnectivity()` auch `TenantPermissionService::compare()` aufrufen.
-  - Ergebnisse in `tenant_permissions`/`granted_permissions` speichern.
-  - `app_status` und Permission-Health aktualisieren.
-  - Audit-Log-Eintrag `tenant.permissions.checked` schreiben.
-
-- [x] T045 [US7] Tests für Permissions:
-  - Unit-Tests für `TenantPermissionService::compare()`:
-    - Szenarien: alle ok, Permission fehlt, Graph-Error.
-  - Feature-Test für Tenant-Detailseite:
-    - required permissions werden angezeigt,
-    - fehlende werden als fehlend markiert,
-    - „Verify configuration“ aktualisiert den Status wie erwartet.
+- [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] Konfiguration `config/tenantpilot.php` auf die in `scope.supported_types` definierten Objekttypen erweitern (type/key, endpoint, label/category, optional risk/restore-Hinweis). Sicherstellen, dass diese Liste die einzige Quelle für Policy-Sync/Backup/Restore ist.
-- [x] T029 [Scope] Filament-UI an neue Typen anpassen: Tabellenfilter/Grouping nach Kategorie (z. B. Config/Compliance/Scripts/Apps/CA), Backup/Restore-Formulare mit Hinweisen zu Restore-Level aus `scope.restore_matrix` (z. B. CA/enrollment restrictions = preview-only).
-
+- [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 machen:
-  - `backup_sets` (und ggf. `backup_items`) Migration/Model mit `SoftDeletes` (deleted_at).
-  - Sicherstellen, dass RestoreRuns keine gelöschten BackupSets verwenden; Delete nur erlauben,
-    wenn keine zugehörigen RestoreRuns existieren.
-
-- [x] T061 [HK] Filament-Delete-Action für BackupSets:
-  - In `BackupSetResource` Delete-Action in List- und/oder Detail-View hinzufügen.
-  - Mit Confirmation-Dialog (“This will archive this backup set and hide it from the UI.”).
-  - Delete disabled/hidden, wenn `restore_runs` für das Set existieren.
-  - Nach Delete Audit-Log (`backup.deleted`) schreiben.
-
-- [x] T062 [HK] PolicyVersions soft deletable machen:
-  - `policy_versions` Migration/Model um `SoftDeletes` erweitern.
-  - Alle Queries und Filament-Resources so lassen, dass standardmäßig nur non-deleted Versions
-    angezeigt werden.
-
-- [x] T063 [HK] Filament-Delete-Action für PolicyVersions:
-  - In `PolicyVersionResource` Delete-Action hinzufügen (List/Detail).
-  - Confirmation + Audit-Log (`policy_version.deleted`).
-
-- [x] T064 [HK] Tests für Housekeeping:
-  - Feature-Test: Löschen eines BackupSets ohne RestoreRun → `deleted_at` gesetzt, UI-Eintrag weg,
-    Audit-Log vorhanden.
-  - Feature-Test: BackupSet mit RestoreRun → Delete-Action nicht verfügbar.
-  - Feature-Test: Löschen einer PolicyVersion → `deleted_at` gesetzt, nicht mehr in List sichtbar.
+- [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 machen:
-  - `tenants` Model um `SoftDeletes` erweitern, Migration ggf. `deleted_at` hinzufügen.
-  - Optional: Feld `status` (enum/string: `active`, `archived`) einführen; beim Delete auf `archived` setzen.
-  - Alle Standard-Queries für Tenants nur `active` / nicht gelöscht anzeigen.
-
-- [x] T071 [HK] Tenant-Delete-Action (Deaktivieren) in `TenantResource`:
-  - Delete-/Archive-Action in der Tenant-Liste und/oder Detailseite hinzufügen.
-  - Deutlich machen: “Deaktiviert diesen Tenant. Historische Daten bleiben vorhanden, neue Aktionen
-    sind nicht mehr möglich.”
-  - Bei Ausführung:
-    - `deleted_at` setzen (und ggf. `status = archived`),
-    - Audit-Log `tenant.deleted` oder `tenant.archived` schreiben.
-
-- [x] T072 [HK] Verhalten für deaktivierte Tenants:
-  - In `PolicySyncService`, `BackupService`, `RestoreService` prüfen, dass nur aktive Tenants
-    verwendet werden; bei deaktiviertem Tenant frühzeitig mit verständlicher Fehlermeldung abbrechen.
-  - In Filament-Navigation Tenants, Policies, Backups, Restores eines deaktivierten Tenants nicht
-    mehr in Standard-Listen anzeigen (es sei denn, es gibt explizite “Show archived”-Filter).
-
-- [x] T073 [HK] (Optional) RestoreRuns soft deletable machen:
-  - `restore_runs` Model/Migration mit `SoftDeletes`.
-  - Delete-Action in `RestoreRunResource` hinzufügen (nur UI-Aufräumung, keine Folgen für Backups).
-  - Audit-Log `restore_run.deleted` schreiben.
-
-- [x] T074 [HK] Tests für Tenant-Delete:
-  - Feature-Test: Tenant löschen/deaktivieren → Tenant taucht nicht mehr in Standardlisten auf,
-    `deleted_at` (und `status`) ist gesetzt, Audit-Event existiert.
-  - Feature-Test: Versuch, mit deaktiviertem Tenant einen Policy-Sync/Backup/Restore zu starten,
-    führt zu einem klaren Fehler (und kein Graph-Call wird ausgeführt).
+- [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 ergänzen:
-  - Filament-Listen für Tenants, BackupSets, PolicyVersions, RestoreRuns erhalten „Force delete“
-    Aktionen (sichtbar nur im Trashed-Filter), mit klarer Confirmation.
-  - BackupSets: Force delete nur, wenn keine RestoreRuns existieren; löscht Items mit.
-  - Tenants: Force delete nur, wenn archiviert; blockiert für aktive Tenants.
-  - Alle Force-Deletes schreiben Audit-Log-Einträge vor der endgültigen Löschung.
-  - Tests für Force-Delete-Flows (erfolgreich/blockiert) ergänzen.
-## Phase 12: Single current tenant ("Highlander")
+- [x] T075 [HK] Force-Delete-Actions (only in trashed; guards; audit before delete) + tests.
 
-- [x] T120 [TENANT] Migration `add_is_current_to_tenants`:
-  - Spalte `is_current` (boolean, default false, not null) zu `tenants` hinzufügen.
-  - Partielle Unique-Index anlegen, z. B.:
-    - `UNIQUE INDEX tenants_current_unique ON tenants (is_current)
-       WHERE is_current = true AND deleted_at IS NULL`.
+## Phase 12b: Single current tenant ("Highlander")
 
-- [x] T121 [TENANT] Tenant-Model anpassen:
-  - Methode `makeCurrent()` implementieren:
-    - Transaktion: alle anderen Tenants `is_current = false`, dieser Tenant `is_current = true`.
-  - Methode `static current()` implementieren:
-    - Wenn `INTUNE_TENANT_ID` gesetzt ist → Tenant mit dieser GUID laden,
-      sonst Exception, wenn nicht gefunden / deaktiviert.
-    - Wenn nicht gesetzt → Tenant mit `is_current = true` und `status = active`
-      (und `deleted_at` null) zurückgeben.
-    - Wenn keiner → Exception “No current tenant selected”.
-  - `findOrCreateDefault()` deprecaten/entfernen; keine Dummy-Tenants mehr erzeugen.
+- [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] T122 [TENANT] Data-Migration / Cleanup:
-  - Falls mindestens ein Tenant mit `app_status = ok` existiert:
-    - einen als `is_current = true` markieren (z. B. den ersten).
-    - `local-tenant` auf `status = archived`, `is_current = false` setzen.
-  - Sicherstellen, dass `local-tenant` nie wieder als aktueller Kontext verwendet wird.
-
-- [x] T123 [TENANT] Filament `TenantResource` UI:
-  - Spalte/Badge für `is_current` in der Liste hinzufügen.
-  - Table-Action "Make current" ergänzen:
-    - nur sichtbar für aktive Tenants, die nicht `is_current` sind.
-    - ruft `makeCurrent()` auf und zeigt Notification.
-  - Alte Logik entfernen, die `local-tenant` automatisch als Default nutzt.
-
-- [x] T124 [TENANT] Consumers refactoren:
-  - Alle Vorkommen von `findOrCreateDefault()` suchen und durch `Tenant::current()`
-    (oder expliziten Tenant) ersetzen:
-    - Policy-Sync (Command + Filament-Action),
-    - BackupSet-Erstellung,
-    - RestoreRun-Erstellung,
-    - ggf. weitere Services.
-
-- [x] T125 [TENANT] Tests:
-  - Unit-Tests für `Tenant::current()`:
-    - INTUNE_TENANT_ID gesetzt → nimmt diesen Tenant, Fehler wenn nicht vorhanden.
-    - INTUNE_TENANT_ID nicht gesetzt → nimmt den mit `is_current = true`.
-    - kein current Tenant → Exception.
-  - Feature-Test für "Make current" in `TenantResource`:
-    - Nach der Action ist genau ein Tenant `is_current = true`, alle anderen `false`.
-  - Optional: Test, dass `local-tenant` nach Cleanup nicht mehr als Kontext gewählt wird.
-
-  - [x] T130 [UX] Tabellen-Aktionen in Dropdown bündeln (ActionGroup)
-  - In `TenantResource` (Tenants-Liste) die Zeilen-Aktionen refaktorieren:
-    - `View` (optional) direkt anzeigen.
-    - Alle weiteren Aktionen (`Edit`, `Admin consent`, `Verify configuration`,
-      `Deactivate`, `Force delete`) in eine `Tables\Actions\ActionGroup` mit
-      "⋯"-Icon verschieben.
-  - Prüfen, ob in anderen Ressourcen mit vielen Row-Actions (z.B. Backups,
-    RestoreRuns) ebenfalls eine `ActionGroup` sinnvoll ist und diese konsistent
-    einsetzen.
+- [x] T130 [UX] Tabellen-Aktionen in Dropdown bündeln (ActionGroup) in TenantResource (+ optional others).
 
 ## Phase 13: Settings Normalization & Display (Priority: P1)
 
-**User Story**: US1b - "Admin can open a policy detail page and see the **effective Intune settings** in a readable, normalized way (not raw JSON dumps)"
+- [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.
 
-**Prerequisites**: Phase 3 (Policy inventory) complete, PolicyResource and PolicyVersionResource exist
+- [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.
 
-**Scope**: FR-019 / FR-019a / FR-019b, Edge-001 (malformed snapshots), Edge-002 (@odata.type mismatch), NFR-006 (normalization coverage) per .specify/plan.md dated 2025-12-10.
-
-### Tests for User Story 1b
-
-- [x] T140 [P] [US1b] Unit test for PolicyNormalizer service in `tests/Unit/PolicyNormalizerTest.php`:
-  - Test OMA-URI/custom policy transformation → path/value table structure
-  - Test Settings Catalog policy transformation → flattened key-value structure
-  - Test standard object transformation → labeled key-value with metadata filtered
-  - Test edge case: malformed snapshot (keys lost) → returns warning indicator
-  - Test edge case: @odata.type mismatch detection → returns mismatch flag
-
-- [x] T141 [P] [US1b] Feature test for Settings section in Policy detail view in `tests/Feature/Filament/PolicySettingsDisplayTest.php`:
-  - Create policy with sample JSONB snapshot (OMA-URI, Settings Catalog, standard object)
-  - Visit Policy detail page via PolicyResource
-  - Assert "Settings" section exists using Infolist component
-  - Assert normalized settings are displayed (not raw JSON)
-  - Assert metadata keys (@odata.type, internal IDs) are hidden
-
-- [x] T142 [P] [US1b] Feature test for pretty JSON + normalized settings in Version detail in `tests/Feature/Filament/PolicyVersionSettingsTest.php`:
-  - Create policy version with JSONB snapshot
-  - Visit PolicyVersion detail page via PolicyVersionResource
-  - Assert pretty-printed JSON exists (monospace, copyable format)
-  - Assert normalized settings section exists below JSON
-  - Assert both views show same data in different formats
-
-- [x] T143 [P] [Edge] Feature test for malformed snapshot warning in `tests/Feature/Filament/MalformedSnapshotWarningTest.php`:
-  - Create policy version with malformed snapshot (array-only, keys lost)
-  - Visit detail pages (Policy and PolicyVersion)
-  - Assert UI warning displayed: "This snapshot may be incomplete or malformed"
-  - Assert partial settings display attempted with warning badge
-
-- [x] T144 [P] [Edge] Feature test for @odata.type mismatch flag in `tests/Feature/Filament/ODataTypeMismatchTest.php`:
-  - Create policy with @odata.type that doesn't match expected platform/type mapping
-  - Visit Policy detail page
-  - Assert warning badge/banner displayed
-  - Create restore run with mismatched policy → assert validation error prevents execution
-
-### Implementation for User Story 1b
-
-- [x] T145 [US1b] Create PolicyNormalizer service in `app/Services/Intune/PolicyNormalizer.php`:
-  - Method `normalize(array $snapshot, string $policyType): array`:
-    - Returns `['status' => 'success|warning|error', 'settings' => [...], 'warnings' => [...]]`
-  - Implement OMA-URI transformation:
-    - Extract `omaSettings` array → convert to `[['path' => '...', 'value' => '...']]` table
-    - Filter metadata keys (@odata.type, id, version, etc.)
-  - Implement Settings Catalog transformation:
-    - Flatten nested `settings` or `settingsDelta` structures
-    - Convert to labeled key-value pairs with categories
-  - Implement standard object transformation:
-    - Extract top-level properties (displayName, description, etc.)
-    - Group by logical sections (General, Assignment, Advanced)
-    - Hide Graph-specific metadata
-  - Add edge case detection:
-    - Check for malformed snapshots (empty keys, string-only serialization)
-    - Check for @odata.type mismatch with policy type
-    - Return warnings array with human-readable messages
-
-- [x] T146 [US1b] Add Settings Infolist section to PolicyResource ViewPolicy page in `app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php`:
-  - Override `infolist()` method to add "Settings" section
-  - Use `Infolist\Components\Section` for grouping
-  - Call `PolicyNormalizer::normalize()` with policy's latest snapshot
-  - Render normalized settings using:
-    - `TextEntry` for simple key-value pairs
-    - `RepeatableEntry` for tables (OMA-URI paths)
-    - `KeyValueEntry` for nested structures
-  - Display warnings if normalization returns warning status
-  - Add fallback: "No settings available" if snapshot missing/empty
-
-- [x] T147 [US1b] Enhance PolicyVersionResource ViewPolicyVersion with pretty JSON component in `app/Filament/Resources/PolicyVersionResource/Pages/ViewPolicyVersion.php`:
-  - Add "Snapshot" section with two subsections:
-    1. **Raw JSON** (collapsible):
-       - Use `ViewEntry` with custom view blade
-       - Format JSON with `json_encode($snapshot, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)`
-       - Wrap in `
` for syntax highlighting
-       - Add "Copy" button using Filament action
-    2. **Normalized Settings**:
-       - Call `PolicyNormalizer::normalize()` with version snapshot
-       - Render same structure as T146 (reuse component if possible)
-  - Handle edge case: malformed snapshot → show warning above both views
-
-- [x] T148 [US1b] Integrate PolicyNormalizer output into existing views:
-  - Update `PolicyResource` list table: add hint column showing "Settings available" badge
-  - Update `PolicyVersionResource` diff view: optionally show normalized diff (if feasible)
-  - Ensure tenant-scoped queries when fetching policies for normalization
-  - Add caching consideration: normalize on-demand, don't store normalized output
-
-- [x] T149 [Edge] Add snapshot validation helper in `app/Services/Intune/SnapshotValidator.php`:
-  - Method `validate(array $snapshot): ValidationResult`:
-    - Check for empty/null snapshot → return error
-    - Check for array-only structure (keys lost) → return warning
-    - Check for string-only serialization → return error
-    - Check for required Graph keys (@odata.type, displayName) → return warning if missing
-  - Return structured result with:
-    - `isValid: bool`
-    - `warnings: array`
-    - `errors: array`
-  - Use in PolicyNormalizer before transformation
-
-- [x] T150 [Edge] Add @odata.type validator in Policy/BackupItem models:
-  - Add static method `Policy::validateODataType(array $snapshot, string $expectedType): bool`
-  - Check if `$snapshot['@odata.type']` matches expected Graph type for policy platform/type
-  - Create mapping array:
-    ```php
-    'deviceConfiguration' => [
-        'windows' => '#microsoft.graph.windowsConfiguration',
-        'ios' => '#microsoft.graph.iosConfiguration',
-        // etc.
-    ]
-    ```
-  - Return bool + optional error message
-  - Call in BackupService before storing backup item
-  - Call in RestoreService before executing restore (gate check)
-
-- [x] T151 [Edge] Display warnings in Filament UI for malformed/mismatched data:
-  - Update `PolicyResource/ViewPolicy` infolist:
-    - Add `Placeholder` component with warning icon + message if validation fails
-    - Show "⚠️ This snapshot may be incomplete or malformed" banner
-  - Update `PolicyVersionResource/ViewPolicyVersion`:
-    - Add warning badge next to "Snapshot" section title if malformed
-  - Update `RestoreRunResource` preview/execution:
-    - Add validation step that checks all selected items for @odata.type mismatch
-    - Show modal with list of problematic items + "Cancel" / "Proceed anyway" options
-    - Log validation warnings to audit log
-  - Ensure warnings are visible but not blocking (except for restore execution gate)
-
-### Documentation for Phase 13
-
-- [x] T152 [US1b] Update `README.md` with Settings normalization feature:
-  - Add section "Policy Settings Display" explaining:
-    - Normalized views for better readability
-    - Supported policy types (OMA-URI, Settings Catalog, standard objects)
-    - Edge case handling (malformed snapshots, type mismatches)
-  - Add screenshot/example of Settings section in Policy detail
-
-- [x] T153 [US1b] Add inline documentation in PolicyNormalizer:
-  - PHPDoc blocks for all public methods
-  - Examples of input/output structures in comments
-  - Edge case handling notes
-
-**Phase 13 Completion Criteria**:
-- All 14 tasks (T140-T153) completed
-- Tests passing for normalization logic and UI display
-- Policy detail shows readable Settings section
-- Version detail shows pretty JSON + normalized settings
-- Edge cases handled with clear warnings
-- Constitution Check: still 7/7 ✅ (auditability maintained, safety-first for restore gates)
-
-## Phase 14: User Story 8 – Intune RBAC Onboarding Wizard (Delegated, Synchronous)
+## 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 [US8] Add TenantResource ActionGroup entry “Setup Intune RBAC”: visible for active tenants with `app_client_id`; sits alongside Admin consent/Verify; guarded by Highlander rules (no deactivated tenants).
-- [ ] T161 [US8] Wizard UI (Filament): Step 1 preconditions/summary with inputs for Role (default Policy and Profile Manager, warn on Intune Admin), Scope (All devices or scope group), Group mode (create default TenantPilot-Intune-RBAC vs select existing security-enabled group); surface least-privilege warning for production.
-- [ ] T162 [US8] Delegated auth step: initiate delegated login for selected tenant; stop with clear error + audit on failure/denied consent; short-lived token only (no storage).
-- [ ] T163 [US8] Execution service (synchronous) with audit per step: resolve service principal by `app_client_id`; ensure/create security group; add SP as member (idempotent); ensure/create/update Intune role assignment for chosen role/scope using Graph abstraction; no queue usage.
+- [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`.
 
-- [ ] T164 [US8] Post-check (mandatory): force fresh token acquisition (clear tenant token cache) and run canary reads:
+- [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: `GET /identity/conditionalAccess/policies?$top=1` only if CA features are enabled
-  - Update `app_status`/permissions health + write audit entries (start/login/group/member/assignment/verify outcomes).
+  - 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 [US8] Tests (Pest, mocked Graph): happy path; rerun idempotent (no duplicates); missing permissions → clear error mapping; scope-limited selection → warning surfaced; delegated login failure path.
+- [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`.
 
-- [ ] T166 [US8] Documentation: README note for wizard behavior (delegated, synchronous), least-privilege defaults, audit expectations, and how to rerun safely.
+## Phase 15: User Story 8 – Graph Contract Registry & Drift Guard
 
-- [ ] T167 [US8-Optional] CLI/Job for CHECK/REPORT only (no grant) to inspect RBAC state; explicitly exclude grant actions from async/queue.
+**Scope**: FR-031 to FR-034; contract registry per type, type-family handling, capability fallbacks, drift checks.
 
-- [ ] T168 [US8] Extend Verify configuration / Health panel to include “Intune RBAC status”:
-  - Determine whether the configured Enterprise App (service principal) is covered by an Intune role assignment for required scopes (OK / Missing / Error).
-  - Show actionable message (“Run Setup Intune RBAC”) when missing.
-  - Persist last_checked_at + short reason code; log audit `tenant.rbac.checked`.
-
-- [ ] T169 [US8] Persist RBAC artifacts on Tenant for stable idempotency:
-  - Add nullable columns to `tenants`: `rbac_group_id`, `rbac_role_assignment_id`, `rbac_role_key`, `rbac_scope_mode`, `rbac_scope_id`.
-  - Prefer stored IDs on reruns; fall back to discovery only if missing.
-  - Add migration + model casts; include IDs in audit context (IDs only; no secrets).
+- [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.
diff --git a/specs/001-rbac-onboarding/plan.md b/specs/001-rbac-onboarding/plan.md
new file mode 100644
index 0000000..e0fc72a
--- /dev/null
+++ b/specs/001-rbac-onboarding/plan.md
@@ -0,0 +1,102 @@
+# Implementation Plan: TenantPilot v1
+
+**Branch**: `tenantpilot-v1`  
+**Date**: 2025-12-12  
+**Spec Source**: `.specify/spec.md` (scope/restore matrix unchanged)
+
+## Summary
+TenantPilot v1 already delivers tenant-scoped Intune inventory, immutable backups, version history with diffs, defensive restore flows, tenant setup, permissions/health, settings normalization/display, and Highlander enforcement. Remaining priority work is the delegated Intune RBAC onboarding wizard (US7) and afterwards the Graph Contract Registry & Drift Guard (US8). All Graph calls stay behind the abstraction with audit logging; snapshots remain JSONB with safety gates (preview-only for high-risk types).
+
+## Status Snapshot (tasks.md is source of truth)
+- **Done**: US1 inventory, US2 backups, US3 versions/diffs, US4 restore preview/exec, scope config, soft-deletes/housekeeping, Highlander single current tenant, tenant setup & verify (US6), permissions/health overview (US6), table ActionGroup UX, settings normalization/display (US1b), Dokploy/Sail runbooks.  
+- **Next up**: **US7** Intune RBAC onboarding wizard (delegated, synchronous Filament flow).  
+- **Upcoming**: **US8** Graph Contract Registry & Drift Guard (contract registry, type-family handling, verification command, fallback strategies).
+
+## Technical Baseline
+- Laravel 12, Filament 4, PHP 8.4; Sail-first with PostgreSQL.  
+- JSONB for policy/backup/version payloads; FK/time indexes, GIN where needed.  
+- Graph abstraction with standardized error mapping/retries; no secrets in logs.  
+- Audit trail across backup/restore/version/tenant/permission/wizard steps; tenant isolation enforced.  
+- Restore matrix and supported types remain config-driven single sources of truth.  
+- Safety: preview/dry-run, confirmation gates, warnings for high-risk types; no implicit tenants (Highlander).
+
+## Constitution Check
+
+This plan is checked against the TenantPilot Constitution (see `.specify/memory/constitution.md`). Below are the principles with a short mapping to where the plan enforces them.
+
+- **I. Safety-First Operations**: Covered by "Safety: preview/dry-run, confirmation gates, warnings for high-risk types" and by restore safety gates (see "Restore Safety Gate" section).  
+- **II. Immutable Versioning**: Implemented via JSONB `policy_versions` and immutable writes (see Completed Workstreams: US3).  
+- **III. Defensive Restore**: Plan requires preview/dry-run, conflict detection and explicit confirmation before apply (Execution Plan: US4/US7).  
+- **IV. Auditability**: Audit logging mandated in each service (RbacOnboardingService, BackupService, RestoreService) and recorded in `audit_logs`.  
+- **V. Tenant-Aware Architecture**: Tenant-scoped data and Highlander enforcement are included (Completed Workstreams & Data section).  
+- **VI. Graph Abstraction**: All Graph calls go through `GraphClientInterface` and `config/graph_contracts.php` is used for contract handling.  
+- **VII. Spec-Driven Development**: This plan references `.specify/*` artifacts and the tasks are produced in `specs/001-rbac-onboarding/tasks.md` (this file); remaining gap: ensure each FR has explicit task mapping (see Next Actions).
+
+GATE: The above mappings satisfy the constitution's required checks for this plan. Any future change to scope or implementation that affects these principles must include an updated Constitution Check note here.
+
+## Completed Workstreams (no new action needed)
+- **US1 Inventory (Phase 3)**: Filament policy listing with type/category/platform filters; tenant-scoped.  
+- **US2 Backups (Phase 4)**: Backup sets/items in JSONB, immutable snapshots, audit logging, relation manager UX for attaching policies, soft-delete rules with restore-run guard.  
+- **US3 Versions/Diffs (Phase 5)**: Version capture, timelines, human+JSON diffs, soft-deletes with audit.  
+- **US4 Restore (Phase 6)**: Preview, selective execution, conflict warnings, per-type restore level (enabled vs preview-only), PowerShell decode/encode respected, audit of outcomes.  
+- **US6 Tenant Setup & Highlander (Phases 8 & 12)**: Tenant CRUD/verify, INTUNE_TENANT_ID override, `is_current` unique enforcement, “Make current” action, block deactivated tenants.  
+- **US6 Permissions/Health (Phase 9)**: Required permissions list, compare/check service, Verify action updates status and audit, permissions panel in Tenant detail.  
+- **US1b Settings Display (Phase 13)**: PolicyNormalizer + SnapshotValidator, warnings for malformed snapshots, normalized settings and pretty JSON on policy/version detail, list badges, README section.  
+- **Housekeeping/UX (Phases 10–12)**: Soft/force deletes for tenants/backups/versions/restore runs with guards; table actions in ActionGroup per UX guideline.  
+- **Ops (Phase 7)**: Sail runbook and Dokploy staging→prod guidance captured.
+
+## Execution Plan: US7 Intune RBAC Onboarding Wizard (Phase 14)
+
+- Objectives: deliver delegated, tenant-scoped wizard that safely converges the Intune RBAC state for the configured service principal; fully audited, idempotent, least-privilege by default.
+- Scope alignment: FR-023–FR-030, constitution (Safety-First, Auditability, Tenant-Aware, Graph Abstraction). No secret/token persistence; delegated tokens stay request-local and are not stored in DB/cache.
+- Design decisions:
+  - Service: `RbacOnboardingService` orchestrates steps using `GraphClientInterface`; reuse `RbacHealthService` for verification; all calls through abstraction with error mapping.
+- Data: use existing tenant RBAC columns (`rbac_group_id`, `rbac_group_name`, `rbac_role_assignment_id`, `rbac_role_key`, `rbac_scope_mode`, `rbac_scope_id`, status fields). No new entities; ensure casts + guards.
+- Audit: log start, delegated login outcome, group ensure, membership ensure, role assignment ensure/update, verify results. No payload logging; only IDs/status codes.
+- Wizard flow (Filament, Tenant detail ActionGroup):
+  1) Preconditions/config step with review screen: show tenant/app info, required permissions, least-privilege warning; inputs for role (default Policy/Profile Manager; Intune Administrator shows warning), scope (global default; optional group picker), group mode (create default `TenantPilot-Intune-RBAC` vs pick existing security-enabled group). Summarize planned changes before proceeding.
+  2) Delegated auth step: initiate login; on failure stop with actionable message + audit; do not store token beyond request.
+  3) Execute (synchronous): resolve service principal by `app_client_id`; on missing SP stop with consent-required hint + audit reason `sp_not_found`; ensure/create security group (validate `securityEnabled=true`); ensure SP membership (idempotent “already exists” OK); ensure/create/patch Intune role assignment for chosen role/scope; persist discovered IDs on tenant for idempotency.
+  4) Post-verify: force fresh token acquisition; run canary reads (deviceConfigurations, deviceCompliancePolicies, conditionalAccess if enabled); update RBAC/permission health; surface warnings if scope-limited; audit verify result.
+  5) Summary: show IDs (group, role assignment), role/scope used, verify status, CTA to retry policy sync.
+- UX rules: action only for active tenants with `app_client_id`; keep in ActionGroup with Admin consent/Verify; show badge/hint if RBAC missing; warnings on selecting Intune Administrator role; block execution if tenant inactive or missing consent/SP.
+- Safety/idempotency: handle “already exists” as success; no self-heal jobs; retry-safe writes; no queue usage to avoid token expiry; timeouts surfaced clearly; no delegated token persistence.
+- Tests: happy path, rerun idempotent, SP missing, insufficient privileges, non-security-enabled group failure, scope-limited warning, delegated auth failure path; Filament wizard visibility + summary rendering; health prompts to run wizard when RBAC missing.
+- Documentation: add wizard behavior, least-privilege defaults, audit expectations, “no token storage”, and how to rerun safely; note CTA to retry policy sync.
+- Operational note: After admin-consent or RBAC changes, force a fresh token acquisition (e.g., clear app token cache) before re-trying sync/backup/restore; Verify should run with a non-stale token. Optional CHECK/REPORT jobs only (no grant) remain out-of-scope for this phase.
+- Testing plan (Pest):
+  - Service unit tests: happy path, rerun idempotent, SP missing, insufficient privileges, scope-limited warning, group exists/not security-enabled failure.
+  - Filament feature: wizard visibility gating, delegated failure path, successful run shows summary and updates health, warnings rendered.
+  - Health integration: Verify reflects RBAC status and prompts to run wizard when missing.
+- Deployment/ops: no new env vars; ensure migrations for tenant RBAC columns are applied; run targeted tests `php artisan test tests/Unit/RbacOnboardingServiceTest.php tests/Feature/Filament/TenantRbacWizardTest.php`; Pint on touched files.
+
+## Upcoming: US8 Graph Contract Registry & Drift Guard (Phase 15)
+
+- Objectives: centralize Graph contract assumptions per supported type/endpoint and provide drift detection + safe fallbacks so preview/restore remain stable on Graph shape/capability changes.
+- Scope alignment: FR-031–FR-034 (spec), constitution (Safety-First, Auditability, Graph Abstraction, Tenant-Aware).
+- Approach:
+  - Artifact: `config/graph_contracts.php` (or similar) with per-type contract data:
+    - resource paths (collection + single item)
+    - allowed `$select` / allowed `$expand`
+    - **type families / allowed `@odata.type` values**
+    - create/update methods, id field
+    - hydration strategy (member expansion vs follow-up fetch vs unavailable)
+  - Service: registry + checker; integrate with Graph client to enforce allowed capabilities and downgrade on capability errors (retry without expands/selects), recording warnings/audit entries.
+  - Type families: treat derived `@odata.type` values **within a declared family** as compatible (no `odata_mismatch`) for routing preview/restore.
+  - Verification: `php artisan graph:contract:check` (staging/CI) to probe endpoints and surface actionable diffs when Graph changes; opt-in/guarded for prod.
+  - Docs: explain registry format and update process when Graph changes.
+- Testing outline: unit for registry lookups/type-family matching/fallback selection; integration/Pest to simulate capability errors and ensure downgrade path + correct routing for derived types.
+
+## Testing & Quality Gates
+- Continue using targeted Pest runs per change set; add/extend tests for US7 wizard now, and for US8 contracts when implemented.  
+- Run Pint on touched files before finalizing.  
+- Maintain tenant isolation, audit logging, and restore safety gates; validate snapshot shape and type-family compatibility prior to restore execution.
+
+### Restore Safety Gate
+- Restore execution MUST be blocked if a snapshot’s `@odata.type` is **outside** the declared **type family** for the target policy type (prevent cross-type/platform restores).
+- Restore preview MAY still render details + warnings for out-of-family snapshots, but MUST NOT offer an apply action.
+
+## Coordination
+- Update `.specify/tasks.md` to reflect progress on US7 wizard and future US8 contract tasks; no new entities or scope changes introduced here.  
+- Stage validation required before production for any migration or restore-impacting change.  
+- Keep Graph integration behind abstraction; no secrets in logs; follow existing UX patterns (ActionGroup, warnings for risky ops).
\ No newline at end of file
diff --git a/specs/001-rbac-onboarding/spec.md b/specs/001-rbac-onboarding/spec.md
new file mode 100644
index 0000000..98ec5af
--- /dev/null
+++ b/specs/001-rbac-onboarding/spec.md
@@ -0,0 +1,709 @@
+# Feature Specification: TenantPilot v1
+
+**Feature Branch**: `tenantpilot-v1`  
+**Created**: 2025-12-10  
+**Status**: Draft  
+**Input**: TenantPilot v1 scope covering Intune configuration inventory (config, compliance, scripts, apps, conditional access, endpoint security, enrollment/autopilot, RBAC), backup, version history, and defensive restore for Intune administrators.
+
+## Scope
+
+```yaml
+scope:
+  description: "v1 muss folgende Intune-Objekttypen inventarisieren, sichern und – je nach Risikoklasse – wiederherstellen können."
+  supported_types:
+    - key: deviceConfiguration
+      name: "Device Configuration"
+      graph_resource: "deviceManagement/deviceConfigurations"
+      notes: "Inklusive Custom OMA-URI, Administrative Templates und Settings Catalog."
+
+    - key: deviceCompliancePolicy
+      name: "Device Compliance"
+      graph_resource: "deviceManagement/deviceCompliancePolicies"
+
+    - key: appProtectionPolicy
+      name: "App Protection (MAM)"
+      graph_resource: "deviceAppManagement/managedAppPolicies"
+      notes: "iOS und Android Managed App Protection."
+
+    - key: conditionalAccessPolicy
+      name: "Conditional Access"
+      graph_resource: "identity/conditionalAccess/policies"
+      notes: "Kritisch für Sicherheit. Policy.Read.All/Policy.ReadWrite.All nötig; v1: Restore nur mit starker Preview."
+
+    - key: deviceManagementScript
+      name: "PowerShell Scripts"
+      graph_resource: "deviceManagement/deviceManagementScripts"
+      notes: "scriptContent wird beim Backup base64-decoded gespeichert und beim Restore wieder encoded (vgl. FR-020)."
+
+    - key: enrollmentRestriction
+      name: "Enrollment Restrictions"
+      graph_resource: "deviceManagement/deviceEnrollmentConfigurations"
+
+    - key: windowsAutopilotDeploymentProfile
+      name: "Windows Autopilot Profiles"
+      graph_resource: "deviceManagement/windowsAutopilotDeploymentProfiles"
+
+    - key: windowsEnrollmentStatusPage
+      name: "Enrollment Status Page (ESP)"
+      graph_resource: "deviceManagement/deviceEnrollmentConfigurations"
+      filter: "odata.type eq '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration'"
+
+    - key: endpointSecurityIntent
+      name: "Endpoint Security Intents"
+      graph_resource: "deviceManagement/intents"
+      notes: "Account Protection, Disk Encryption etc.; Zuordnung über bekannte Templates."
+
+    - key: mobileApp
+      name: "Applications (Metadata only)"
+      graph_resource: "deviceAppManagement/mobileApps"
+      notes: "Backup nur von Metadaten/Zuweisungen (kein Binary-Download in v1)."
+    
+    - key: settingsCatalogPolicy
+      name: "Settings Catalog Policy"
+      graph_resource: "deviceManagement/configurationPolicies"
+      notes: "Intune Settings Catalog Policies liegen NICHT unter deviceConfigurations, sondern unter configurationPolicies. v1 behandelt sie als eigenen Typ."
+
+  restore_matrix:
+    deviceConfiguration:
+      backup: full
+      restore: enabled
+      risk: medium
+      notes: "Standard-Case für Backup+Restore; starke Preview/Audit Pflicht."
+
+    deviceCompliancePolicy:
+      backup: full
+      restore: enabled
+      risk: medium
+      notes: "Compliance-Änderungen können Zugriff beeinflussen, aber sind gut verständlich."
+
+    appProtectionPolicy:
+      backup: full
+      restore: enabled
+      risk: medium-high
+      notes: "MAM-Änderungen wirken auf Datenzugriff in Apps; Preview und Diff wichtig."
+
+    conditionalAccessPolicy:
+      backup: full
+      restore: preview-only
+      risk: high
+      notes: "Hohe Ausfallgefahr. v1: Backup, Versioning, Diff + ausführliche Preview; Restore nur manuell anhand Preview."
+
+    deviceManagementScript:
+      backup: full
+      restore: enabled
+      risk: medium
+      notes: "Script-Inhalt und Einstellungen werden gesichert; Decode/Encode beachten."
+
+    enrollmentRestriction:
+      backup: full
+      restore: preview-only
+      risk: high
+      notes: "Kann Enrollment blockieren; v1 eher nur Preview + manuelle Umsetzung."
+
+    windowsAutopilotDeploymentProfile:
+      backup: full
+      restore: enabled
+      risk: medium-high
+      notes: "Provisioning-kritisch; Preview + Audit, aber automatisierbar."
+
+    windowsEnrollmentStatusPage:
+      backup: full
+      restore: enabled
+      risk: medium
+      notes: "ESP beeinflusst OOBE UX; Änderungen klar sichtbar."
+
+    endpointSecurityIntent:
+      backup: full
+      restore: enabled
+      risk: high
+      notes: "Security-relevante Einstellungen (z. B. Credential Guard); Preview + klare Konflikt-Warnungen nötig."
+    
+    settingsCatalogPolicy:
+      backup: full
+      restore: enabled
+      risk: medium
+      notes: "Settings Catalog Policies sind Standard-Config-Policies (Settings Catalog). Preview/Audit Pflicht; Restore automatisierbar."
+
+    mobileApp:
+      backup: metadata-only
+      restore: enabled
+      risk: low-medium
+      notes: "Nur Metadaten/Zuweisungen; kein Binary; Restore setzt Konfigurationen/Zuweisungen wieder."
+```
+
+## User Scenarios & Testing *(mandatory)*
+
+### User Story 1 - Policy inventory listing (Priority: P1)
+
+Admin can view supported Intune object types (as defined in the scope) with normalized metadata for selection.
+
+**Why this priority**: Inventory is the entry point for backup/version flows. Without it, no downstream workflows are usable.
+
+**Independent Test**: From Filament, navigate to Policies; verify supported types render with identifiers, type/category, platform metadata, and tenant scoping.
+
+**Acceptance Scenarios**:
+
+1. **Given** an authenticated admin, **When** they open the Policies list, **Then** they see supported object types with identifiers, type/category, platform, and last-updated metadata.
+2. **Given** filtering by type/category, **When** the admin selects a type, **Then** only matching objects appear and the view remains tenant-scoped.
+3. **Given** Settings Catalog Policies exist in Intune, **When** the admin opens the Policies list and syncs, **Then** Settings Catalog Policies are listed as type `Settings Catalog Policy` (settingsCatalogPolicy) and are not mixed into `Device Configuration`.
+---
+### User Story 1b - Policy detail shows readable settings (Priority: P1)
+
+Admin can open a policy detail page and see the **effective Intune settings** in a readable, normalized way (not raw JSON dumps).
+
+**Independent Test**: From Filament, open a policy detail view; verify a "Settings" section renders normalized key/value pairs (or tables for special cases) derived from the latest snapshot.
+
+**Acceptance Scenarios**:
+
+1. **Given** a policy with at least one captured snapshot, **When** the admin opens the policy detail view, **Then** they see a "Settings" section rendering the policy configuration in a readable format (grouped/labeled).
+2. **Given** the snapshot contains nested structures or list-based settings (e.g., OMA-URI / Settings Catalog), **When** the admin views settings, **Then** values are flattened/grouped or rendered as tables, and irrelevant metadata keys are hidden.
+---
+### User Story 2 - Backup creation and browsing (Priority: P1)
+
+Admin creates backup sets containing multiple objects (config, compliance, scripts, apps, CA, etc.) with immutable snapshots and can browse backup details in Filament.
+
+**Why this priority**: Backups provide safety and enable restore; immutability and audit are foundational.
+
+**Independent Test**: Initiate a backup set selecting multiple objects; confirm immutable JSONB snapshots persisted, audit log written, and Filament shows backup detail and items.
+
+
+**Acceptance Scenarios**:
+
+1. **Given** selected objects from different categories, **When** the admin creates a backup set, **Then** backup items store immutable payload snapshots (full or metadata-only as per the restore matrix) with identifiers and types.
+2. **Given** a completed backup set, **When** the admin opens its detail page, **Then** all items and metadata display along with the audit record of creation.
+
+3. **Given** mehrere Backup-Sets existieren,
+   **When** der Admin ein Backup-Set auswählen oder ansehen möchte,
+   **Then** sieht er für jedes Set:
+   - einen sprechenden Namen (nicht nur Timestamp),
+   - das Erstellungsdatum,
+   - die Anzahl der enthaltenen Items,
+   - und optional eine kurze Beschreibung, damit er das Set sinnvoll unterscheiden kann.
+
+---
+
+### User Story 3 - Version history and diff (Priority: P1)
+
+Admin can capture versions for any supported object, view timelines, and compare any two versions with meaningful diffs.
+
+**Why this priority**: Version visibility and diffs enable rollback readiness and change comprehension.
+
+**Independent Test**: Create multiple versions for a given object; verify timeline ordering, version metadata, and diff output (human summary + JSON diff where feasible) between any two versions.
+
+**Acceptance Scenarios**:
+
+1. **Given** an admin triggers version capture for an object, **When** the version is saved, **Then** an immutable snapshot and metadata (actor, time, type, tenant) are recorded.
+2. **Given** two versions of the same object, **When** the admin requests a comparison, **Then** the UI shows a human-readable summary and structured JSON diff where available.
+3. **Given** a saved policy version, **When** the admin opens the version detail page, **Then** the snapshot is displayed as pretty-printed JSON and, where possible, as normalized settings (not as an unreadable serialized array/string).
+---
+
+### User Story 4 - Restore with preview and confirmation (Priority: P1)
+
+Admin can run a restore from a backup set with preview/dry-run, selective restore, clear warnings, and required confirmation before execution.
+
+**Why this priority**: Restore is high-risk; safety features are mandatory for production readiness.
+
+**Independent Test**: Start a restore from a backup set in preview; view change summary and warnings; select items; confirm execution; verify audit logs and outcomes recorded (success/failure/partial).
+
+**Acceptance Scenarios**:
+
+1. **Given** a backup set, **When** the admin initiates a restore in preview mode, **Then** the system shows a change summary with selectable items and conflict warnings.
+2. **Given** selected items and explicit confirmation, **When** execution proceeds, **Then** applied changes are tenant-scoped and audit logs record start, result, and any failures.
+
+3. **Given** mehrere Backup-Sets existieren,
+   **When** der Admin einen Restore Run erstellt,
+   **Then** zeigt die Auswahl für das "Backup set" mindestens:
+   - den Backup-Namen,
+   - das Erstellungsdatum,
+   - die Anzahl der Items,
+   damit der Admin das richtige Backup-Set sicher auswählen kann.
+
+4. **Given** ein Restore Run wurde erstellt,
+   **When** der Admin die Detailseite des Restore Runs öffnet,
+   **Then** sieht er, welche Policies/Items in diesem Run enthalten sind
+   (z. B. Liste der Policies mit Name/Typ/Plattform).
+---
+
+### User Story 4b - Rerun a restore operation (Priority: P2)
+
+Admin can rerun a previous restore operation to re-apply the same set of policies with the same settings.
+
+**Why this priority**: If a restore operation fails partially, or if an admin wants to re-apply a known good configuration, a rerun option provides a quick and safe way to do so without re-selecting all policies manually.
+
+**Independent Test**: From the Restore Run detail page, trigger the "Rerun" action. Verify that a new Restore Run is created with the same settings and items as the original run.
+
+**Acceptance Scenarios**:
+
+1. **Given** a completed or failed Restore Run, **When** the admin triggers the "Rerun" action, **Then** a new Restore Run is created with the same `backup_set_id`, selected items, and `dry_run` flag as the original run.
+2. **Given** a new Restore Run created via the "Rerun" action, **When** the admin executes it, **Then** it follows the same safety gates and confirmation steps as a manually created Restore Run.
+3. **Given** a "Rerun" action is triggered, **Then** an audit event `restore_run.rerun_created` is logged with a reference to the original `restore_run_id`.
+---
+
+### User Story 5 - Operational readiness and environments (Priority: P2)
+
+Local development uses Sail; deployments target Dokploy staging then production with clear validation steps.
+
+**Why this priority**: Ensures reproducible local setup and safe promotion to production.
+
+**Independent Test**: Run the app locally via Sail; validate migrations on staging before production; confirm required env vars and queues/workers are documented.
+
+
+
+
+### User Story 6 - Berechtigungsübersicht & Health-Status (Priority: P1)
+
+Als Admin möchte ich für jeden Tenant sehen, welche Microsoft Graph-Berechtigungen
+erforderlich sind, welche bereits erteilt wurden und welche fehlen, damit ich
+sicherstellen kann, dass alle Funktionen von TenantPilot sicher und vollständig
+arbeiten.
+
+**Why this priority**: Jede neue Funktion kann zusätzliche Berechtigungen benötigen.
+Ohne transparente Übersicht und Abgleich besteht das Risiko, dass Features still
+kaputt sind oder unsicher laufen.
+
+**Acceptance Scenarios**:
+
+1. **Given** ein Tenant ist in TenantPilot hinterlegt,
+   **When** der Admin die Tenant-Detailseite öffnet,
+   **Then** sieht er eine Liste aller *erforderlichen* Berechtigungen mit Status
+   (z. B. OK, fehlt).
+
+2. **Given** neue Funktionen wurden eingeführt, die zusätzliche Berechtigungen benötigen
+   und diese wurden in der zentralen Permissions-Liste hinzugefügt,
+   **When** der Admin die Tenant-Detailseite öffnet,
+   **Then** erscheinen die neuen Berechtigungen automatisch in der Übersicht und
+   fehlende Berechtigungen werden klar als fehlend markiert.
+
+3. **Given** der Admin klickt auf "Verify configuration",
+   **When** TenantPilot einen Graph-Twestcall und/oder das Permission-Setup prüft,
+   **Then** wird der Status der Berechtigungen aktualisiert (OK/fehlt/Fehler) und
+   es wird ein Audit-Eintrag erstellt.
+
+4. **Given** ein Tenant hat fehlende kritische Berechtigungen,
+   **When** andere Features (Policy-Sync, Backup, Restore) diesen Tenant verwenden,
+   **Then** kann TenantPilot dem Admin entsprechende Warnungen anzeigen oder die
+   Funktion mit einem klaren Fehler abbrechen.
+
+
+**Acceptance Scenarios**:
+
+1. **Given** a fresh checkout, **When** Sail commands run (`./vendor/bin/sail up -d`, `./vendor/bin/sail artisan migrate`), **Then** the app boots with PostgreSQL and Filament admin available.
+2. **Given** a pending release, **When** migrations and restore flows are validated on staging, **Then** production deployment proceeds with documented steps and environment parity.
+
+  ### User Story 8 – Graph Contract Registry & Drift Guard (Priority: P1)
+Admin soll sich darauf verlassen können, dass Backup/Restore/Preview nicht wegen Graph-Shape-Details (derived @odata.type, verbotene $expand/$select, Property-Abweichungen) “random” bricht.
+
+Acceptance Scenarios:
+	1.	Given ein Backup enthält @odata.type = #microsoft.graph.windows10CompliancePolicy,
+When Preview/Restore läuft,
+Then wird das als gültiger deviceCompliancePolicy-Family Typ behandelt (kein odata_mismatch), und der korrekte Endpoint/Method wird genutzt.
+	2.	Given ein Endpoint erlaubt bestimmte Expands/Selects nicht,
+When TenantPilot Requests baut,
+Then werden nur “allowed capabilities” verwendet (kein 400 durch OData parsing).
+	3.	Given Microsoft/Intune ändert Shape/Capabilities,
+When graph:contract:check läuft,
+Then schlägt der Check kontrolliert fehl und zeigt welcher Contract angepasst werden muss (statt dass Prod-Flows brechen).
+
+2) Neue Functional Requirements (FR-03x) ergänzen
+
+Beispiel, passend zu deinem Stil:
+	•	FR-031: System MUST maintain a central Graph Contract Registry per supported type/endpoint (resource path, allowed $select, allowed $expand, “type family” / allowed @odata.type values, create/update methods).
+	•	FR-032: Restore/Preview MUST treat derived @odata.type values as compatible within a declared type family (e.g. compliance policy family), and MUST NOT hard-fail on base-vs-derived mismatches.
+	•	FR-033: System MUST provide a verification command (e.g. php artisan graph:contract:check) that validates registry assumptions against live Graph behavior (at least via canary calls / lightweight probes), logging actionable diffs.
+	•	FR-034: When Graph returns capability errors (OData select/expand, unsupported features), system MUST downgrade to a safe fallback strategy (e.g. “no expand, extra fetches”) and MUST record a warning/audit entry.
+	•	FR-035: System MUST provide a "Rerun" action for any completed or failed restore run, which creates a new restore run pre-populated with the same backup set, selected items, and dry-run flag as the original run. The rerun MUST follow the same safety and confirmation gates.
+
+(Du kannst FR-033 “live check” auch optional machen für prod, aber mindestens in CI/Staging wertvoll.)
+
+3) Implementation Notes / Data Artefacts ergänzen
+
+Ein kleines, versioniertes Artefakt einführen, z. B.:
+	•	config/graph_contracts.php oder .specify/contracts/graph.yaml
+
+Darin pro Objekt-Typ:
+	•	resource (collection + single-item path)
+	•	type_family (Liste erlaubter @odata.type)
+	•	allowed_select / allowed_expand
+	•	member_hydration_strategy (z. B. “property array” vs “subresource” vs “not available”)
+	•	create_method / update_method / id_field
+
+Das verhindert “Wissens-Leaks” quer durch Services.
+### Edge Cases
+
+- Graph permissions missing or expired, causing policy fetch/restore failures with clear error mapping and audit entries.
+- Large policy payloads or many items in a backup set; ensure JSONB storage and pagination handle load without timeouts.
+- Restore conflicts when target tenant already has newer versions; preview must surface warnings and allow skip.
+- Partial restore failures; audits must capture per-item outcomes and surface retry guidance.
+- Diff generation for incompatible or malformed payloads should fail gracefully with admin-facing messaging.
+- Retention/size concerns for snapshots; document defaults and guard against unbounded growth.
+- Snapshots stored as serialized strings or array-only dumps (keys lost) must be detected; UI should show a clear warning and fall back to raw display.
+- Policies whose `@odata.type` does not match the expected platform/type mapping should be flagged to prevent wrong restore previews (e.g., stored as Windows but snapshot indicates Android).
+## Requirements *(mandatory)*
+
+### Functional Requirements
+
+- **FR-001**: System MUST list all Intune objects defined in the `scope.supported_types` section with normalized metadata and tenant scoping for selection.
+- **FR-002**: System MUST allow admins to create backup sets containing multiple objects (configuration, compliance, scripts, apps, conditional access, etc.) with immutable JSONB payload snapshots.
+- **FR-003**: Backup creation MUST log audit events including actor, timestamp, tenant, items, and outcome.
+- **FR-004**: System MUST capture policy versions on demand and present per-policy timelines.
+- **FR-005**: Users MUST be able to diff any two versions with a human-readable summary and structured JSON diff where feasible.
+- **FR-006**: Restore MUST support preview/dry-run, selective item restore, and explicit confirmation before applying changes, within the per-type restore level defined in `scope.restore_matrix`.
+- **FR-007**: Restore execution MUST produce audit logs covering success, failure, and partial outcomes.
+- **FR-008**: Graph integration MUST route through a dedicated abstraction layer with standardized error mapping, safe retries, and high-level logging without secrets.
+- **FR-009**: All policy, version, backup, and restore data MUST be tenant-aware; queries enforce tenant isolation.
+- **FR-010**: Application MUST run locally via Laravel Sail with PostgreSQL and provide Filament admin flows.
+- **FR-011**: Deployments MUST target Dokploy staging before production with documented migration and worker implications.
+- **FR-012**: Tests MUST cover backup composition rules, version immutability, audit events, and Filament backup/restore flows (with Graph boundaries mocked).
+- **FR-013**: Raw policy snapshots and backup payloads MUST be stored as JSONB with indexes justified by query needs (e.g., FK and time-based; GIN when filters require).
+- **FR-014**: UI MUST provide clear warnings for potential restore conflicts and require confirmation for destructive operations; for types with `restore: preview-only` in `scope.restore_matrix` no direct apply action MAY be offered.
+- **FR-015**: Admins MUST be able to safely delete (archive) backup sets that are no
+  longer needed. Deletion is implemented as soft-delete with audit logging, and
+  backup sets referenced by completed restore runs cannot be removed.
+
+- **FR-016**: Admins MUST be able to delete individual policy versions for housekeeping.
+  Deletion is implemented as soft-delete with audit logging.
+
+- **FR-017**: Admins MUST be able to deactivate (soft-delete) a tenant. 
+Deactivated
+  tenants:
+  - do not appear in default lists,
+  - cannot be used for new sync/backup/restore operations,
+  - keep their historical data and audit logs for traceability.
+- **FR-018**: Admins MAY soft-delete restore runs to keep the UI clean; underlying
+  backup and policy data remains untouched.
+
+- **FR-019**: The system MUST normalize different payload structures for display via a `PolicyNormalizer` (or equivalent): OMA-URI/custom policies as path/value tables, Settings Catalog policies as flattened structures, and standard objects as key-value views, aligned with `scope.supported_types`.
+- **FR-019a**: Policy detail views MUST display a "Settings" section derived from the latest available snapshot (using the normalizer output when available).
+- **FR-019b**: Policy version detail views MUST render snapshots as pretty-printed JSON (monospace, copyable) and SHOULD also render normalized settings via the same normalizer.
+- **FR-020**: For PowerShell script objects (`deviceManagementScript` in `scope.supported_types`), the `scriptContent` MUST be base64-decoded when stored in backups/versions for readability/diffing and encoded again when sent back to Graph during restore.
+- **FR-021**: Restore behavior MUST follow the per-type configuration in `scope.restore_matrix`: `backup` determines full vs metadata-only snapshots; `restore` determines whether automated restore is enabled or preview-only; `risk` informs warning/confirmation UX.
+- **FR-022**: For high-risk types with `restore: preview-only` in `scope.restore_matrix` (e.g., `conditionalAccessPolicy`, `enrollmentRestriction`), TenantPilot MUST provide full backups, version history, and diffs plus detailed restore previews, but MUST NOT expose direct Graph apply actions; restore is manual, guided by the preview.
+
+### Key Entities *(include if feature involves data)*
+
+- **tenants**: Represents the deployment tenant context; referenced by all scoped data.
+- **policies**: Normalized metadata for supported Intune policies.
+- **policy_versions**: Immutable snapshots with metadata (actor, timestamp, tenant, policy type).
+- **backup_sets**: Group of backup items with creator, timestamp, and tenant context.
+- **backup_items**: Individual policy snapshots within a backup set (immutable JSONB payload + identifiers).
+- **restore_runs**: Execution records for restores, including preview/actual flags and outcomes.
+- **audit_logs**: Audit trail entries for backups, restores, version captures, and significant Graph actions.
+
+## Success Criteria *(mandatory)*
+
+### Measurable Outcomes
+
+- **SC-001**: Admin can create a backup set selecting multiple policies and view immutable backup items with audit logs in Filament.
+- **SC-002**: Policy version history timeline is available per policy and supports comparing any two versions with summary and JSON diff outputs.
+- **SC-003**: Restore preview shows change summaries and conflict warnings; execution requires explicit confirmation and produces audit logs for all outcomes.
+- **SC-004**: Core flows run locally via Sail; staging validation of migrations and restore paths completes before production deployments.
+- **SC-005**: Automated tests covering backup composition, version immutability, audit logging, and Filament backup/restore flows pass via `./vendor/bin/sail artisan test`.
+
+
+### Technical Story – Enforce Single Current Tenant ("Highlander Principle")
+
+**Context**
+
+Aktuell können mehrere Tenants `status = active` sein. Graph-Operationen (Policy Sync,
+Backup, Restore) wählen den Kontext über Heuristiken (`findOrCreateDefault`,
+`local-tenant`), was zu falschen Tenants und Fehlern führt.
+
+**Goal**
+
+Es soll **immer genau einen klar definierten "current" Tenant** geben, über den
+alle Graph-Operationen laufen. Die Auswahl dieses Tenants ist explizit und
+transparent (UI + Env), nicht implizit.
+
+**Requirements**
+
+- Es gibt ein Flag `is_current` in `tenants`, das den aktuell verwendeten Kontext
+  markiert.
+- Die Datenbank erzwingt per partiellem Unique Index, dass höchstens ein
+  nicht-gelöschter Tenant `is_current = true` haben kann.
+- `Tenant::current()` liefert:
+  - falls `INTUNE_TENANT_ID` gesetzt ist, **genau diesen** Tenant (Fehler, wenn
+    er nicht existiert oder deaktiviert ist),
+  - sonst den Tenant mit `is_current = true` und `status = active`.
+  - falls keiner gefunden wird, eine klare Exception (“No current tenant selected”);
+    es werden keine Dummy-Tenants erzeugt.
+- In der Tenant-Verwaltung gibt es eine Action "Make current", die:
+  - in einer Transaktion alle anderen Tenants auf `is_current = false` setzt
+    und den gewählten Tenant auf `is_current = true`,
+  - nur für aktive Tenants verfügbar ist.
+- Der frühere Placeholder `local-tenant` darf nicht mehr als Graph-Kontext genutzt
+  werden; sobald ein echter Tenant existiert, wird er archiviert und ist nie
+  `is_current`.
+- Alle Graph-basierten Funktionen (Policy Sync, Backup, Restore) verwenden
+  konsistent `Tenant::current()` oder einen explizit übergebenen Tenant.
+
+  Tenant-level actions such as "Admin consent" and "Verify configuration"
+MUST be exposed on the tenant detail view (and/or row actions), not as a
+global button without explicit tenant context.
+
+
+### UX Guideline – Table Actions / Dropdowns
+
+- Tabellen in Filament mit mehr als zwei Zeilen-Aktionen (z.B. View, Edit,
+  Admin consent, Verify, Deactivate, Force delete) MÜSSEN ihre Aktionen in
+  einem kompakten Dropdown / ActionGroup bündeln, statt alle Buttons nebeneinander
+  anzuzeigen.
+- Ausnahmen: besonders häufige, nicht-destruktive Aktionen (z.B. "View")
+  dürfen weiterhin als einzelner Button sichtbar bleiben; alle weiteren
+  Aktionen (z.B. Admin-Aktionen, Housekeeping) sollen im Dropdown liegen.
+- Ziel: die Tabellen bleiben übersichtlich, Spaltenbreite wird begrenzt,
+  und Admins bekommen eine konsistente "⋯"-Interaktion für erweiterte Aktionen.
+
+
+
+## User Story 7 – Intune RBAC Onboarding Wizard (Delegated Admin Login) *(Priority: P1)*
+
+### Problem / Context
+
+TenantPilot arbeitet primär **app-only (Client Credentials)** gegen Microsoft Graph.  
+Für viele Intune-Objekte reicht „Graph App Permissions + Admin Consent“ allein nicht aus: Intune kann zusätzlich über **Intune RBAC**
+blockieren, wenn der **Service Principal** (Enterprise App) keine passende **Intune Role Assignment** inkl. Scope hat.
+Das äußert sich typischerweise als **403** mit „Application is not authorized to perform this operation“.
+
+Dieses Setup ist ein **Bootstrap-Problem**:
+- Ohne RBAC-Zuweisung sind Intune Reads/Writes blockiert.
+- Ohne ausreichende Rechte kann TenantPilot die RBAC-Zuweisung nicht „self-service“ per app-only herstellen.
+
+**Ziel:** TenantPilot bietet pro Tenant einen **Onboarding-Wizard**, bei dem ein Admin sich **interaktiv (delegated)** anmeldet,
+und TenantPilot automatisiert (idempotent) die erforderliche Intune-RBAC-Konfiguration für die konfigurierte Enterprise App herstellt.
+Danach funktionieren Policy Sync / Backup / Restore (gemäß Restore Matrix) zuverlässig.
+
+---
+
+### User Value
+
+- Admins können RBAC-Probleme direkt in TenantPilot beheben (kein “Portal-Rätselraten”).
+- Klarer, auditierter Ablauf (wer hat wann welche Rechte/Sopes gesetzt).
+- Minimiert Ausfälle bei Policy-Sync/Backup/Restore und reduziert Support-Aufwand.
+
+---
+
+### In Scope (v1)
+
+- Wizard in Filament auf der **Tenant-Detailseite** (tenant-scoped).
+- Delegated Admin Login (interaktiv).
+- Idempotente Ausführung:
+  - Service Principal (zu `tenant.app_client_id`) auflösen
+  - RBAC-Membership via **Security Group (recommended)** herstellen
+  - Intune Role Assignment erstellen/aktualisieren (Rolle + Scope)
+  - Abschließender Verify-Run (Health/Permissions aktualisieren)
+- Vollständige Audit-Logs pro Step.
+
+---
+
+### Out of Scope (v1)
+
+- Vollautomatisches “Self-heal” ohne Admin-Interaktion.
+- Zeitgesteuerte Jobs, die RBAC-Rechte vergeben (ohne explizite Admin-Aktion).
+- Unterstützung mehrerer paralleler RBAC-Profile pro Tenant (nur ein “recommended setup”).
+
+---
+
+## UX / Entry Points
+
+### Entry Point: Tenant Detail (Filament)
+
+Auf der `TenantResource` Detailseite im Action-Dropdown:
+
+- `Setup Intune RBAC` (Wizard)
+- `Admin consent`
+- `Verify configuration`
+
+**Visibility rules:**
+- Nur für `status=active` Tenants.
+- Nur wenn `app_client_id` gesetzt ist.
+- Optional: Badge/Hint “RBAC missing” aus Health-Check.
+
+**Copy/Help:**
+- Kurze Erklärung: “Graph Permissions ≠ Intune RBAC”.
+- Hinweis auf Least Privilege.
+- Klarer Hinweis, dass Änderungen tenantweit wirken (je nach Scope).
+
+---
+
+## Wizard Flow
+
+### Step 1 — Configuration (Role / Scope / Group)
+
+**Inputs:**
+- **Role** (Dropdown):
+  - Default: `Policy and Profile Manager` (Least Privilege für Policy/Config-Workflows)
+  - Optional: `Intune Administrator` (mit Warnung)
+- **Scope** (Dropdown):
+  - Default: `Global / All devices` (wenn verfügbar)
+  - Optional: Auswahl einer Scope Group / Device Group (falls euer Modell das nutzt)
+- **Group Mode**:
+  - Default: `Use Security Group (recommended)`
+  - Options:
+    - `Create new group` (Default-Name: `TenantPilot-Intune-RBAC`)
+    - `Use existing group` (Picker)
+
+**UI Requirements:**
+- “Review screen” zeigt *genau*, was erstellt/geändert wird (Role, Scope, Group).
+
+### Step 2 — Delegated Admin Login
+
+- Admin führt interaktiven Login durch (delegated).
+- Wizard zeigt klar:
+  - welcher Tenant
+  - welche App (Client ID / Display Name, sofern auflösbar)
+  - dass nur kurzzeitig ein User-Token genutzt wird
+
+**Security rule (mandatory):**
+- Delegated Access Tokens werden **nicht persistiert** (keine Speicherung in DB/Cache).
+- Tokens existieren nur im Request-Kontext / Session und werden nach Abschluss verworfen.
+
+### Step 3 — Execute Setup (Idempotent + Safe)
+
+Wizard führt folgenden Ablauf aus (alle Operationen tenant-scoped, über Graph-Abstraktion, mit Error-Mapping):
+
+1) **Resolve Service Principal**
+- Auflösen des Service Principals zur `tenant.app_client_id`.
+- Wenn nicht gefunden:
+  - Wizard stoppt mit Hinweis: “Enterprise App ist im Tenant nicht vorhanden. Bitte zuerst Admin Consent durchführen.”
+  - Audit log: `tenant.rbac.setup.failed` (reason: sp_not_found)
+
+2) **Ensure Security Group**
+- Falls “Create new group”:
+  - Security Group erstellen (securityEnabled=true, mailEnabled=false).
+  - Wenn bereits vorhanden (gleiches displayName): wiederverwenden (oder per gespeicherter `rbac_group_id`).
+- Falls “Use existing group”:
+  - Validieren: `securityEnabled=true`.
+- Ergebnis-IDs werden gespeichert:
+  - `tenants.rbac_group_id` (neu, optional)
+  - `tenants.rbac_group_name` (optional, nur für UX)
+
+3) **Ensure Membership (SP ∈ Group)**
+- Service Principal als Member hinzufügen, wenn nicht vorhanden.
+- Konflikte (already exists) müssen als OK behandelt werden.
+
+4) **Ensure Intune Role Assignment**
+- Suche nach existierendem Role Assignment, das:
+  - die gewünschte RoleDefinition referenziert
+  - die Group als Member enthält
+  - den gewünschten Scope abdeckt
+- Wenn vorhanden: **Patch/Update** (z. B. Scope ergänzen)
+- Wenn nicht vorhanden: **Create** Role Assignment
+
+5) **Post-Verify (mandatory)**
+- Direkt nach Setup:
+  - `Verify configuration` ausführen (inkl. Permission-Matrix Update)
+  - Zusätzlich 1–2 “Canary Calls” gegen Intune-Endpunkte, die für v1 kritisch sind (Read-Only reicht).
+- Ergebnisse werden in Tenant-Health gespeichert (`app_status`, permissions health).
+
+**Execution mode (v1):**
+- Wizard führt die Steps synchron aus (kein Queue-Job), um Token-Probleme zu vermeiden.
+- Timeouts: klare Fehlermeldung + Audit.
+
+### Step 4 — Summary
+
+- Wizard zeigt:
+  - Group (Name + ObjectId)
+  - Role (Name)
+  - Scope (global / group id)
+  - RoleAssignmentId (falls verfügbar)
+  - Verify result (OK / Partial / Failed)
+- CTA: “Retry policy sync”
+
+---
+
+## Data Model Additions (minimal)
+
+Erweiterung `tenants` (optional aber empfohlen, für Transparenz & Idempotenz):
+
+- `rbac_group_id` (nullable string/GUID)
+- `rbac_role_assignment_id` (nullable string/GUID)
+- `rbac_role_key` (nullable string; z. B. `policy_profile_manager`)
+- `rbac_scope_mode` (nullable string; z. B. `global|group`)
+- `rbac_scope_id` (nullable string/GUID)
+
+> Hinweis: Wenn ihr strikt ohne zusätzliche Felder arbeiten wollt, geht es auch rein über Discovery,
+> aber gespeicherte IDs machen den Wizard deutlich stabiler und schneller.
+
+---
+
+## Functional Requirements (additions)
+
+- **FR-023**: System MUST expose a per-tenant onboarding wizard “Setup Intune RBAC” in Filament.
+- **FR-024**: Wizard MUST use delegated admin login and MUST NOT store delegated tokens.
+- **FR-025**: Wizard MUST be idempotent (re-run safe) and MUST converge to the desired RBAC state.
+- **FR-026**: Wizard MUST support group-based RBAC membership (recommended) and MUST ensure the service principal is a member.
+- **FR-027**: Wizard MUST create or update Intune role assignments with an explicit role + scope.
+- **FR-028**: Wizard MUST run a post-setup verification that updates tenant health and permissions UI.
+- **FR-029**: Wizard MUST write audit logs for start, each step outcome, and final result (success/failed/partial).
+- **FR-030**: Wizard MUST enforce tenant isolation and use explicit tenant context (no implicit defaults).
+
+---
+
+## Non-Functional / Safety Requirements
+
+- Least Privilege:
+  - Default role selection is non-global admin (Policy/Profile manager).
+  - Selecting higher-privilege roles shows a warning and requires explicit confirmation.
+- Clear Failure UX:
+  - Every failure must map to an actionable message (e.g., “Admin consent missing”, “Insufficient directory permissions”).
+- No secrets:
+  - No access tokens, secrets, or payloads in logs/audits.
+- Deterministic logging:
+  - Audit entries include tenant_id, actor_user_id, action_key, resource IDs (group, roleAssignment), and status.
+
+---
+
+## Acceptance Scenarios
+
+1) **Missing RBAC → Wizard fixes it**
+- **Given** ein aktiver Tenant mit konfigurierter App (`app_client_id`) und Admin Consent,
+  aber Intune Calls liefern RBAC-403,
+- **When** der Admin den Wizard ausführt,
+- **Then** wird Gruppe+Membership+RoleAssignment hergestellt, Verify wird OK, und Policy Sync funktioniert.
+
+2) **Admin Consent fehlt**
+- **Given** `app_client_id` ist gesetzt, aber der Service Principal kann nicht aufgelöst werden,
+- **When** Wizard startet,
+- **Then** bricht er mit “Bitte zuerst Admin Consent durchführen” ab und schreibt Audit `sp_not_found`.
+
+3) **Idempotenz**
+- **Given** Wizard wurde bereits erfolgreich ausgeführt,
+- **When** Wizard erneut mit gleichen Einstellungen ausgeführt wird,
+- **Then** werden keine Duplikate erzeugt, und die Summary zeigt “No changes / Already compliant”.
+
+4) **Insufficient privileges**
+- **Given** ein Admin loggt sich ein, aber hat nicht die nötigen Rechte,
+- **When** Setup ausgeführt wird,
+- **Then** stoppt der Wizard mit klarer Fehlermeldung pro Step (z. B. group create / role assignment create),
+  und Audit enthält den Step und Fehlercode.
+
+5) **Restricted scope**
+- **Given** Admin wählt eine eingeschränkte Scope Group,
+- **When** Setup abgeschlossen ist,
+- **Then** Verify markiert ggf. “Partial” mit Hinweis “Inventory limited by scope”.
+
+---
+
+## Implementation Notes (for plan.md linkage)
+
+- Reuse existing services pattern:
+  - `IntuneRbacSetupService` (new) in `app/Services/Intune/`
+  - Uses `GraphClientInterface` and existing error mapping/logging hooks.
+  - Uses `AuditLogger` for stepwise audit events.
+- Extend `TenantPermissionService` (User Story 7 in tasks) to include an RBAC check state:
+  - status: `ok|missing|error`
+  - message: “Intune RBAC role assignment missing (Wizard required)”
+- Add Filament wizard page/action under `TenantResource`.
+
+---
+
+## Edge Cases
+
+- Group exists but is not security-enabled → fail with actionable message.
+- Role assignment exists but wrong scope → patch and warn.
+- Multiple “similar” groups by name → prefer stored `rbac_group_id` if present, else prompt.
+- Tenant mismatch: Wizard must never operate on non-selected tenant (enforce `Tenant::current()` or explicit tenant param).
+- Token expiry mid-run → show “Please retry” + audit partial.
+
+
+Previous draft archived under spechistory/spec.md
\ No newline at end of file
diff --git a/specs/001-rbac-onboarding/tasks.md b/specs/001-rbac-onboarding/tasks.md
new file mode 100644
index 0000000..c00b9c9
--- /dev/null
+++ b/specs/001-rbac-onboarding/tasks.md
@@ -0,0 +1,937 @@
+---
+description: "Task list for TenantPilot v1 implementation"
+---
+
+# Tasks: TenantPilot v1
+
+**Input**: Design documents from `.specify/spec.md` and `.specify/plan.md`
+**Prerequisites**: plan.md (complete), spec.md (complete)
+
+---
+
+## Constitution Evidence Ledger (Discovery + Verification)
+
+> This section is the canonical evidence record to satisfy Constitution VII (Spec-Driven Development) and IV (Auditability).
+> Each completed phase has: (a) discovery notes, (b) verification commands, (c) where to look in repo/UX.
+
+### Evidence: Phases 1–6 (US1–US4 core)
+- **Discovery:** Verified existing Filament resources and services implement tenant scoping and Graph abstraction; restore flow includes preview + confirmation; versions stored immutable JSONB; audits written for critical operations.
+- **Verification:**
+  - `./vendor/bin/pest tests/Feature/Filament/PolicyListingTest.php`
+  - `./vendor/bin/pest tests/Feature/Filament/BackupCreationTest.php`
+  - `./vendor/bin/pest tests/Feature/Filament/RestorePreviewTest.php tests/Feature/Filament/RestoreExecutionTest.php`
+  - `./vendor/bin/pest tests/Feature/Filament/PolicyVersionTest.php tests/Unit/VersionDiffTest.php`
+  - `./vendor/bin/pint --dirty`
+- **Manual checks:** Filament UI: Policies list/filter, BackupSet detail + items, RestoreRun preview/execution, PolicyVersion view/diff.
+
+### Evidence: Phase 13 (US1b settings display + safety gates)
+- **Discovery:** Normalized settings display added; malformed snapshot warnings; @odata.type mismatch gates block restore execution.
+- **Verification:**
+  - `./vendor/bin/pest tests/Unit/PolicyNormalizerTest.php`
+  - `./vendor/bin/pest tests/Feature/Filament/PolicySettingsDisplayTest.php`
+  - `./vendor/bin/pest tests/Feature/Filament/PolicyVersionSettingsTest.php`
+  - `./vendor/bin/pint --dirty`
+
+### Evidence: Phase 14 (US7 RBAC wizard)
+- **Discovery:** RBAC wizard stack present (TenantResource action, delegated auth controller, onboarding service, health panel, migrations, tests).
+- **Verification:**
+  - `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`
+  - `./vendor/bin/pest tests/Unit/RbacOnboardingServiceTest.php`
+  - `./vendor/bin/pint --dirty`
+
+### Evidence: Phase 15 (US8 Graph Contract Registry & Drift Guard)
+- **Discovery:** Contract registry + fallback integrated in Graph client; drift-check command added; type-family tolerant @odata validation added.
+- **Verification:**
+  - `./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php tests/Unit/GraphContractFallbackTest.php`
+  - `./vendor/bin/pest tests/Feature/Filament/ODataTypeMismatchTest.php`
+  - `./vendor/bin/pint --dirty`
+
+### Evidence: Settings Catalog (settingsCatalogPolicy) extensions
+- **Discovery:** Added first-class sync/type + restore hardening + hydration + normalized display improvements.
+- **Verification:**
+  - `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicySyncTest.php`
+  - `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogRestoreTest.php`
+  - `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php`
+  - `./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogTest.php`
+  - `./vendor/bin/pint --dirty`
+
+---
+
+## FR → Tasks Traceability Matrix (Explicit)
+
+> This matrix makes FR coverage explicit (tooling/audits). Tasks also carry local Implements: tags where most useful.
+
+- **FR-001 (Inventory)** → T008–T011
+- **FR-002 (Backups)** → T012–T015, T131–T132
+- **FR-003 (Auditability baseline)** → T006, T015, T020, T025
+- **FR-004 (Versions)** → T016–T020
+- **FR-005 (Diffs)** → T017, T019
+- **FR-006–FR-010 (Restore safety + preview + gating)** → T021–T026, T144, T151
+- **FR-011–FR-018 (Tenant-aware + Graph abstraction + governance basics)** → T003–T007, T035, T120–T125
+- **FR-019.1–FR-019.2 (Settings normalization + edge cases)** → T140–T153
+- **FR-023–FR-030 (RBAC onboarding wizard)** → T160–T169
+- **FR-031–FR-034 (Contract registry + drift guard)** → T170–T175
+- **FR-035 (Rerun restore)** → T156
+
+---
+
+# Tasks: TenantPilot v1
+
+
+## Measurable Thresholds (NFR/UX)
+
+These thresholds make qualitative terms measurable and testable.
+
+### Payload / Rendering Limits
+- **Settings table max rows:** 1000 rows per rendered table block (truncate with notice).
+- **Flatten recursion depth:** max depth 8; if exceeded, stop and warn.
+- **Max value length:** 500 characters rendered inline; provide copy/full view for longer values.
+- **Max JSON pretty print:** 1 MB rendered inline; above that show “download/copy only”.
+
+### Graph Request Limits
+- **Default Graph request timeout:** 30s per request.
+- **Hydration pagination limit:** max 50 pages per subresource; if exceeded → warning + partial snapshot.
+
+### Restore Safety
+- **Dry-run is binary:** a restore run is either dry-run or execute; no “default dry-run=true” semantics.
+- **Type mismatch gate:** `@odata.type` mismatch MUST block execution (preview may show).
+
+### Retention / Housekeeping
+- **Soft-deleted entities:** retained indefinitely unless explicitly force-deleted.
+- **Audit logs:** retained indefinitely by default (configurable later).
+
+### “Large payload” definition
+- Any snapshot JSONB > 1 MB OR settings table > 1000 rows is considered **large** and triggers truncation rules above.
+
+
+### FR-019 Settings Normalization & Display
+
+FR-019.1 **Normalized Settings View**
+- Admin can view a policy and policy version with settings rendered in a readable normalized format.
+- The normalized output MUST hide Graph metadata keys unless explicitly requested.
+
+FR-019.2 **Raw Snapshot + Copy**
+- Admin can view raw JSON snapshot (pretty-printed where possible) and copy it.
+
+FR-019.3 **Edge Handling**
+- Malformed snapshots MUST show a warning banner and attempt partial rendering.
+- `@odata.type` mismatch MUST show a warning; restore execution MUST be blocked.
+
+FR-019.4 **Thresholds**
+- Rendering and snapshot size limits MUST follow “Measurable Thresholds (NFR/UX)”.
+
+
+- [x] T023 [US4] Implement restore service with preview/dry-run and selective item application in `app/Services/Intune/RestoreService.php`, integrating Graph adapter and conflict detection.
+  - Implements: FR-006, FR-008, FR-009
+  - Implements: FR-021
+  - Implements: FR-020
+  - Verified by: `./vendor/bin/pest tests/Feature/Filament/RestorePreviewTest.php tests/Feature/Filament/RestoreExecutionTest.php`
+
+- [x] T145 [US1b] Create PolicyNormalizer service in `app/Services/Intune/PolicyNormalizer.php`.
+  - Implements: FR-019.1, FR-019.3
+  - Verified by: `./vendor/bin/pest tests/Unit/PolicyNormalizerTest.php`
+
+- [x] T160 [US7] Add TenantResource ActionGroup entry “Setup Intune RBAC” …
+  - Implements: FR-023, FR-024
+  - Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`
+
+- [x] T170 [US8] Add contract registry artifact …
+  - Implements: FR-031
+  - Verified by: `./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php` (See  above for file contents. You may not need to search or read the file again.)
+
+
+## Phase 1: Setup (Shared Infrastructure)
+
+- [x] T001 [P] [Shared] Confirm Sail/Env ready; ensure `.env` has PostgreSQL settings for Sail and Filament admin user seeded (if missing) in `database/seeders/`.
+- [x] T002 [P] [Shared] Add baseline docs for local dev and staging promotion notes in `README.md` (Sail commands, staging-before-prod reminder).
+
+## Phase 2: Foundational (Blocking Prerequisites)
+
+- [x] T003 [Shared] Add tenant-aware migrations for `tenants`, `policies`, `policy_versions`, `backup_sets`, `backup_items`, `restore_runs`, `audit_logs` with JSONB payloads and FK/time indexes in `database/migrations/`.
+  - Implements: FR-013
+- [x] T004 [Shared] Create models with relationships and guarded attributes for the above entities in `app/Models/`.
+- [x] T005 [Shared] Implement Graph abstraction contracts (`GraphClientInterface`, error mapping, logging hooks) in `app/Services/Graph/` with a mockable adapter.
+- [x] T006 [Shared] Add audit logging service/helper to capture actor, tenant, operation, resources, outcome in `app/Services/Intune/AuditLogger.php`.
+- [x] T007 [Shared] Seed supported policy types/metadata for initial scope in `database/seeders/PoliciesSeeder.php` and ensure tenant scoping.
+
+## Phase 3: User Story 1 - Policy inventory listing (Priority: P1)
+
+### Tests for User Story 1
+
+- [x] T008 [P] [US1] Feature test for Filament policy listing and filtering (tenant-scoped) in `tests/Feature/Filament/PolicyListingTest.php` using mocked Graph sync.
+- [x] T176 [Scope][US1] Add Settings Catalog Policies as first-class type (`settingsCatalogPolicy`)
+  - **Goal**: Intune **Settings Catalog Policies** werden als **eigener Typ** synchronisiert, angezeigt und sind für Backup/Version/Diff/Preview/Restore (gemäß Matrix) korrekt routbar.
+  - **Why**: Settings Catalog Policies liegen in Graph unter `deviceManagement/configurationPolicies` (nicht unter `deviceManagement/deviceConfigurations`). Aktuell erscheinen sie daher nicht (oder nur unvollständig).
+
+  ## Implementation
+  1) **Config: supported_types erweitern (Single Source of Truth)**
+     - In `config/tenantpilot.php` (oder eurem zentralen Type-Registry-File) neuen Typ hinzufügen:
+       - `key`: `settingsCatalogPolicy`
+       - `name`: `Settings Catalog Policy`
+       - `graph_resource`: `deviceManagement/configurationPolicies`
+       - `category`: `Configuration`
+       - `platform`: `windows` *(oder `all` + später per snapshot/@odata ableiten – je nach eurer Modelllogik)*
+     - UI-Label so wählen, dass Admin sofort erkennt: **“Settings Catalog”** (z. B. Badge/Label).
+
+  2) **Restore-Matrix erweitern**
+     - In eurer Restore-Konfig (`scope.restore_matrix` bzw. config-driven Matrix):
+       - `settingsCatalogPolicy: backup: full, restore: enabled, risk: medium` *(optional `medium-high` falls ihr strenger sein wollt)*
+     - Restore-Warnungen/Badges müssen den neuen Typ korrekt anzeigen.
+
+  3) **Graph Contract Registry erweitern**
+     - In `config/graph_contracts.php` Contract für `settingsCatalogPolicy` hinzufügen:
+       - Resource paths (collection + single item)
+       - `allowed_select`/`allowed_expand` (konservativ starten)
+       - `type_family` / erlaubte `@odata.type` Werte für diesen Typ
+       - Create/Update routing (`POST`/`PATCH` wie bei euren anderen Typen)
+     - Sicherstellen, dass **capability fallback** (downgrade ohne `$select/$expand`) auch hier greift.
+
+  4) **PolicySyncService erweitern**
+     - Sync-Pipeline muss zusätzlich `deviceManagement/configurationPolicies` abfragen und upserten:
+       - `policies.type_key = settingsCatalogPolicy`
+       - `external_id = Graph id`
+       - `display_name`, `description`, `last_modified`, etc.
+     - Tenant-scoping beibehalten.
+     - **No duplicates**: gleiche `external_id` darf nicht in zwei Typen landen (Unique/Guard prüfen).
+
+  5) **Snapshots / Settings availability**
+     - Für die Spalte/Badge **“Settings”** (Available/Missing):
+       - Snapshot-Fetch muss für `settingsCatalogPolicy` über den neuen Endpoint laufen (single item fetch).
+     - Normalizer/Validator:
+       - `@odata.type` muss für diesen Typ als kompatibel erkannt werden (über Contract/type-family).
+
+  6) **UI (Filament)**
+     - `PolicyResource`:
+       - Type/Category Filter um `Settings Catalog Policy` erweitern
+       - Optional: Category bleibt `Configuration`, aber Typ klar `Settings Catalog`
+     - Detailseite:
+       - Normalized Settings anzeigen (wenn euer Normalizer Settings Catalog schon kann)
+       - sonst mind.: **Raw JSON + Hinweis** “Settings Catalog normalization pending” (kein silent fail).
+
+  7) **Permissions/Health**
+     - Verify/Permissions-Liste prüfen, ob für `deviceManagement/configurationPolicies` zusätzliche Graph-Permissions nötig sind.
+     - Falls ja:
+       - `config/intune_permissions.php` ergänzen
+       - Health Panel zeigt fehlende Permission sauber an.
+
+  ## Tests (Pest)
+  - **Unit**:
+    - Contract Registry erkennt `settingsCatalogPolicy`
+    - type-family ok (derived `@odata.type` accepted)
+    - fallback ok (capability downgrade)
+  - **Feature**:
+    - Policy Sync importiert `configurationPolicies` als `settingsCatalogPolicy` und listet sie in der UI
+    - Settings badge wird **Available**, sobald Snapshot vorhanden ist
+  - **Regression**:
+    - `deviceConfiguration` Sync bleibt unverändert (keine Vermischung)
+
+  ## Verification
+  - `./vendor/bin/pest tests/Feature/Filament/PolicyListingTest.php`
+  - ggf. neue Tests:
+    - `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicySyncTest.php`
+  - Registry Tests erweitern:
+    - `./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php`
+
+  ## Acceptance Criteria
+  - In der Policies-Liste erscheinen Intune **Settings Catalog Policies** als eigener Typ **Settings Catalog Policy**.
+  - Admin kann danach **Backup/Version/Preview/Restore** (gemäß Matrix) für diesen Typ nutzen.
+  - **Keine Duplikate/Überlappung** mit `deviceConfiguration`.
+- [x] T177 [US4][Bugfix] Settings Catalog Restore: Graph-Fehlerdetails speichern + PATCH-Payload sanitizen (contract-driven)
+  - **Goal**: Restore von `settingsCatalogPolicy` soll nicht mehr als generisches `400 Graph apply failed` enden, sondern:
+    1) echte Graph-Fehlerdetails persistieren + im UI sichtbar machen
+    2) beim PATCH nur ein zulässiges Payload senden (read-only/meta Felder raus, whitelist/contract-driven)
+  - **Why**: `deviceManagement/configurationPolicies` akzeptiert beim PATCH i. d. R. keinen vollständigen Snapshot → read-only Felder führen zu 400.
+
+  **Implementation**
+  1) **RestoreRun Results verbessern (Fehlerdetails persistieren)**
+     - In `RestoreService` (oder zentralem Graph-Apply Catch):
+       - Bei Graph-Exception zusätzlich in `restore_run_item_results`/`results` JSON speichern:
+         - `graph_error_code`
+         - `graph_error_message`
+         - optional (falls vorhanden): `graph_request_id`, `graph_client_request_id`, `graph_date`
+       - UI (RestoreRun Detail) soll bei failed Items neben `code/reason` auch `graph_error_message` anzeigen (kurz) + “Details” (expand/collapsible) für request ids.
+
+  2) **Contract Registry: update sanitizer für settingsCatalogPolicy**
+     - In `config/graph_contracts.php` bei `settingsCatalogPolicy` ergänzen:
+       - entweder `update_whitelist` (preferred) **oder** `update_strip_keys`
+     - `update_whitelist` konservativ starten (nur Felder, die PATCH typischerweise akzeptiert), z. B.:
+       - `name`, `description`, `settings`, `technologies`, `platforms`, `roleScopeTagIds`
+       - `assignments` **nur** wenn Restore wirklich Assignments patcht (sonst weglassen)
+     - In `GraphContractRegistry` (oder äquivalent) Methode bereitstellen:
+       - `sanitizeUpdatePayload(string $typeKey, array $snapshot): array`
+       - Entfernt immer: `id`, `createdDateTime`, `lastModifiedDateTime`, `@odata.*`, `version`, `roleScopeTagIds@odata.*`, sowie unbekannte Keys
+     - In `RestoreService` beim UPDATE/PATCH:
+       - für `settingsCatalogPolicy` vor dem Graph PATCH immer `sanitizeUpdatePayload()` verwenden.
+
+  3) **Graph apply: bessere Diagnose im Audit**
+     - Audit-Event (z. B. `restore.item.failed`) soll zusätzlich `graph_error_code` + `graph_request_id` enthalten (keine Tokens/payloads).
+
+  **Tests (Pest)**
+  - Unit: `GraphContractRegistry` sanitizer
+    - Given snapshot mit read-only/meta Feldern → sanitized payload enthält nur whitelist
+  - Feature: Restore execution für settingsCatalogPolicy mit “bad payload”
+    - Mock Graph 400 mit error body → RestoreRun result speichert `graph_error_message` + IDs
+    - UI assertion: Fehlermeldung sichtbar (kurz) + Details optional
+
+  **Verification**
+  - `./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php`
+  - `./vendor/bin/pest tests/Feature/Filament/RestoreExecutionTest.php` (oder neues `SettingsCatalogPolicyRestoreTest.php`)
+  - Manuell: RestoreRun detail zeigt bei 400 die echte Graph-Fehlermeldung + request-id; kein generisches “apply failed” ohne Details.
+
+  **Acceptance Criteria**
+  - Restore von `settingsCatalogPolicy` nutzt PATCH mit sanitiziertem Payload.
+  - Bei Fehlern ist im RestoreRun klar ersichtlich *warum* (Graph error message), inkl. request ids für Support.
+
+- [x] T178 [US4][Bugfix] Settings Catalog Restore: PATCH strikt auf {name, description, settings} begrenzen + Property-Mapping (displayName→name) + case-insensitive strip
+  - **Problem**:
+    - Restore von `settingsCatalogPolicy` schlägt mit 400 fehl:
+      - “Invalid patch, attempting to patch property Platforms is not allowed. Valid properties are Name, Description, and Settings.”
+    - Sanitizer lässt `platforms/Platforms` noch durch und/oder es wird `displayName` statt `name` gepatcht.
+  - **Implementation**:
+    1) **Contract fix** (`config/graph_contracts.php`)
+       - Für `settingsCatalogPolicy` `update_whitelist` auf exakt:
+         - `name`, `description`, `settings`
+       - Optional: `update_map` definieren:
+         - `displayName` → `name`
+         - (und ggf. `Description`/`Settings` casing normalisieren)
+    2) **Sanitizer hardening** (`app/Services/Graph/GraphContractRegistry.php`)
+       - Whitelist/Strip **case-insensitive** anwenden (z. B. `Platforms`, `platforms`, `PlatformS` immer entfernen).
+       - Vor dem Final-Payload:
+         - Mapping anwenden (displayName→name)
+         - Blocklist zusätzlich hart erzwingen: `platforms`, `technologies`, `templateReference`, `id`, `@odata.*`, `createdDateTime`, `lastModifiedDateTime`
+       - Ergebnis-Payload für update muss **nur** `name/description/settings` enthalten.
+    3) **RestoreService** (`app/Services/Intune/RestoreService.php`)
+       - Sicherstellen, dass für `settingsCatalogPolicy` Update-Payload aus Sanitizer kommt (kein “merge back” später).
+       - Bei leerem Payload: als `noop`/`skipped` behandeln statt PATCH.
+  - **Tests (Pest)**:
+    - Unit: Sanitizer entfernt `platforms/Platforms` zuverlässig + mapping `displayName→name`:
+      - `tests/Unit/GraphContractRegistryTest.php` (erweitern)
+    - Feature: Restore Settings Catalog erzeugt PATCH ohne platforms und läuft durch (Graph mocked):
+      - `tests/Feature/Filament/SettingsCatalogRestoreTest.php` (happy-path ergänzen)
+  - **Verification**:
+    - `./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php tests/Feature/Filament/SettingsCatalogRestoreTest.php`
+    - `./vendor/bin/pint --dirty`
+  - **Acceptance**:
+    - Restore von `settingsCatalogPolicy` scheitert nicht mehr an `Platforms`.
+    - Results zeigen bei Fehlern weiterhin request-id/client-request-id (bleibt wie T177).
+
+- [ ] T179 [US1b][Scope][settingsCatalogPolicy] Hydrate Settings Catalog “Configuration settings” for all snapshots (versions, backups, previews) and ensure normalized display.
+
+- **Goal:** For `settingsCatalogPolicy`, the **Configuration settings** (as seen in the Intune Portal under *Configuration settings*) must be visible throughout the system. This includes:
+  - being part of the raw JSON in **Policy Versions** and **Backup Snapshots**.
+  - being displayed in the **Normalized settings** section in a readable list/table format.
+  - ensuring that **Diff, Preview, and Restore** operations are based on these detailed settings, not just on general metadata.
+
+- **Why:** The base entity for `deviceManagement/configurationPolicies` often only provides metadata (`name`, `platforms`, `settingCount`, etc.). The actual settings reside in a subresource (e.g., `.../configurationPolicies/{id}/settings`). Without hydrating this data, TenantPilot cannot display or work with the most relevant policy details like PIN length or biometric settings.
+
+---
+
+## Implementation
+
+### 1) Centralize Snapshot Hydration
+- In the service responsible for capturing snapshots (e.g., a central `PolicySnapshotService`, or within `VersionService` and `BackupService`), implement a method to hydrate `settingsCatalogPolicy` data.
+- When the `type_key` is `settingsCatalogPolicy`:
+    1. `GET deviceManagement/configurationPolicies/{id}` (Base entity).
+    2. `GET deviceManagement/configurationPolicies/{id}/settings` (with proper paging).
+    3. Merge the retrieved settings into the snapshot under a consistent key (e.g., `snapshot['settings'] = [...]`).
+- This hydration logic MUST be used for creating **policy versions**, **backup items**, and **restore previews**.
+
+### 2) Enhance PolicyNormalizer
+- In `app/Services/Intune/PolicyNormalizer.php`, ensure the normalizer can interpret and display the `snapshot['settings']` data for `settingsCatalogPolicy`.
+- It should render a readable table/list of the settings, not just metadata.
+
+### 3) Update UI Components
+- Ensure the **Policy Detail** and **Policy Version Detail** pages use the hydrated snapshots to display the settings.
+- The "Settings available" badge for `settingsCatalogPolicy` should only show "Available" if the snapshot contains the hydrated `settings`.
+
+### 4) Testing
+- **Feature Test:** Create a `SettingsCatalogPolicyHydrationTest.php` that:
+  - Mocks the Graph API for both the base entity and the `/settings` subresource.
+  - Triggers both a **Version Capture** and a **Backup**.
+  - Asserts that the resulting `PolicyVersion` and `BackupItem` snapshots contain the hydrated `settings`.
+  - Asserts that the Policy Detail and Version Detail pages display the normalized settings correctly.
+- **Unit Test:** `PolicyNormalizerSettingsCatalogTest.php` should be updated to verify the rendering of a hydrated snapshot.
+
+### Verification
+- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php`
+- `./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogTest.php`
+- `./vendor/bin/pint --dirty`
+
+- [x] T180 - DUPLICATE of T179. Merged into T179.
+
+
+
+
+- [x] T182 [US1b][settingsCatalogPolicy][UX] Dynamic normalization of Settings Catalog “settings” (generic flatten + readable labels)
+
+- **Goal:** `settingsCatalogPolicy` soll im **Normalized settings** Bereich nicht mehr nur “setting -” anzeigen, sondern die hydrierten `settings[]` **generisch** (ohne hartes Mapping pro Setting) als verständliche Liste/Tabelle darstellen:
+  - pro Setting: **SettingDefinitionId**, **Instance Type**, **Value** (und ggf. Choice-Value)
+  - nested `children` / group collections werden **rekursiv geflattet**
+  - optional: einfache Gruppierung (z. B. nach Prefix der definitionId oder “group root”)
+- **Why:** Microsoft hat unzählige Settings. Wir brauchen eine **dynamische** Darstellung, die immer funktioniert – auch für neue Settings, ohne dass wir jedes Setting kennen.
+
+---
+
+## Implementation
+
+### 1) PolicyNormalizer: settingsCatalogPolicy → generic flatten
+- In `app/Services/Intune/PolicyNormalizer.php`:
+  - Bei `policyType === settingsCatalogPolicy`:
+    - Wenn `snapshot['settings']` existiert:
+      - Erzeuge eine Normalizer-Sektion `Settings` als Tabelle/Repeatable:
+        - `definitionId` (string)
+        - `instanceType` (string, aus `settingInstance['@odata.type']`)
+        - `value` (string/number/bool/json; aus `simpleSettingValue.value` oder `choiceSettingValue.value`)
+        - `path` (optional): zusammengesetzter Pfad zur Einordnung (z. B. parentDefinitionId > childDefinitionId)
+      - Implementiere `flattenSettingsCatalogSettingInstances(array $settings): array`:
+        - Iteriere `settings[]` Einträge
+        - Extrahiere `settingInstance`
+        - Unterstütze generisch (mindestens):
+          - `deviceManagementConfigurationSimpleSettingInstance` → `simpleSettingValue.value`
+          - `deviceManagementConfigurationChoiceSettingInstance` → `choiceSettingValue.value`
+          - `deviceManagementConfigurationGroupSettingCollectionInstance`:
+            - iteriere `groupSettingCollectionValue[]`
+            - rekursiv `children[]`
+          - Fallback: wenn unbekannt → `value = json_encode(settingInstance)` (kurz/gekürzt)
+        - Für Rekursion: maximal Depth (z. B. 8) + Schutz gegen Zyklen/zu große Payloads.
+      - Optional: wenn Value ein “enum-like” String ist, zusätzlich `displayValue` = letzter Token nach `_` (nur für bessere Lesbarkeit, ohne Semantik zu behaupten).
+    - Wenn `settings` fehlt:
+      - Zeige Banner “Settings not hydrated” (oder “Partial snapshot”) und nur Metadaten.
+
+### 2) Filament View: bessere Darstellung (Table statt “setting -”)
+- In `PolicyResource/ViewPolicy` und `PolicyVersionResource/ViewPolicyVersion`:
+  - Stelle sicher, dass die Normalizer-Ausgabe für `Settings` als Tabelle angezeigt wird:
+    - Spalten: `Definition`, `Type`, `Value`, optional `Path`
+  - Lange Values: truncated mit “copy” möglich (oder expand/collapse).
+
+### 3) Diff: Fokus auf echte Settings (optional, aber empfohlen)
+- In der diff-summary Logik (falls vorhanden):
+  - Wenn `policyType=settingsCatalogPolicy` und `settings` vorhanden:
+    - Summary soll zumindest sagen: “X setting values changed/added/removed”
+    - (Die JSON diff bleibt weiterhin verfügbar.)
+
+### 4) Performance & Safety
+- Guardrails:
+  - max rows (z. B. 1000) → danach “truncated”
+  - value length max (z. B. 500 chars) → danach “truncated”
+  - depth limit
+- Keine Secrets loggen; Normalizer arbeitet nur auf Snapshot JSONB.
+
+---
+
+## Tests (Pest)
+
+### Unit: `tests/Unit/PolicyNormalizerSettingsCatalogFlattenTest.php`
+- Input Snapshot mit:
+  - simpleSettingInstance (int)
+  - choiceSettingInstance (string)
+  - groupSettingCollectionInstance mit children (mix)
+- Assert:
+  - Normalizer liefert `Settings` Sektion mit mehreren Zeilen
+  - jede Zeile hat `definitionId`, `instanceType`, `value`
+  - rekursive children werden als eigene Zeilen enthalten
+  - Unknown instance type fällt auf fallback (json string) ohne crash
+
+### Feature: `tests/Feature/Filament/SettingsCatalogPolicyNormalizedDisplayTest.php`
+- Erzeuge PolicyVersion (settingsCatalogPolicy) mit Snapshot inkl. `settings[]`
+- Öffne Version-Detail und Policy-Detail
+- Assert:
+  - In Normalized settings existiert Sektion “Settings”
+  - Tabelle enthält erwartete definitionIds und Werte (z. B. minimumpinlength=12, usebiometrics=true)
+  - Keine “setting -” Platzhalter mehr für diesen Snapshot
+
+### Regression
+- Bestehende Normalizer-Ausgaben für andere Typen bleiben unverändert.
+
+---
+
+## Verification
+
+- `./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogFlattenTest.php`
+- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicyNormalizedDisplayTest.php`
+- `./vendor/bin/pint --dirty`
+
+---
+
+## Acceptance Criteria
+
+- Settings Catalog Policy zeigt im **Normalized settings** Bereich eine verständliche **Settings-Tabelle** (DefinitionId/Type/Value) statt generischem “setting -”.
+- Rekursive Group/Children-Settings werden sichtbar (nicht verloren).
+- Darstellung ist **dynamisch** (kein hardcoded mapping pro Setting).
+- Guardrails verhindern UI/Memory Explosion bei sehr großen Policies.
+
+
+- [x] T183 [US1b][UX] Make Policy Version detail readable (Tabs + scroll-safe tables)
+
+- **Goal:** Policy Version Detail (und optional Policy Detail) soll für Admins **lesbar** sein:
+  - **Normalized Settings** ist Default/primär sichtbar
+  - **Raw JSON** ist weiterhin verfügbar, aber UI zerbricht nicht durch riesige Payloads
+  - Settings Catalog Tabellen/Paths/IDs werden sauber dargestellt (kein “Textsalat”)
+
+- **Why:** Aktuell verdrängt Raw JSON + lange SettingDefinitionIds/Paths die gesamte Seite. Admins sehen nicht mehr “was geändert wurde”, sondern nur Datenmüll.
+
+---
+
+## Implementation
+
+### 1) UI Layout: Tabs (Normalized default, Raw JSON secondary)
+- In `app/Filament/Resources/PolicyVersionResource/Pages/ViewPolicyVersion.php` (und optional `ViewPolicy.php`)
+  - ersetze die aktuelle Darstellung durch **Tabs**:
+    - Tab 1: **Normalized settings** (Default)
+    - Tab 2: **Raw JSON** (mit Copy Button)
+    - optional Tab 3: **Diff** (falls vorhanden)
+- Falls Filament Infolist-Komponenten keine Tabs erlauben:
+  - nutze eine `ViewEntry` und rendere Tabs in Blade via `x-filament::tabs`.
+
+### 2) Raw JSON: Max height + scroll + monospace
+- In der Raw JSON Blade-View (z.B. `resources/views/filament/infolists/entries/raw-json.blade.php` oder bestehende View)
+  - Wrap `
` mit:
+    - `class="max-h-[520px] overflow-auto rounded-lg border bg-gray-50 p-3 text-xs font-mono leading-relaxed"`
+  - optional: “Expand” action (modal/slideOver) für Vollbildansicht.
+
+### 3) Normalized settings tables: horizontal scroll + readable columns
+- In `resources/views/filament/infolists/entries/normalized-settings.blade.php`
+  - Table container:
+    - `class="overflow-x-auto rounded-lg border"`
+  - Table:
+    - `class="min-w-[900px] table-fixed"`
+  - Cells:
+    - Definition/Path: `font-mono text-xs break-all whitespace-normal`
+    - Value: `break-words whitespace-normal`
+  - Column widths:
+    - Definition: `w-[35%]`, Type: `w-[20%]`, Value: `w-[25%]`, Path: `w-[20%]`
+  - Long values: clamp (optional):
+    - `line-clamp-2` + “Show more” (details/modal)
+
+### 4) Optional: Search within Settings (nice-to-have)
+- Add a small client-side filter input (Alpine) above settings table:
+  - filters rows by DefinitionId/Value/Path
+- Keep it optional if you want minimal change in v1.
+
+---
+
+## Tests (Pest)
+
+### Feature: `tests/Feature/Filament/PolicyVersionReadableLayoutTest.php`
+- Given a `settingsCatalogPolicy` version with long `settings` payload
+- Assert:
+  - Tabs render (Normalized + Raw JSON)
+  - Raw JSON container has max-height/overflow classes
+  - Normalized table wrapper uses overflow-x
+  - Page does not contain extremely long unbroken lines without wrappers (basic assertion on classes)
+
+---
+
+## Verification
+
+- `./vendor/bin/pest tests/Feature/Filament/PolicyVersionReadableLayoutTest.php`
+- `./vendor/bin/pint --dirty`
+
+---
+
+## Acceptance Criteria
+
+- Policy Version page is readable on normal screen widths:
+  - Normalized settings are immediately visible without scrolling past raw JSON
+  - Raw JSON is accessible in second tab and scrolls inside its container
+  - Settings table does not break layout; long IDs/paths wrap/scroll cleanly
+- No regressions for other policy types (deviceConfiguration/compliance/scripts).
+
+
+
+- [x] T184 [US1b][UX] Use Filament Tables for Settings Catalog settings (Policy + Version) with responsive layout + SlideOver details
+
+- **Goal:** `settingsCatalogPolicy` Settings sollen **lesbar, scannbar und bedienbar** sein:
+  - als echte **Filament Table** (nicht “pseudo table” im Infolist-Blade)
+  - mit **truncate + tooltip**, horizontal scroll, sticky header
+  - mit **Details** (SlideOver) + Copy pro Row
+  - identisch nutzbar in **Policy Detail** und **Policy Version Detail**
+
+- **Why:** Die aktuelle Darstellung bricht Layout/Spaltenbreiten (Definition/Type/Value laufen ineinander). Filament Tables lösen genau diese Probleme (fixed layout, responsive, actions, search).
+
+---
+
+## Implementation
+
+### 1) Introduce reusable Livewire component for Settings Catalog settings table
+- New: `app/Livewire/SettingsCatalogSettingsTable.php`
+  - Props:
+    - `array $settingsRows` (aus PolicyNormalizer Output oder direkt aus Snapshot `settings`)
+    - `string $context` (`policy|version`) optional
+  - Intern: build a Filament `Table` with columns:
+    - **Definition** (`TextColumn::make('definition')`)
+      - `wrap(false)`, `searchable()`, `tooltip(fn($state) => $state)`, `limit(60)`
+    - **Type** (`TextColumn::make('type')`)
+      - `wrap(false)`, `toggleable()`, `limit(50)`, `tooltip(...)`
+    - **Value** (`TextColumn::make('value')`)
+      - `wrap(false)`, `limit(60)`, `tooltip(...)`
+      - render `(group)` badge for group rows
+    - **Path** (`TextColumn::make('path')`)
+      - `toggleable(isToggledHiddenByDefault: true)`, `limit(80)`, `tooltip(...)`
+  - Table config:
+    - `paginated([25, 50, 100])` (default 25)
+    - `searchPlaceholder('Search definition/value…')`
+    - `striped()`, `deferLoading()`
+  - Row Action:
+    - `Action::make('details')->label('Details')->icon('heroicon-m-eye')`
+    - opens **SlideOver**
+      - shows full Definition/Type/Value/Path + optional raw setting JSON (pretty)
+      - Copy buttons for Definition/Value
+
+### 2) Embed component via ViewEntry in Policy + PolicyVersion detail
+- Policy detail (`app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php`)
+  - For `settingsCatalogPolicy`:
+    - render `SettingsCatalogSettingsTable` (instead of current table block)
+    - pass rows from Normalizer (`normalize()` should expose a stable rows array)
+- PolicyVersion detail (`app/Filament/Resources/PolicyVersionResource/Pages/ViewPolicyVersion.php`)
+  - same embedding for `settingsCatalogPolicy`
+
+> Rule: Nur für `settingsCatalogPolicy` auf Table UI umstellen. Andere Typen bleiben Infolist/KeyValue.
+
+### 3) Tailwind/Filament styling guardrails (no layout break)
+- Ensure table container is responsive:
+  - wrap table in `div class="overflow-x-auto"`
+  - set columns non-wrapping by default (truncate)
+- Sticky header:
+  - enable sticky header in table (Filament supports sticky header via table wrapper CSS; if needed add a small CSS utility class in your Filament theme)
+
+### 4) Normalizer output contract (stable)
+- Ensure `PolicyNormalizer` returns for settingsCatalogPolicy:
+  - `['settings_table' => ['columns' => [...], 'rows' => [...]]]`
+  - rows fields: `definition`, `type`, `value`, `path`, `raw` (optional)
+- Table uses **rows**, not parsing raw snapshot again (single source).
+
+---
+
+## Tests (Pest)
+
+### Feature: `tests/Feature/Filament/SettingsCatalogSettingsTableRenderTest.php` (new)
+- Create a policy + version with hydrated `settings`
+- Visit Policy detail and PolicyVersion detail
+- Assert:
+  - table headers visible (Definition/Type/Value)
+  - at least one known definition appears
+  - “Details” action exists
+
+### Unit (optional): `tests/Unit/PolicyNormalizerSettingsCatalogRowsTest.php`
+- Given snapshot with nested settings instances
+- Assert normalizer returns rows with `definition/type/value/path`
+
+---
+
+## Verification
+
+- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogSettingsTableRenderTest.php`
+- `./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogRowsTest.php`
+- `./vendor/bin/pint --dirty`
+
+---
+
+## Acceptance Criteria
+
+- In **Policy Detail** und **Policy Version Detail** sind Settings Catalog Settings als **Filament Table** sichtbar (lesbar, nicht überlappend).
+- Lange Werte sind **truncated** aber per Tooltip/Details vollständig erreichbar.
+- Pro Row gibt es **Details SlideOver** + Copy.
+- Kein Layout-Bruch auf typischen Screenbreiten (Laptop/FullHD).
+
+
+
+- [ ]T185 [UX][US1b][settingsCatalogPolicy] Make Settings Catalog settings readable (label/value parsing + table ergonomics)
+
+- **Goal:** Settings Catalog Policies sollen im Policy/Version Detail **für Admins lesbar** sein, ohne dass wir “alle Settings kennen müssen”.
+  - Tabelle zeigt **sprechende Bezeichnung** + **kompakte Werte**
+  - Lange IDs bleiben verfügbar (Tooltip/Copy/Details), aber dominieren nicht die UI
+
+- **Why:** Aktuell sind `definitionId` und `choiceSettingValue.value` so lang, dass sie in der Tabelle abgeschnitten werden und der Admin weder Setting noch Wert versteht.
+
+---
+
+## Implementation
+
+### 1) Presentation layer: generate human-friendly labels (no registry needed)
+- Add helper in `PolicyNormalizer` (oder kleiner `SettingsCatalogPresenter`):
+  - `labelFromDefinitionId(string $definitionId): string`
+    - remove common prefixes: `device_vendor_msft_`, `user_vendor_msft_`, `policy_config_`, `admx_`
+    - replace `_` with spaces
+    - keep last 2–4 segments if string is huge
+    - replace `{tenantid}` with `{tenant}`
+  - Output example:
+    - `user_vendor_msft_policy_config_admx_desktop_nomydocumentsico...` → `Desktop: No My Documents Icon` (heuristic), fallback: last segments nicely spaced
+
+> Heuristic only. If no good split possible, fallback to “last segments” label.
+
+### 2) Parse values into a short “effective value”
+- Implement `valuePreview(array $settingInstance): string`:
+  - For `SimpleSettingValue`: return scalar (`12`, `0`, `true/false`)
+  - For `ChoiceSettingValue.value`: return last token after last `_` OR map known boolean patterns:
+    - suffix `_true`/`_false` → `True`/`False`
+    - suffix `_0`/`_1` for allowed/blocked → show `0`/`1` but also tag `Allowed/Blocked` if detectable
+  - For group instances: show `(group)` and put children into details view only
+
+### 3) Improve table ergonomics (Filament Table / Livewire)
+- In `SettingsCatalogSettingsTable`:
+  - Columns:
+    1) **Setting** (human label) + small muted secondary line showing truncated definitionId
+    2) **Value** (valuePreview)
+    3) **Type** (badge: Choice/Simple/Group)
+    4) Optional: **Path** (toggleable, hidden by default)
+  - Add:
+    - `->searchable()` should search both label + raw definitionId + raw value
+    - `->wrap()` / `->limit()` for long strings
+    - tooltips showing full definitionId/value on hover
+    - “Copy” icon action in row details (SlideOver) for Definition + Raw JSON
+  - Ensure horizontal scroll only inside table container:
+    - wrapper `div` with `overflow-x-auto` + `max-w-full`
+    - table layout fixed where possible (`table-fixed`) to prevent column blowouts
+
+### 4) Keep Raw JSON accessible but not primary
+- In PolicyVersion view:
+  - Put Raw JSON into collapsible section or separate tab.
+  - Normalized Settings tab becomes default for settingsCatalogPolicy.
+
+---
+
+## Tests (Pest)
+
+### Unit: `tests/Unit/SettingsCatalogPresenterTest.php`
+- labelFromDefinitionId() produces readable output and stable fallback
+- valuePreview() returns expected previews for:
+  - choice true/false, numeric, group
+
+### Feature: `tests/Feature/Filament/SettingsCatalogSettingsTableUsabilityTest.php`
+- Render policy detail with one very long definition + long choice value
+- Assert:
+  - label column shows shortened readable label (not the full raw string)
+  - value column shows preview (e.g., `True`, `12`, `Never`)
+  - details slide-over contains full raw definition/value + copy UI
+
+---
+
+## Verification
+- `./vendor/bin/pest tests/Unit/SettingsCatalogPresenterTest.php tests/Feature/Filament/SettingsCatalogSettingsTableUsabilityTest.php`
+- `./vendor/bin/pint --dirty`
+
+---
+
+## Acceptance Criteria
+- In Policy Detail, Settings table shows:
+  - **Readable Setting name** (not a cut-off vendor string)
+  - **Readable Value preview** (True/False/12/etc.)
+- Full raw definitionId and raw value remain accessible via tooltip and SlideOver + copy button.
+- No layout overflow/broken columns on common laptop viewport widths.
+
+
+
+### Implementation for User Story 1
+
+- [x] T009 [US1] Implement policy sync/import orchestrator using Graph abstraction in `app/Services/Intune/PolicySyncService.php` (no direct Graph in UI).
+  - Implements: FR-001
+- [x] T010 [US1] Create Filament resource/table for policies with filters and metadata columns in `app/Filament/Resources/PolicyResource.php`.
+  - Implements: FR-001
+- [x] T011 [US1] Add command/job to sync policies (queues-ready) in `app/Console/Commands/SyncPolicies.php` and queue job under `app/Jobs/`.
+
+## Phase 4: User Story 2 - Backup creation and browsing (Priority: P1)
+
+### Tests for User Story 2
+
+- [x] T012 [P] [US2] Feature test for creating backup sets with multiple policies and verifying immutable JSONB snapshots + audit log in `tests/Feature/Filament/BackupCreationTest.php`.
+
+### Implementation for User Story 2
+
+- [x] T013 [US2] Implement backup domain service to assemble snapshots from policies with Graph payload retrieval in `app/Services/Intune/BackupService.php`.
+  - Implements: FR-002
+  - Implements: FR-020
+- [x] T014 [US2] Add Filament resource/pages for backup sets and items (list/detail) in `app/Filament/Resources/BackupSetResource.php`.
+  - Implements: FR-002
+
+- [x] T131 [UX] [US2] Refactor BackupSet policy selection to RelationManager:
+  - Remove the multi-select policy picker from the BackupSet **Create** form (keep Create minimal: name/description).
+  - After create, redirect to BackupSet **Edit/View** where items can be managed.
+  - Add `BackupItemsRelationManager` to `BackupSetResource` showing a table with columns: Policy Name, Type (badge), Restore (badge), Risk (badge).
+  - Add header action “Policies hinzufügen” (searchable, multiple) that adds items/attaches policies **tenant-scoped** and prevents duplicates per BackupSet.
+  - Provide a remove action (detach/soft-delete as per domain rules).
+
+- [x] T132 [P] [US2] Update/extend `tests/Feature/Filament/BackupCreationTest.php` to cover the new UX flow:
+  - Create BackupSet without policies.
+  - Add multiple policies via RelationManager action.
+  - Verify immutable JSONB snapshots + audit log behavior remains correct.
+
+- [x] T015 [US2] Wire audit logging for backup creation events in `app/Services/Intune/BackupService.php` using `AuditLogger`.
+  - Implements: FR-003
+
+## Phase 5: User Story 3 - Version history and diff (Priority: P1)
+
+### Tests for User Story 3
+
+- [x] T016 [P] [US3] Feature test for version capture and timeline display in `tests/Feature/Filament/PolicyVersionTest.php`.
+- [x] T017 [P] [US3] Unit test for diff generation (human summary + JSON diff) in `tests/Unit/VersionDiffTest.php`.
+
+### Implementation for User Story 3
+
+- [x] T018 [US3] Implement version capture service with immutable JSONB writes in `app/Services/Intune/VersionService.php`.
+  - Implements: FR-004
+- [x] T019 [US3] Create diff helper (summary + structured JSON) in `app/Services/Intune/VersionDiff.php` and surface in Filament version compare view in `app/Filament/Resources/PolicyVersionResource.php`.
+  - Implements: FR-005
+- [x] T020 [US3] Hook version capture into relevant flows (manual trigger + backup/restore hooks) ensuring audit logging.
+
+## Phase 6: User Story 4 - Restore with preview and confirmation (Priority: P1)
+
+### Tests for User Story 4
+
+- [x] T021 [P] [US4] Feature test for restore preview (change summary, conflicts, selective items) in `tests/Feature/Filament/RestorePreviewTest.php`.
+- [x] T022 [P] [US4] Feature test for confirmed restore execution capturing audit logs and per-item outcomes in `tests/Feature/Filament/RestoreExecutionTest.php`.
+
+### Implementation for User Story 4
+
+- [x] T023 [US4] Implement restore service with preview/dry-run and selective item application in `app/Services/Intune/RestoreService.php`, integrating Graph adapter and conflict detection.
+- [x] T024 [US4] Add Filament restore UI (wizard or pages) showing preview, warnings, and confirmation gate in `app/Filament/Resources/RestoreRunResource.php`.
+  - Implements: FR-022
+- [x] T025 [US4] Record restore run lifecycle (start, per-item result, completion) and audit events in `restore_runs` and `audit_logs`.
+  - Implements: FR-007
+- [ ] T156 [US4][UX] Add “Rerun” action to RestoreRun row actions (ActionGroup): creates a new RestoreRun cloned from selected run (same backup_set_id, same selected items, same dry_run flag), enforces same safety gates/confirmations as original execution path, writes audit event restore_run.rerun_created with source_restore_run_id.
+  - Implements: FR-035
+  - Implements: FR-035
+
+
+## Phase 7: User Story 5 - Operational readiness and environments (Priority: P2)
+
+- [x] T026 [US5] Document Dokploy staging→production promotion steps, required env vars, queue/worker expectations, and migration safety notes in `README.md` or `docs/deploy.md`.
+  - Implements: FR-010
+  - Implements: FR-011
+  - Implements: FR-012
+- [x] T027 [US5] Add quick Sail commands and test invocation notes to `README.md` (e.g., `./vendor/bin/sail artisan test`) and ensure sample env entries for Graph credentials.
+
+## Phase 8: Tenant Management (Tenant hinzufügen, App-Setup, Verify) (Priority: P1)
+
+> Hinweis: Diese Phase ist “Tenant Management” und **nicht** US6, damit US6 sauber “Permissions/Health” bleibt.
+
+- [x] T030 [TENANT] Migration für `tenants` ergänzen/prüfen (name, tenant_id, domain, app_client_id, app_status, app_notes, timestamps).
+  - Implements: FR-011
+  - Implements: FR-014
+  - Implements: FR-015
+  - Implements: FR-016
+  - Implements: FR-017
+  - Implements: FR-018
+- [x] T031 [TENANT] Eloquent Model `Tenant` (Beziehungen, tenant-aware scopes).
+- [x] T032 [TENANT] Filament `TenantResource` (list/create/edit/detail; Actions: Open in Entra, Copy consent URL optional).
+- [x] T033 [TENANT] `TenantConfigService` / Graph connectivity check.
+- [x] T034 [TENANT] Action „Verify configuration“ + Audit (`tenant.config.verified`).
+- [x] T035 [TENANT] Tenant-Kontext in Policy/Backup/Restore/Audit Services (tenant_id überall setzen).
+  - Implements: FR-009
+- [x] T036 [TENANT] Feature-Test `TenantSetupTest` (ok/error + Audit).
+- [x] T037 [TENANT] Admin-Consent Callback Route (state → tenant mapping, status update, UI page).
+
+## Phase 9: User Story 6 - Berechtigungsübersicht & Health-Status (Priority: P1)
+
+- [x] T040 [P] [US6] Zentrale Permissions-Liste `config/intune_permissions.php` (+ optional `docs/permissions.md`).
+  - Implements: FR-006
+  - Implements: FR-008
+- [x] T041 [US6] Datenmodell Tenant-Berechtigungen (JSONB `granted_permissions` oder `tenant_permissions` Tabelle; status ok/missing/error).
+- [x] T042 [US6] `TenantPermissionService` (required, granted, compare DTO).
+- [x] T043 [US6] Tenant-Detail UI Panel „Permissions“ (required list + status).
+- [x] T044 [US6] Verify erweitern: compare + persist + Audit `tenant.permissions.checked`.
+  - Implements: FR-006
+  - Implements: FR-008
+- [x] T045 [US6] Tests: Unit compare + Feature Tenant detail status + Verify updates.
+
+## Phase 9b: Scope-Ausrichtung auf neue Objekttypen
+
+- [x] T028 [Scope] `config/tenantpilot.php` auf `scope.supported_types` erweitern; single source for sync/backup/restore.
+- [x] T029 [Scope] Filament-UI an neue Typen anpassen (Filter/Grouping + Restore-Level Hinweise).
+
+## Phase 10: Housekeeping – Delete-Funktionen für Backups & Versions
+
+- [x] T060 [HK] BackupSets soft deletable + Guard gegen RestoreRuns.
+- [x] T061 [HK] Filament Delete BackupSets + Confirmation + Audit (`backup.deleted`) + Guard.
+- [x] T062 [HK] PolicyVersions soft deletable + Queries/Resources default non-deleted.
+- [x] T063 [HK] Filament Delete PolicyVersions + Audit (`policy_version.deleted`).
+- [x] T064 [HK] Tests Housekeeping (BackupSet delete ok/block + PolicyVersion delete).
+
+## Phase 11: Housekeeping – Tenant löschen/deaktivieren
+
+- [x] T070 [HK] Tenants soft deletable (optional status active/archived).
+- [x] T071 [HK] Tenant deactivate/archive action + Audit (`tenant.archived`).
+- [x] T072 [HK] Block operations for deactivated tenants (sync/backup/restore early fail).
+- [x] T073 [HK] RestoreRuns soft deletable (optional) + Audit (`restore_run.deleted`).
+- [x] T074 [HK] Tests Tenant delete/deactivate behavior + clear errors, no Graph calls.
+
+## Phase 12: Housekeeping – Hard Deletes (Force Delete)
+
+- [x] T075 [HK] Force-Delete-Actions (only in trashed; guards; audit before delete) + tests.
+
+## Phase 12b: Single current tenant ("Highlander")
+
+- [x] T120 [TENANT] Migration add `is_current` + partial unique index.
+- [x] T121 [TENANT] Tenant::current() + makeCurrent() + remove implicit defaults.
+- [x] T122 [TENANT] Data cleanup (mark one current; archive local-tenant).
+- [x] T123 [TENANT] Filament UI badge + “Make current” action.
+- [x] T124 [TENANT] Consumers refactor to `Tenant::current()` or explicit tenant.
+- [x] T125 [TENANT] Tests for current selection + “Make current”.
+
+- [x] T130 [UX] Tabellen-Aktionen in Dropdown bündeln (ActionGroup) in TenantResource (+ optional others).
+
+## Phase 13: Settings Normalization & Display (Priority: P1)
+
+- [x] T140 [P] [US1b] Unit test PolicyNormalizer.
+- [x] T141 [P] [US1b] Feature test Policy Settings section.
+- [x] T142 [P] [US1b] Feature test Version detail pretty JSON + normalized.
+- [x] T143 [P] [Edge] Feature test malformed snapshot warning.
+- [x] T144 [P] [Edge] Feature test @odata.type mismatch flag + restore exec block.
+
+- [x] T145 [US1b] PolicyNormalizer service.
+  - Implements: FR-019
+- [x] T146 [US1b] Settings infolist in PolicyResource.
+- [x] T147 [US1b] PolicyVersion view pretty JSON + normalized.
+- [x] T148 [US1b] Integrations (list badge, optional diff enhancements, tenant scoping).
+- [x] T149 [Edge] SnapshotValidator helper.
+- [x] T150 [Edge] @odata.type validator (policy/backup/restore gates).
+- [x] T151 [Edge] UI warnings + restore execution gating (preview may show).
+- [x] T152 [US1b] README docs for settings display.
+- [x] T153 [US1b] Inline docs in PolicyNormalizer.
+
+## Phase 14: User Story 7 – Intune RBAC Onboarding Wizard (Delegated, Synchronous)
+
+**Scope**: FR-023 to FR-030; delegated login and grant run synchronously in Filament (no queue for grant). Optional jobs/CLI only for CHECK/REPORT (no grant).
+
+- [x] T160 [US7] Add TenantResource ActionGroup entry “Setup Intune RBAC”: visible for active tenants with `app_client_id`; sits alongside Admin consent/Verify; guarded by Highlander rules. Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
+  - Implements: FR-023
+  - Implements: FR-025
+  - Implements: FR-026
+  - Implements: FR-027
+  - Implements: FR-028
+  - Implements: FR-029
+- [x] T161 [US7] Wizard UI (Filament): Step 1 preconditions/summary with inputs for Role, Scope, Group mode; least-privilege warnings; review screen of planned changes. Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
+- [x] T162 [US7] Delegated auth step: initiate delegated login; stop with clear error + audit on failure; token not persisted. Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
+- [x] T163 [US7] Execution service (sync) with audit per step: resolve SP by `app_client_id`; ensure/create security group (`securityEnabled=true`); add SP as member (idempotent); ensure/create/update Intune role assignment; persist IDs on tenant for idempotency. Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
+  - Implements: FR-030
+
+- [x] T164 [US7] Post-check (mandatory): clear app token cache / force fresh token acquisition and run canary reads:
+  - `GET /deviceManagement/deviceConfigurations?$top=1`
+  - `GET /deviceManagement/deviceCompliancePolicies?$top=1`
+  - optional CA canary only if CA features enabled
+  - update tenant health + audit verify outcome.
+  Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
+
+- [x] T165 [US7] Tests (Pest, mocked Graph): happy path; rerun idempotent; missing permissions error mapping; scope-limited warning; delegated login failure path; non-security-enabled group failure. Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
+- [x] T166 [US7] Documentation: README note for wizard behavior (delegated, sync), least-privilege defaults, audit expectations, rerun safety. Verified by: manual review of README.md update.
+- [ ] T167 [US7-Optional] CLI/Job for CHECK/REPORT only (no grant), explicitly exclude async grant.
+- [x] T168 [US7] Extend Verify configuration / Health panel to include “Intune RBAC status” (OK/Missing/Error) + CTA “Run Setup Intune RBAC”, persist last_checked_at + reason; Audit `tenant.rbac.checked`. Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
+  - Implements: FR-024
+- [x] T169 [US7] Persist RBAC artifacts on Tenant for idempotency:
+  - migration add nullable columns: `rbac_group_id`, `rbac_group_name`, `rbac_role_assignment_id`, `rbac_role_key`, `rbac_scope_mode`, `rbac_scope_id`
+  - prefer stored IDs on reruns; discovery fallback.
+  Verified by: `./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php`.
+
+## Phase 15: User Story 8 – Graph Contract Registry & Drift Guard
+
+**Scope**: FR-031 to FR-034; contract registry per type, type-family handling, capability fallbacks, drift checks.
+
+- [x] T170 [US8] Add contract registry artifact (e.g., `config/graph_contracts.php`) capturing per supported type: resource paths, allowed `$select`/`$expand`, allowed @odata.type family, create/update methods, id field, hydration strategy. Verified by: manual review.
+- [x] T171 [US8] Implement registry service + integration in Graph client to enforce allowed capabilities and downgrade on capability errors (retry without expand/select), logging warnings/audit entries. Verified by: `./vendor/bin/pest tests/Unit/GraphContractFallbackTest.php`.
+  - Implements: FR-032
+- [x] T172 [US8] Implement type-family handling so derived @odata.type within a family routes correctly for preview/restore (no odata_mismatch) while still blocking unknown types. Verified by: `./vendor/bin/pest tests/Feature/Filament/ODataTypeMismatchTest.php`.
+  - Implements: FR-033
+- [x] T173 [US8] Add verification command `php artisan graph:contract:check` (staging/CI) to probe endpoints, detect drift, and emit actionable diff/log output; make prod opt-in/guarded. Verified by: manual review.
+  - Implements: FR-034
+- [x] T174 [US8] Tests (Pest/unit/integration): registry lookups, fallback selection on capability errors, derived type acceptance, drift-check command behavior. Verified by: `./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php tests/Unit/GraphContractFallbackTest.php`.
+- [x] T175 [US8] Documentation: describe registry format/update process, fallback behavior, and how/when to run `graph:contract:check`. Verified by: manual review of README update.
diff --git a/specs/002-filament-json/plan.md b/specs/002-filament-json/plan.md
new file mode 100644
index 0000000..97be9ba
--- /dev/null
+++ b/specs/002-filament-json/plan.md
@@ -0,0 +1,107 @@
+```markdown
+# Implementation Plan: [FEATURE]
+
+**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
+**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
+
+**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
+
+## Summary
+
+[Extract from feature spec: primary requirement + technical approach from research]
+
+## Technical Context
+
+
+
+**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]  
+**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]  
+**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]  
+**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]  
+**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
+**Project Type**: [single/web/mobile - determines source structure]  
+**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]  
+**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]  
+**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
+
+## Constitution Check
+
+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
+
+[Gates determined based on constitution file]
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/[###-feature]/
+├── plan.md              # This file (/speckit.plan command output)
+├── research.md          # Phase 0 output (/speckit.plan command)
+├── data-model.md        # Phase 1 output (/speckit.plan command)
+├── quickstart.md        # Phase 1 output (/speckit.plan command)
+├── contracts/           # Phase 1 output (/speckit.plan command)
+└── tasks.md             # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
+```
+
+### Source Code (repository root)
+
+
+```text
+# [REMOVE IF UNUSED] Option 1: Single project (DEFAULT)
+src/
+├── models/
+├── services/
+├── cli/
+└── lib/
+
+tests/
+├── contract/
+├── integration/
+└── unit/
+
+# [REMOVE IF UNUSED] Option 2: Web application (when "frontend" + "backend" detected)
+backend/
+├── src/
+│   ├── models/
+│   ├── services/
+│   └── api/
+└── tests/
+
+frontend/
+├── src/
+│   ├── components/
+│   ├── pages/
+│   └── services/
+└── tests/
+
+# [REMOVE IF UNUSED] Option 3: Mobile + API (when "iOS/Android" detected)
+api/
+└── [same as backend above]
+
+ios/ or android/
+└── [platform-specific structure: feature modules, UI flows, platform tests]
+```
+
+**Structure Decision**: [Document the selected structure and reference the real
+directories captured above]
+
+## Complexity Tracking
+
+> **Fill ONLY if Constitution Check has violations that must be justified**
+
+| Violation | Why Needed | Simpler Alternative Rejected Because |
+|-----------|------------|-------------------------------------|
+| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
+| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
+
+```
\ No newline at end of file
diff --git a/specs/002-filament-json/spec.md b/specs/002-filament-json/spec.md
new file mode 100644
index 0000000..b90c06d
--- /dev/null
+++ b/specs/002-filament-json/spec.md
@@ -0,0 +1,118 @@
+```markdown
+# Feature Specification: [FEATURE NAME]
+
+**Feature Branch**: `[###-feature-name]`  
+**Created**: [DATE]  
+**Status**: Draft  
+**Input**: User description: "$ARGUMENTS"
+
+## User Scenarios & Testing *(mandatory)*
+
+
+
+### User Story 1 - [Brief Title] (Priority: P1)
+
+[Describe this user journey in plain language]
+
+**Why this priority**: [Explain the value and why it has this priority level]
+
+**Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"]
+
+**Acceptance Scenarios**:
+
+1. **Given** [initial state], **When** [action], **Then** [expected outcome]
+2. **Given** [initial state], **When** [action], **Then** [expected outcome]
+
+---
+
+### User Story 2 - [Brief Title] (Priority: P2)
+
+[Describe this user journey in plain language]
+
+**Why this priority**: [Explain the value and why it has this priority level]
+
+**Independent Test**: [Describe how this can be tested independently]
+
+**Acceptance Scenarios**:
+
+1. **Given** [initial state], **When** [action], **Then** [expected outcome]
+
+---
+
+### User Story 3 - [Brief Title] (Priority: P3)
+
+[Describe this user journey in plain language]
+
+**Why this priority**: [Explain the value and why it has this priority level]
+
+**Independent Test**: [Describe how this can be tested independently]
+
+**Acceptance Scenarios**:
+
+1. **Given** [initial state], **When** [action], **Then** [expected outcome]
+
+---
+
+[Add more user stories as needed, each with an assigned priority]
+
+### Edge Cases
+
+
+
+- What happens when [boundary condition]?
+- How does system handle [error scenario]?
+
+## Requirements *(mandatory)*
+
+
+
+### Functional Requirements
+
+- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]
+- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]  
+- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]
+- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]
+- **FR-005**: System MUST [behavior, e.g., "log all security events"]
+
+*Example of marking unclear requirements:*
+
+- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]
+- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]
+
+### Key Entities *(include if feature involves data)*
+
+- **[Entity 1]**: [What it represents, key attributes without implementation]
+- **[Entity 2]**: [What it represents, relationships to other entities]
+
+## Success Criteria *(mandatory)*
+
+
+
+### Measurable Outcomes
+
+- **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"]
+- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"]
+- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"]
+- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"]
+
+```
\ No newline at end of file
diff --git a/specs/002-filament-json/tasks.md b/specs/002-filament-json/tasks.md
new file mode 100644
index 0000000..26125a8
--- /dev/null
+++ b/specs/002-filament-json/tasks.md
@@ -0,0 +1,250 @@
+````markdown
+---
+
+description: "Task list template for feature implementation"
+---
+
+# Tasks: [FEATURE NAME]
+
+**Input**: Design documents from `/specs/[###-feature-name]/`
+**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
+
+**Tests**: The examples below include test tasks. Tests are OPTIONAL - only include them if explicitly requested in the feature specification.
+
+**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
+
+## Format: `[ID] [P?] [Story] Description`
+
+- **[P]**: Can run in parallel (different files, no dependencies)
+- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
+- Include exact file paths in descriptions
+
+## Path Conventions
+
+- **Single project**: `src/`, `tests/` at repository root
+- **Web app**: `backend/src/`, `frontend/src/`
+- **Mobile**: `api/src/`, `ios/src/` or `android/src/`
+- Paths shown below assume single project - adjust based on plan.md structure
+
+
+
+## Phase 1: Setup (Shared Infrastructure)
+
+**Purpose**: Project initialization and basic structure
+
+- [ ] T001 Create project structure per implementation plan
+- [ ] T002 Initialize [language] project with [framework] dependencies
+- [ ] T003 [P] Configure linting and formatting tools
+
+---
+
+## Phase 2: Foundational (Blocking Prerequisites)
+
+**Purpose**: Core infrastructure that MUST be complete before ANY user story can be implemented
+
+**⚠️ CRITICAL**: No user story work can begin until this phase is complete
+
+Examples of foundational tasks (adjust based on your project):
+
+- [ ] T004 Setup database schema and migrations framework
+- [ ] T005 [P] Implement authentication/authorization framework
+- [ ] T006 [P] Setup API routing and middleware structure
+- [ ] T007 Create base models/entities that all stories depend on
+- [ ] T008 Configure error handling and logging infrastructure
+- [ ] T009 Setup environment configuration management
+
+**Checkpoint**: Foundation ready - user story implementation can now begin in parallel
+
+---
+
+## Phase 3: User Story 1 - [Title] (Priority: P1) 🎯 MVP
+
+**Goal**: [Brief description of what this story delivers]
+
+**Independent Test**: [How to verify this story works on its own]
+
+### Tests for User Story 1 (OPTIONAL - only if tests requested) ⚠️
+
+> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
+
+- [ ] T010 [P] [US1] Contract test for [endpoint] in tests/contract/test_[name].py
+- [ ] T011 [P] [US1] Integration test for [user journey] in tests/integration/test_[name].py
+
+### Implementation for User Story 1
+
+- [ ] T012 [P] [US1] Create [Entity1] model in src/models/[entity1].py
+- [ ] T013 [P] [US1] Create [Entity2] model in src/models/[entity2].py
+- [ ] T014 [US1] Implement [Service] in src/services/[service].py (depends on T012, T013)
+- [ ] T015 [US1] Implement [endpoint/feature] in src/[location]/[file].py
+- [ ] T016 [US1] Add validation and error handling
+- [ ] T017 [US1] Add logging for user story 1 operations
+
+**Checkpoint**: At this point, User Story 1 should be fully functional and testable independently
+
+---
+
+## Phase 4: User Story 2 - [Title] (Priority: P2)
+
+**Goal**: [Brief description of what this story delivers]
+
+**Independent Test**: [How to verify this story works on its own]
+
+### Tests for User Story 2 (OPTIONAL - only if tests requested) ⚠️
+
+- [ ] T018 [P] [US2] Contract test for [endpoint] in tests/contract/test_[name].py
+- [ ] T019 [P] [US2] Integration test for [user journey] in tests/integration/test_[name].py
+
+### Implementation for User Story 2
+
+- [ ] T020 [P] [US2] Create [Entity] model in src/models/[entity].py
+- [ ] T021 [US2] Implement [Service] in src/services/[service].py
+- [ ] T022 [US2] Implement [endpoint/feature] in src/[location]/[file].py
+- [ ] T023 [US2] Integrate with User Story 1 components (if needed)
+
+**Checkpoint**: At this point, User Stories 1 AND 2 should both work independently
+
+---
+
+## Phase 5: User Story 3 - [Title] (Priority: P3)
+
+**Goal**: [Brief description of what this story delivers]
+
+**Independent Test**: [How to verify this story works on its own]
+
+### Tests for User Story 3 (OPTIONAL - only if tests requested) ⚠️
+
+- [ ] T024 [P] [US3] Contract test for [endpoint] in tests/contract/test_[name].py
+- [ ] T025 [P] [US3] Integration test for [user journey] in tests/integration/test_[name].py
+
+### Implementation for User Story 3
+
+- [ ] T026 [P] [US3] Create [Entity] model in src/models/[entity].py
+- [ ] T027 [US3] Implement [Service] in src/services/[service].py
+- [ ] T028 [US3] Implement [endpoint/feature] in src/[location]/[file].py
+
+**Checkpoint**: All user stories should now be independently functional
+
+---
+
+[Add more user story phases as needed, following the same pattern]
+
+---
+
+## Phase N: Polish & Cross-Cutting Concerns
+
+**Purpose**: Improvements that affect multiple user stories
+
+- [ ] TXXX [P] Documentation updates in docs/
+- [ ] TXXX Code cleanup and refactoring
+- [ ] TXXX Performance optimization across all stories
+- [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/
+- [ ] TXXX Security hardening
+- [ ] TXXX Run quickstart.md validation
+
+---
+
+## Dependencies & Execution Order
+
+### Phase Dependencies
+
+- **Setup (Phase 1)**: No dependencies - can start immediately
+- **Foundational (Phase 2)**: Depends on Setup completion - BLOCKS all user stories
+- **User Stories (Phase 3+)**: All depend on Foundational phase completion
+  - User stories can then proceed in parallel (if staffed)
+  - Or sequentially in priority order (P1 → P2 → P3)
+- **Polish (Final Phase)**: Depends on all desired user stories being complete
+
+### User Story Dependencies
+
+- **User Story 1 (P1)**: Can start after Foundational (Phase 2) - No dependencies on other stories
+- **User Story 2 (P2)**: Can start after Foundational (Phase 2) - May integrate with US1 but should be independently testable
+- **User Story 3 (P3)**: Can start after Foundational (Phase 2) - May integrate with US1/US2 but should be independently testable
+
+### Within Each User Story
+
+- Tests (if included) MUST be written and FAIL before implementation
+- Models before services
+- Services before endpoints
+- Core implementation before integration
+- Story complete before moving to next priority
+
+### Parallel Opportunities
+
+- All Setup tasks marked [P] can run in parallel
+- All Foundational tasks marked [P] can run in parallel (within Phase 2)
+- Once Foundational phase completes, all user stories can start in parallel (if team capacity allows)
+- All tests for a user story marked [P] can run in parallel
+- Models within a story marked [P] can run in parallel
+- Different user stories can be worked on in parallel by different team members
+
+### Parallel Example: User Story 1
+
+```bash
+# Launch all tests for User Story 1 together (if tests requested):
+Task: "Contract test for [endpoint] in tests/contract/test_[name].py"
+Task: "Integration test for [user journey] in tests/integration/test_[name].py"
+
+# Launch all models for User Story 1 together:
+Task: "Create [Entity1] model in src/models/[entity1].py"
+Task: "Create [Entity2] model in src/models/[entity2].py"
+```
+
+---
+
+## Implementation Strategy
+
+### MVP First (User Story 1 Only)
+
+1. Complete Phase 1: Setup
+2. Complete Phase 2: Foundational (CRITICAL - blocks all stories)
+3. Complete Phase 3: User Story 1
+4. **STOP and VALIDATE**: Test User Story 1 independently
+5. Deploy/demo if ready
+
+### Incremental Delivery
+
+1. Complete Setup + Foundational → Foundation ready
+2. Add User Story 1 → Test independently → Deploy/Demo (MVP!)
+3. Add User Story 2 → Test independently → Deploy/Demo
+4. Add User Story 3 → Test independently → Deploy/Demo
+5. Each story adds value without breaking previous stories
+
+### Parallel Team Strategy
+
+With multiple developers:
+
+1. Team completes Setup + Foundational together
+2. Once Foundational is done:
+   - Developer A: User Story 1
+   - Developer B: User Story 2
+   - Developer C: User Story 3
+3. Stories complete and integrate independently
+
+### Notes
+
+- [P] tasks = different files, no dependencies
+- [Story] label maps task to specific user story for traceability
+- Each user story should be independently completable and testable
+- Verify tests fail before implementing
+- Commit after each task or logical group
+- Stop at any checkpoint to validate story independently
+- Avoid: vague tasks, same file conflicts, cross-story dependencies that break independence
+
+````markdown
-- 
2.45.2


From 06382aed018dad9074dfcc1709863af4b1863521 Mon Sep 17 00:00:00 2001
From: Ahmed Darrazi 
Date: Sat, 13 Dec 2025 22:56:45 +0100
Subject: [PATCH 02/18] Fix: Address 'Too few arguments' error in settings
 catalog view

---
 .../settings-catalog-grouped.blade.php        | 160 ++++++++++++++++++
 1 file changed, 160 insertions(+)
 create mode 100644 resources/views/filament/infolists/entries/settings-catalog-grouped.blade.php

diff --git a/resources/views/filament/infolists/entries/settings-catalog-grouped.blade.php b/resources/views/filament/infolists/entries/settings-catalog-grouped.blade.php
new file mode 100644
index 0000000..1be2232
--- /dev/null
+++ b/resources/views/filament/infolists/entries/settings-catalog-grouped.blade.php
@@ -0,0 +1,160 @@
+@php
+    use Illuminate\Support\Str;
+    use function Filament\Support\evaluate;
+
+    // Normalize incoming state from Filament ViewEntry. Accept multiple shapes:
+    // - $groups passed directly
+    // - $state as array with ['groups' => [...]]
+    // - $state as JSON string
+    // - $state as a Closure
+    $groups = [];
+    $searchQuery = $searchQuery ?? '';
+
+    // If $state is a closure, resolve it first.
+    $state = evaluate($state);
+
+    if (isset($groups) && is_array($groups) && count($groups) > 0) {
+        // $groups already provided by caller
+        // leave as-is
+    } elseif (isset($state)) {
+        if (is_string($state) && Str::startsWith(trim($state), '{')) {
+            $decoded = json_decode($state, true);
+            if (is_array($decoded)) {
+                $groups = $decoded['groups'] ?? $decoded;
+            }
+        } elseif (is_array($state)) {
+            $groups = $state['groups'] ?? $state;
+        } elseif (is_object($state)) {
+            $arr = (array) $state;
+            $groups = $arr['groups'] ?? $arr;
+        }
+    }
+
+    // Ensure groups is an array
+    if (! is_array($groups)) {
+        $groups = [];
+    }
+@endphp
+
+
+ @if(empty($groups)) +
+

No settings available

+
+ @else + @foreach($groups as $groupIndex => $group) + @php + // Filter settings by search query + $filteredSettings = collect($group['settings'] ?? [])->filter(function($setting) use ($searchQuery) { + if (empty($searchQuery)) { + return true; + } + + $searchLower = strtolower($searchQuery); + return str_contains(strtolower($setting['label'] ?? ''), $searchLower) || + str_contains(strtolower($setting['value_display'] ?? ''), $searchLower); + })->all(); + + $settingCount = count($filteredSettings); + @endphp + + @if($settingCount > 0) + + + + {{ $settingCount }} {{ Str::plural('setting', $settingCount) }} + + + +
+ @foreach($filteredSettings as $setting) +
+
+
+ + {{ $setting['label'] }} + + + @if($setting['is_fallback'] ?? false) + + Definition not cached + + @endif +
+ + @if(!empty($setting['help_text'])) +

+ {{ Str::limit($setting['help_text'], 200) }} +

+ @endif +
+ +
+
+ @if(is_bool($setting['value_raw'])) + + {{ $setting['value_display'] }} + + @elseif(is_int($setting['value_raw'])) + + {{ $setting['value_display'] }} + + @else + + {{ $setting['value_display'] }} + + @endif +
+ + @if(strlen($setting['value_display'] ?? '') > 50) + + @endif +
+
+ @endforeach +
+
+ @endif + @endforeach + + @if(collect($groups)->sum(fn($g) => count(collect($g['settings'] ?? [])->filter(function($s) use ($searchQuery) { + if (empty($searchQuery)) return true; + $searchLower = strtolower($searchQuery); + return str_contains(strtolower($s['label'] ?? ''), $searchLower) || + str_contains(strtolower($s['value_display'] ?? ''), $searchLower); + }))) === 0) +
+

No settings match your search

+ +
+ @endif + @endif +
-- 2.45.2 From 82b342e7f41974dfe187149fa731e956ec229202 Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Sun, 14 Dec 2025 13:28:10 +0100 Subject: [PATCH 03/18] =?UTF-8?q?chore:=20settings-catalog=20=E2=80=94=20i?= =?UTF-8?q?nclude=20settings=20in=20PATCH,=20preserve=20@odata.type,=20upd?= =?UTF-8?q?ate=20sanitizers,=20restore=20flow,=20tests=20and=20views?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 16 + .gemini/commands/speckit.analyze.toml | 188 +++++ .gemini/commands/speckit.checklist.toml | 298 ++++++++ .gemini/commands/speckit.clarify.toml | 185 +++++ .gemini/commands/speckit.constitution.toml | 86 +++ .gemini/commands/speckit.implement.toml | 139 ++++ .gemini/commands/speckit.plan.toml | 93 +++ .gemini/commands/speckit.specify.toml | 262 +++++++ .gemini/commands/speckit.tasks.toml | 141 ++++ .gemini/commands/speckit.taskstoissues.toml | 34 + .gemini/settings.json | 5 + .npmignore | 8 + .prettierignore | 12 + .specify/research_t186.md | 46 ++ .specify/tasks.md | 33 + GEMINI.md | 671 ++++++++++++++++++ README.md | 34 + app/Console/Commands/GraphContractCheck.php | 68 ++ .../Commands/TestSettingsCatalogCache.php | 93 +++ app/Filament/Resources/PolicyResource.php | 216 +++++- .../PolicyResource/Pages/ViewPolicy.php | 45 ++ .../Resources/PolicyVersionResource.php | 96 ++- app/Filament/Resources/RestoreRunResource.php | 6 +- app/Livewire/SettingsCatalogSettingsTable.php | 125 ++++ app/Models/SettingsCatalogDefinition.php | 38 + app/Services/Graph/GraphContractRegistry.php | 381 ++++++++++ app/Services/Graph/MicrosoftGraphClient.php | 73 +- app/Services/Intune/BackupService.php | 68 +- app/Services/Intune/PolicyNormalizer.php | 592 ++++++++++++++- app/Services/Intune/PolicySnapshotService.php | 217 ++++++ app/Services/Intune/PolicySyncService.php | 10 + app/Services/Intune/RestoreService.php | 270 ++++++- .../SettingsCatalogDefinitionResolver.php | 272 +++++++ app/Services/Intune/VersionService.php | 30 +- .../Concerns/InteractsWithODataTypes.php | 16 +- composer.json | 3 +- composer.lock | 158 ++++- config/graph_contracts.php | 185 +++++ config/intune_permissions.php | 7 + config/tenantpilot.php | 10 + ...ate_settings_catalog_definitions_table.php | 40 ++ .../filament-json/filament-json-styles.css | 2 + .../entries/normalized-settings.blade.php | 83 ++- .../policy-settings-standard.blade.php | 151 ++++ .../entries/restore-results.blade.php | 153 ++++ .../settings-catalog-grouped.blade.php | 151 ++-- .../infolists/entries/snapshot-json.blade.php | 38 +- ...settings-catalog-setting-details.blade.php | 70 ++ .../settings-catalog-settings-table.blade.php | 5 + specs/001-rbac-onboarding/plan.md | 170 ++--- specs/002-filament-json/DEPLOYMENT.md | 251 +++++++ specs/002-filament-json/plan.md | 388 ++++++++-- specs/002-filament-json/quickstart.md | 385 ++++++++++ specs/002-filament-json/research.md | 332 +++++++++ specs/002-filament-json/spec.md | 213 ++++-- specs/002-filament-json/tasks.md | 424 ++++++----- .../IMPLEMENTATION_STATUS.md | 469 ++++++++++++ .../MANUAL_VERIFICATION_GUIDE.md | 312 ++++++++ specs/185-settings-catalog-readable/plan.md | 414 +++++++++++ specs/185-settings-catalog-readable/spec.md | 240 +++++++ specs/185-settings-catalog-readable/tasks.md | 472 ++++++++++++ tests/Feature/Filament/BackupCreationTest.php | 5 + .../Filament/ODataTypeMismatchTest.php | 7 +- .../PolicyVersionReadableLayoutTest.php | 80 +++ .../Feature/Filament/RestoreExecutionTest.php | 5 + tests/Feature/Filament/RestorePreviewTest.php | 5 + .../SettingsCatalogPolicyHydrationTest.php | 156 ++++ ...ingsCatalogPolicyNormalizedDisplayTest.php | 110 +++ .../SettingsCatalogPolicySyncTest.php | 128 ++++ ...gsCatalogRestoreApplySettingsPatchTest.php | 174 +++++ .../Filament/SettingsCatalogRestoreTest.php | 304 ++++++++ ...SettingsCatalogSettingsTableRenderTest.php | 78 ++ tests/Feature/Filament/TenantSetupTest.php | 5 + tests/Unit/GraphContractFallbackTest.php | 66 ++ .../GraphContractRegistryActualDataTest.php | 51 ++ ...ractRegistrySettingsApplySanitizerTest.php | 67 ++ ...tractRegistrySettingsWriteStrategyTest.php | 30 + tests/Unit/GraphContractRegistryTest.php | 106 +++ ...cyNormalizerSettingsCatalogFlattenTest.php | 92 +++ .../PolicyNormalizerSettingsCatalogTest.php | 33 + tests/Unit/PolicyNormalizerTest.php | 5 +- 81 files changed, 10807 insertions(+), 693 deletions(-) create mode 100644 .dockerignore create mode 100644 .gemini/commands/speckit.analyze.toml create mode 100644 .gemini/commands/speckit.checklist.toml create mode 100644 .gemini/commands/speckit.clarify.toml create mode 100644 .gemini/commands/speckit.constitution.toml create mode 100644 .gemini/commands/speckit.implement.toml create mode 100644 .gemini/commands/speckit.plan.toml create mode 100644 .gemini/commands/speckit.specify.toml create mode 100644 .gemini/commands/speckit.tasks.toml create mode 100644 .gemini/commands/speckit.taskstoissues.toml create mode 100644 .gemini/settings.json create mode 100644 .npmignore create mode 100644 .prettierignore create mode 100644 .specify/research_t186.md create mode 100644 GEMINI.md create mode 100644 app/Console/Commands/GraphContractCheck.php create mode 100644 app/Console/Commands/TestSettingsCatalogCache.php create mode 100644 app/Livewire/SettingsCatalogSettingsTable.php create mode 100644 app/Models/SettingsCatalogDefinition.php create mode 100644 app/Services/Graph/GraphContractRegistry.php create mode 100644 app/Services/Intune/PolicySnapshotService.php create mode 100644 app/Services/Intune/SettingsCatalogDefinitionResolver.php create mode 100644 config/graph_contracts.php create mode 100644 database/migrations/2025_12_13_212126_create_settings_catalog_definitions_table.php create mode 100644 public/css/pepperfm/filament-json/filament-json-styles.css create mode 100644 resources/views/filament/infolists/entries/policy-settings-standard.blade.php create mode 100644 resources/views/filament/infolists/entries/restore-results.blade.php create mode 100644 resources/views/filament/modals/settings-catalog-setting-details.blade.php create mode 100644 resources/views/livewire/settings-catalog-settings-table.blade.php create mode 100644 specs/002-filament-json/DEPLOYMENT.md create mode 100644 specs/002-filament-json/quickstart.md create mode 100644 specs/002-filament-json/research.md create mode 100644 specs/185-settings-catalog-readable/IMPLEMENTATION_STATUS.md create mode 100644 specs/185-settings-catalog-readable/MANUAL_VERIFICATION_GUIDE.md create mode 100644 specs/185-settings-catalog-readable/plan.md create mode 100644 specs/185-settings-catalog-readable/spec.md create mode 100644 specs/185-settings-catalog-readable/tasks.md create mode 100644 tests/Feature/Filament/PolicyVersionReadableLayoutTest.php create mode 100644 tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php create mode 100644 tests/Feature/Filament/SettingsCatalogPolicyNormalizedDisplayTest.php create mode 100644 tests/Feature/Filament/SettingsCatalogPolicySyncTest.php create mode 100644 tests/Feature/Filament/SettingsCatalogRestoreApplySettingsPatchTest.php create mode 100644 tests/Feature/Filament/SettingsCatalogRestoreTest.php create mode 100644 tests/Feature/Filament/SettingsCatalogSettingsTableRenderTest.php create mode 100644 tests/Unit/GraphContractFallbackTest.php create mode 100644 tests/Unit/GraphContractRegistryActualDataTest.php create mode 100644 tests/Unit/GraphContractRegistrySettingsApplySanitizerTest.php create mode 100644 tests/Unit/GraphContractRegistrySettingsWriteStrategyTest.php create mode 100644 tests/Unit/GraphContractRegistryTest.php create mode 100644 tests/Unit/PolicyNormalizerSettingsCatalogFlattenTest.php create mode 100644 tests/Unit/PolicyNormalizerSettingsCatalogTest.php diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9503447 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +node_modules/ +vendor/ +.git/ +.env +.env.* +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +public/build/ +public/hot/ +storage/debugbar/ +storage/*.key +/references/ +.idea/ +.vscode/ diff --git a/.gemini/commands/speckit.analyze.toml b/.gemini/commands/speckit.analyze.toml new file mode 100644 index 0000000..fbc847c --- /dev/null +++ b/.gemini/commands/speckit.analyze.toml @@ -0,0 +1,188 @@ +description = "Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation." + +prompt = """ +--- +description: Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation. +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Goal + +Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit.tasks` has successfully produced a complete `tasks.md`. + +## Operating Constraints + +**STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually). + +**Constitution Authority**: The project constitution (`.specify/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit.analyze`. + +## Execution Steps + +### 1. Initialize Analysis Context + +Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths: + +- SPEC = FEATURE_DIR/spec.md +- PLAN = FEATURE_DIR/plan.md +- TASKS = FEATURE_DIR/tasks.md + +Abort with an error message if any required file is missing (instruct the user to run missing prerequisite command). +For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\\''m Groot' (or double-quote if possible: "I'm Groot"). + +### 2. Load Artifacts (Progressive Disclosure) + +Load only the minimal necessary context from each artifact: + +**From spec.md:** + +- Overview/Context +- Functional Requirements +- Non-Functional Requirements +- User Stories +- Edge Cases (if present) + +**From plan.md:** + +- Architecture/stack choices +- Data Model references +- Phases +- Technical constraints + +**From tasks.md:** + +- Task IDs +- Descriptions +- Phase grouping +- Parallel markers [P] +- Referenced file paths + +**From constitution:** + +- Load `.specify/memory/constitution.md` for principle validation + +### 3. Build Semantic Models + +Create internal representations (do not include raw artifacts in output): + +- **Requirements inventory**: Each functional + non-functional requirement with a stable key (derive slug based on imperative phrase; e.g., "User can upload file" → `user-can-upload-file`) +- **User story/action inventory**: Discrete user actions with acceptance criteria +- **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases) +- **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements + +### 4. Detection Passes (Token-Efficient Analysis) + +Focus on high-signal findings. Limit to 50 findings total; aggregate remainder in overflow summary. + +#### A. Duplication Detection + +- Identify near-duplicate requirements +- Mark lower-quality phrasing for consolidation + +#### B. Ambiguity Detection + +- Flag vague adjectives (fast, scalable, secure, intuitive, robust) lacking measurable criteria +- Flag unresolved placeholders (TODO, TKTK, ???, ``, etc.) + +#### C. Underspecification + +- Requirements with verbs but missing object or measurable outcome +- User stories missing acceptance criteria alignment +- Tasks referencing files or components not defined in spec/plan + +#### D. Constitution Alignment + +- Any requirement or plan element conflicting with a MUST principle +- Missing mandated sections or quality gates from constitution + +#### E. Coverage Gaps + +- Requirements with zero associated tasks +- Tasks with no mapped requirement/story +- Non-functional requirements not reflected in tasks (e.g., performance, security) + +#### F. Inconsistency + +- Terminology drift (same concept named differently across files) +- Data entities referenced in plan but absent in spec (or vice versa) +- Task ordering contradictions (e.g., integration tasks before foundational setup tasks without dependency note) +- Conflicting requirements (e.g., one requires Next.js while other specifies Vue) + +### 5. Severity Assignment + +Use this heuristic to prioritize findings: + +- **CRITICAL**: Violates constitution MUST, missing core spec artifact, or requirement with zero coverage that blocks baseline functionality +- **HIGH**: Duplicate or conflicting requirement, ambiguous security/performance attribute, untestable acceptance criterion +- **MEDIUM**: Terminology drift, missing non-functional task coverage, underspecified edge case +- **LOW**: Style/wording improvements, minor redundancy not affecting execution order + +### 6. Produce Compact Analysis Report + +Output a Markdown report (no file writes) with the following structure: + +## Specification Analysis Report + +| ID | Category | Severity | Location(s) | Summary | Recommendation | +|----|----------|----------|-------------|---------|----------------| +| A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version | + +(Add one row per finding; generate stable IDs prefixed by category initial.) + +**Coverage Summary Table:** + +| Requirement Key | Has Task? | Task IDs | Notes | +|-----------------|-----------|----------|-------| + +**Constitution Alignment Issues:** (if any) + +**Unmapped Tasks:** (if any) + +**Metrics:** + +- Total Requirements +- Total Tasks +- Coverage % (requirements with >=1 task) +- Ambiguity Count +- Duplication Count +- Critical Issues Count + +### 7. Provide Next Actions + +At end of report, output a concise Next Actions block: + +- If CRITICAL issues exist: Recommend resolving before `/speckit.implement` +- If only LOW/MEDIUM: User may proceed, but provide improvement suggestions +- Provide explicit command suggestions: e.g., "Run /speckit.specify with refinement", "Run /speckit.plan to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'" + +### 8. Offer Remediation + +Ask the user: "Would you like me to suggest concrete remediation edits for the top N issues?" (Do NOT apply them automatically.) + +## Operating Principles + +### Context Efficiency + +- **Minimal high-signal tokens**: Focus on actionable findings, not exhaustive documentation +- **Progressive disclosure**: Load artifacts incrementally; don't dump all content into analysis +- **Token-efficient output**: Limit findings table to 50 rows; summarize overflow +- **Deterministic results**: Rerunning without changes should produce consistent IDs and counts + +### Analysis Guidelines + +- **NEVER modify files** (this is read-only analysis) +- **NEVER hallucinate missing sections** (if absent, report them accurately) +- **Prioritize constitution violations** (these are always CRITICAL) +- **Use examples over exhaustive rules** (cite specific instances, not generic patterns) +- **Report zero issues gracefully** (emit success report with coverage statistics) + +## Context + +{{args}} +""" diff --git a/.gemini/commands/speckit.checklist.toml b/.gemini/commands/speckit.checklist.toml new file mode 100644 index 0000000..7951c88 --- /dev/null +++ b/.gemini/commands/speckit.checklist.toml @@ -0,0 +1,298 @@ +description = "Generate a custom checklist for the current feature based on user requirements." + +prompt = """ +--- +description: Generate a custom checklist for the current feature based on user requirements. +--- + +## Checklist Purpose: "Unit Tests for English" + +**CRITICAL CONCEPT**: Checklists are **UNIT TESTS FOR REQUIREMENTS WRITING** - they validate the quality, clarity, and completeness of requirements in a given domain. + +**NOT for verification/testing**: + +- ❌ NOT "Verify the button clicks correctly" +- ❌ NOT "Test error handling works" +- ❌ NOT "Confirm the API returns 200" +- ❌ NOT checking if code/implementation matches the spec + +**FOR requirements quality validation**: + +- ✅ "Are visual hierarchy requirements defined for all card types?" (completeness) +- ✅ "Is 'prominent display' quantified with specific sizing/positioning?" (clarity) +- ✅ "Are hover state requirements consistent across all interactive elements?" (consistency) +- ✅ "Are accessibility requirements defined for keyboard navigation?" (coverage) +- ✅ "Does the spec define what happens when logo image fails to load?" (edge cases) + +**Metaphor**: If your spec is code written in English, the checklist is its unit test suite. You're testing whether the requirements are well-written, complete, unambiguous, and ready for implementation - NOT whether the implementation works. + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Execution Steps + +1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS list. + - All file paths must be absolute. + - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\\''m Groot' (or double-quote if possible: "I'm Groot"). + +2. **Clarify intent (dynamic)**: Derive up to THREE initial contextual clarifying questions (no pre-baked catalog). They MUST: + - Be generated from the user's phrasing + extracted signals from spec/plan/tasks + - Only ask about information that materially changes checklist content + - Be skipped individually if already unambiguous in `$ARGUMENTS` + - Prefer precision over breadth + + Generation algorithm: + 1. Extract signals: feature domain keywords (e.g., auth, latency, UX, API), risk indicators ("critical", "must", "compliance"), stakeholder hints ("QA", "review", "security team"), and explicit deliverables ("a11y", "rollback", "contracts"). + 2. Cluster signals into candidate focus areas (max 4) ranked by relevance. + 3. Identify probable audience & timing (author, reviewer, QA, release) if not explicit. + 4. Detect missing dimensions: scope breadth, depth/rigor, risk emphasis, exclusion boundaries, measurable acceptance criteria. + 5. Formulate questions chosen from these archetypes: + - Scope refinement (e.g., "Should this include integration touchpoints with X and Y or stay limited to local module correctness?") + - Risk prioritization (e.g., "Which of these potential risk areas should receive mandatory gating checks?") + - Depth calibration (e.g., "Is this a lightweight pre-commit sanity list or a formal release gate?") + - Audience framing (e.g., "Will this be used by the author only or peers during PR review?") + - Boundary exclusion (e.g., "Should we explicitly exclude performance tuning items this round?") + - Scenario class gap (e.g., "No recovery flows detected—are rollback / partial failure paths in scope?") + + Question formatting rules: + - If presenting options, generate a compact table with columns: Option | Candidate | Why It Matters + - Limit to A–E options maximum; omit table if a free-form answer is clearer + - Never ask the user to restate what they already said + - Avoid speculative categories (no hallucination). If uncertain, ask explicitly: "Confirm whether X belongs in scope." + + Defaults when interaction impossible: + - Depth: Standard + - Audience: Reviewer (PR) if code-related; Author otherwise + - Focus: Top 2 relevance clusters + + Output the questions (label Q1/Q2/Q3). After answers: if ≥2 scenario classes (Alternate / Exception / Recovery / Non-Functional domain) remain unclear, you MAY ask up to TWO more targeted follow‑ups (Q4/Q5) with a one-line justification each (e.g., "Unresolved recovery path risk"). Do not exceed five total questions. Skip escalation if user explicitly declines more. + +3. **Understand user request**: Combine `$ARGUMENTS` + clarifying answers: + - Derive checklist theme (e.g., security, review, deploy, ux) + - Consolidate explicit must-have items mentioned by user + - Map focus selections to category scaffolding + - Infer any missing context from spec/plan/tasks (do NOT hallucinate) + +4. **Load feature context**: Read from FEATURE_DIR: + - spec.md: Feature requirements and scope + - plan.md (if exists): Technical details, dependencies + - tasks.md (if exists): Implementation tasks + + **Context Loading Strategy**: + - Load only necessary portions relevant to active focus areas (avoid full-file dumping) + - Prefer summarizing long sections into concise scenario/requirement bullets + - Use progressive disclosure: add follow-on retrieval only if gaps detected + - If source docs are large, generate interim summary items instead of embedding raw text + +5. **Generate checklist** - Create "Unit Tests for Requirements": + - Create `FEATURE_DIR/checklists/` directory if it doesn't exist + - Generate unique checklist filename: + - Use short, descriptive name based on domain (e.g., `ux.md`, `api.md`, `security.md`) + - Format: `[domain].md` + - If file exists, append to existing file + - Number items sequentially starting from CHK001 + - Each `/speckit.checklist` run creates a NEW file (never overwrites existing checklists) + + **CORE PRINCIPLE - Test the Requirements, Not the Implementation**: + Every checklist item MUST evaluate the REQUIREMENTS THEMSELVES for: + - **Completeness**: Are all necessary requirements present? + - **Clarity**: Are requirements unambiguous and specific? + - **Consistency**: Do requirements align with each other? + - **Measurability**: Can requirements be objectively verified? + - **Coverage**: Are all scenarios/edge cases addressed? + + **Category Structure** - Group items by requirement quality dimensions: + - **Requirement Completeness** (Are all necessary requirements documented?) + - **Requirement Clarity** (Are requirements specific and unambiguous?) + - **Requirement Consistency** (Do requirements align without conflicts?) + - **Acceptance Criteria Quality** (Are success criteria measurable?) + - **Scenario Coverage** (Are all flows/cases addressed?) + - **Edge Case Coverage** (Are boundary conditions defined?) + - **Non-Functional Requirements** (Performance, Security, Accessibility, etc. - are they specified?) + - **Dependencies & Assumptions** (Are they documented and validated?) + - **Ambiguities & Conflicts** (What needs clarification?) + + **HOW TO WRITE CHECKLIST ITEMS - "Unit Tests for English"**: + + ❌ **WRONG** (Testing implementation): + - "Verify landing page displays 3 episode cards" + - "Test hover states work on desktop" + - "Confirm logo click navigates home" + + ✅ **CORRECT** (Testing requirements quality): + - "Are the exact number and layout of featured episodes specified?" [Completeness] + - "Is 'prominent display' quantified with specific sizing/positioning?" [Clarity] + - "Are hover state requirements consistent across all interactive elements?" [Consistency] + - "Are keyboard navigation requirements defined for all interactive UI?" [Coverage] + - "Is the fallback behavior specified when logo image fails to load?" [Edge Cases] + - "Are loading states defined for asynchronous episode data?" [Completeness] + - "Does the spec define visual hierarchy for competing UI elements?" [Clarity] + + **ITEM STRUCTURE**: + Each item should follow this pattern: + - Question format asking about requirement quality + - Focus on what's WRITTEN (or not written) in the spec/plan + - Include quality dimension in brackets [Completeness/Clarity/Consistency/etc.] + - Reference spec section `[Spec §X.Y]` when checking existing requirements + - Use `[Gap]` marker when checking for missing requirements + + **EXAMPLES BY QUALITY DIMENSION**: + + Completeness: + - "Are error handling requirements defined for all API failure modes? [Gap]" + - "Are accessibility requirements specified for all interactive elements? [Completeness]" + - "Are mobile breakpoint requirements defined for responsive layouts? [Gap]" + + Clarity: + - "Is 'fast loading' quantified with specific timing thresholds? [Clarity, Spec §NFR-2]" + - "Are 'related episodes' selection criteria explicitly defined? [Clarity, Spec §FR-5]" + - "Is 'prominent' defined with measurable visual properties? [Ambiguity, Spec §FR-4]" + + Consistency: + - "Do navigation requirements align across all pages? [Consistency, Spec §FR-10]" + - "Are card component requirements consistent between landing and detail pages? [Consistency]" + + Coverage: + - "Are requirements defined for zero-state scenarios (no episodes)? [Coverage, Edge Case]" + - "Are concurrent user interaction scenarios addressed? [Coverage, Gap]" + - "Are requirements specified for partial data loading failures? [Coverage, Exception Flow]" + + Measurability: + - "Are visual hierarchy requirements measurable/testable? [Acceptance Criteria, Spec §FR-1]" + - "Can 'balanced visual weight' be objectively verified? [Measurability, Spec §FR-2]" + + **Scenario Classification & Coverage** (Requirements Quality Focus): + - Check if requirements exist for: Primary, Alternate, Exception/Error, Recovery, Non-Functional scenarios + - For each scenario class, ask: "Are [scenario type] requirements complete, clear, and consistent?" + - If scenario class missing: "Are [scenario type] requirements intentionally excluded or missing? [Gap]" + - Include resilience/rollback when state mutation occurs: "Are rollback requirements defined for migration failures? [Gap]" + + **Traceability Requirements**: + - MINIMUM: ≥80% of items MUST include at least one traceability reference + - Each item should reference: spec section `[Spec §X.Y]`, or use markers: `[Gap]`, `[Ambiguity]`, `[Conflict]`, `[Assumption]` + - If no ID system exists: "Is a requirement & acceptance criteria ID scheme established? [Traceability]" + + **Surface & Resolve Issues** (Requirements Quality Problems): + Ask questions about the requirements themselves: + - Ambiguities: "Is the term 'fast' quantified with specific metrics? [Ambiguity, Spec §NFR-1]" + - Conflicts: "Do navigation requirements conflict between §FR-10 and §FR-10a? [Conflict]" + - Assumptions: "Is the assumption of 'always available podcast API' validated? [Assumption]" + - Dependencies: "Are external podcast API requirements documented? [Dependency, Gap]" + - Missing definitions: "Is 'visual hierarchy' defined with measurable criteria? [Gap]" + + **Content Consolidation**: + - Soft cap: If raw candidate items > 40, prioritize by risk/impact + - Merge near-duplicates checking the same requirement aspect + - If >5 low-impact edge cases, create one item: "Are edge cases X, Y, Z addressed in requirements? [Coverage]" + + **🚫 ABSOLUTELY PROHIBITED** - These make it an implementation test, not a requirements test: + - ❌ Any item starting with "Verify", "Test", "Confirm", "Check" + implementation behavior + - ❌ References to code execution, user actions, system behavior + - ❌ "Displays correctly", "works properly", "functions as expected" + - ❌ "Click", "navigate", "render", "load", "execute" + - ❌ Test cases, test plans, QA procedures + - ❌ Implementation details (frameworks, APIs, algorithms) + + **✅ REQUIRED PATTERNS** - These test requirements quality: + - ✅ "Are [requirement type] defined/specified/documented for [scenario]?" + - ✅ "Is [vague term] quantified/clarified with specific criteria?" + - ✅ "Are requirements consistent between [section A] and [section B]?" + - ✅ "Can [requirement] be objectively measured/verified?" + - ✅ "Are [edge cases/scenarios] addressed in requirements?" + - ✅ "Does the spec define [missing aspect]?" + +6. **Structure Reference**: Generate the checklist following the canonical template in `.specify/templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### ` lines with globally incrementing IDs starting at CHK001. + +7. **Report**: Output full path to created checklist, item count, and remind user that each run creates a new file. Summarize: + - Focus areas selected + - Depth level + - Actor/timing + - Any explicit user-specified must-have items incorporated + +**Important**: Each `/speckit.checklist` command invocation creates a checklist file using short, descriptive names unless file already exists. This allows: + +- Multiple checklists of different types (e.g., `ux.md`, `test.md`, `security.md`) +- Simple, memorable filenames that indicate checklist purpose +- Easy identification and navigation in the `checklists/` folder + +To avoid clutter, use descriptive types and clean up obsolete checklists when done. + +## Example Checklist Types & Sample Items + +**UX Requirements Quality:** `ux.md` + +Sample items (testing the requirements, NOT the implementation): + +- "Are visual hierarchy requirements defined with measurable criteria? [Clarity, Spec §FR-1]" +- "Is the number and positioning of UI elements explicitly specified? [Completeness, Spec §FR-1]" +- "Are interaction state requirements (hover, focus, active) consistently defined? [Consistency]" +- "Are accessibility requirements specified for all interactive elements? [Coverage, Gap]" +- "Is fallback behavior defined when images fail to load? [Edge Case, Gap]" +- "Can 'prominent display' be objectively measured? [Measurability, Spec §FR-4]" + +**API Requirements Quality:** `api.md` + +Sample items: + +- "Are error response formats specified for all failure scenarios? [Completeness]" +- "Are rate limiting requirements quantified with specific thresholds? [Clarity]" +- "Are authentication requirements consistent across all endpoints? [Consistency]" +- "Are retry/timeout requirements defined for external dependencies? [Coverage, Gap]" +- "Is versioning strategy documented in requirements? [Gap]" + +**Performance Requirements Quality:** `performance.md` + +Sample items: + +- "Are performance requirements quantified with specific metrics? [Clarity]" +- "Are performance targets defined for all critical user journeys? [Coverage]" +- "Are performance requirements under different load conditions specified? [Completeness]" +- "Can performance requirements be objectively measured? [Measurability]" +- "Are degradation requirements defined for high-load scenarios? [Edge Case, Gap]" + +**Security Requirements Quality:** `security.md` + +Sample items: + +- "Are authentication requirements specified for all protected resources? [Coverage]" +- "Are data protection requirements defined for sensitive information? [Completeness]" +- "Is the threat model documented and requirements aligned to it? [Traceability]" +- "Are security requirements consistent with compliance obligations? [Consistency]" +- "Are security failure/breach response requirements defined? [Gap, Exception Flow]" + +## Anti-Examples: What NOT To Do + +**❌ WRONG - These test implementation, not requirements:** + +```markdown +- [ ] CHK001 - Verify landing page displays 3 episode cards [Spec §FR-001] +- [ ] CHK002 - Test hover states work correctly on desktop [Spec §FR-003] +- [ ] CHK003 - Confirm logo click navigates to home page [Spec §FR-010] +- [ ] CHK004 - Check that related episodes section shows 3-5 items [Spec §FR-005] +``` + +**✅ CORRECT - These test requirements quality:** + +```markdown +- [ ] CHK001 - Are the number and layout of featured episodes explicitly specified? [Completeness, Spec §FR-001] +- [ ] CHK002 - Are hover state requirements consistently defined for all interactive elements? [Consistency, Spec §FR-003] +- [ ] CHK003 - Are navigation requirements clear for all clickable brand elements? [Clarity, Spec §FR-010] +- [ ] CHK004 - Is the selection criteria for related episodes documented? [Gap, Spec §FR-005] +- [ ] CHK005 - Are loading state requirements defined for asynchronous episode data? [Gap] +- [ ] CHK006 - Can "visual hierarchy" requirements be objectively measured? [Measurability, Spec §FR-001] +``` + +**Key Differences:** + +- Wrong: Tests if the system works correctly +- Correct: Tests if the requirements are written correctly +- Wrong: Verification of behavior +- Correct: Validation of requirement quality +- Wrong: "Does it do X?" +- Correct: "Is X clearly specified?" +""" diff --git a/.gemini/commands/speckit.clarify.toml b/.gemini/commands/speckit.clarify.toml new file mode 100644 index 0000000..af1f9a6 --- /dev/null +++ b/.gemini/commands/speckit.clarify.toml @@ -0,0 +1,185 @@ +description = "Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec." + +prompt = """ +--- +description: Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec. +handoffs: + - label: Build Technical Plan + agent: speckit.plan + prompt: Create a plan for the spec. I am building with... +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Outline + +Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file. + +Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `/speckit.plan`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases. + +Execution steps: + +1. Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root **once** (combined `--json --paths-only` mode / `-Json -PathsOnly`). Parse minimal JSON payload fields: + - `FEATURE_DIR` + - `FEATURE_SPEC` + - (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.) + - If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment. + - For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\\''m Groot' (or double-quote if possible: "I'm Groot"). + +2. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked). + + Functional Scope & Behavior: + - Core user goals & success criteria + - Explicit out-of-scope declarations + - User roles / personas differentiation + + Domain & Data Model: + - Entities, attributes, relationships + - Identity & uniqueness rules + - Lifecycle/state transitions + - Data volume / scale assumptions + + Interaction & UX Flow: + - Critical user journeys / sequences + - Error/empty/loading states + - Accessibility or localization notes + + Non-Functional Quality Attributes: + - Performance (latency, throughput targets) + - Scalability (horizontal/vertical, limits) + - Reliability & availability (uptime, recovery expectations) + - Observability (logging, metrics, tracing signals) + - Security & privacy (authN/Z, data protection, threat assumptions) + - Compliance / regulatory constraints (if any) + + Integration & External Dependencies: + - External services/APIs and failure modes + - Data import/export formats + - Protocol/versioning assumptions + + Edge Cases & Failure Handling: + - Negative scenarios + - Rate limiting / throttling + - Conflict resolution (e.g., concurrent edits) + + Constraints & Tradeoffs: + - Technical constraints (language, storage, hosting) + - Explicit tradeoffs or rejected alternatives + + Terminology & Consistency: + - Canonical glossary terms + - Avoided synonyms / deprecated terms + + Completion Signals: + - Acceptance criteria testability + - Measurable Definition of Done style indicators + + Misc / Placeholders: + - TODO markers / unresolved decisions + - Ambiguous adjectives ("robust", "intuitive") lacking quantification + + For each category with Partial or Missing status, add a candidate question opportunity unless: + - Clarification would not materially change implementation or validation strategy + - Information is better deferred to planning phase (note internally) + +3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints: + - Maximum of 10 total questions across the whole session. + - Each question must be answerable with EITHER: + - A short multiple‑choice selection (2–5 distinct, mutually exclusive options), OR + - A one-word / short‑phrase answer (explicitly constrain: "Answer in <=5 words"). + - Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation. + - Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved. + - Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness). + - Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests. + - If more than 5 categories remain unresolved, select the top 5 by (Impact * Uncertainty) heuristic. + +4. Sequential questioning loop (interactive): + - Present EXACTLY ONE question at a time. + - For multiple‑choice questions: + - **Analyze all options** and determine the **most suitable option** based on: + - Best practices for the project type + - Common patterns in similar implementations + - Risk reduction (security, performance, maintainability) + - Alignment with any explicit project goals or constraints visible in the spec + - Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice). + - Format as: `**Recommended:** Option [X] - ` + - Then render all options as a Markdown table: + + | Option | Description | + |--------|-------------| + | A |