TenantAtlas/resources/views/filament/infolists/entries/assignments-diff.blade.php
ahmido a449ecec5b feat/044-drift-mvp (#58)
Beschreibung
Implementiert das Drift MVP Feature (Spec: 044-drift-mvp) mit Fokus auf automatische Drift-Erkennung zwischen Inventory Sync Runs und Bulk-Triage für Findings.

Was wurde implementiert?
Drift-Erkennung: Vergleicht Policy-Snapshots, Assignments und Scope Tags zwischen Baseline- und Current-Runs. Deterministische Fingerprints verhindern Duplikate.
Findings UI: Neue Filament Resource für Findings mit Listen- und Detail-Ansicht. DB-only Diffs (keine Graph-Calls zur Laufzeit).
Bulk Acknowledge:
"Acknowledge selected" (Bulk-Action auf der Liste)
"Acknowledge all matching" (Header-Action, respektiert aktuelle Filter; Type-to-Confirm bei >100 Findings)
Scope Tag Fix: Behebt False Positives bei Legacy-Daten ohne scope_tags.ids (inferiert Default-Werte).
Authorization: Tenant-isoliert, Rollen-basiert (Owner/Manager/Operator können acknowledge).
Tests: Vollständige Pest-Coverage (28 Tests, 347 Assertions) für Drift-Logik, UI und Bulk-Actions.
Warum diese Änderungen?
Problem: Keine automatisierte Drift-Erkennung; manuelle Triage bei vielen Findings ist mühsam.
Lösung: Async Drift-Generierung mit persistenter Findings-Tabelle. Safe Bulk-Tools für Massen-Triage ohne Deletes.
Konformität: Folgt AGENTS.md Workflow, Spec-Kit (Tasks + Checklists abgehakt), Laravel/Filament Best Practices.
Technische Details
Neue Dateien: ~40 (Models, Services, Tests, Views, Migrations)
Änderungen: Filament Resources, Jobs, Policies
DB: Neue findings Tabelle (JSONB für Evidence, Indexes für Performance)
Tests: ./vendor/bin/sail artisan test tests/Feature/Drift --parallel → 28 passed
Migration: ./vendor/bin/sail artisan migrate (neue Tabelle + Indexes)
Screenshots / Links
Spec: spec.md
Tasks: tasks.md (alle abgehakt)
UI: Findings-Liste mit Bulk-Actions; Detail-View mit Diffs
Checklist
 Tests passieren (parallel + serial)
 Code formatiert (./vendor/bin/pint --dirty)
 Migration reversibel
 Tenant-Isolation enforced
 No Graph-Calls in Views
 Authorization checks
 Spec + Tasks aligned
Deployment Notes
Neue Migration: create_findings_table
Neue Permissions: drift.view, drift.acknowledge
Queue-Job: GenerateDriftFindingsJob (async, deduped)
2026-01-14 23:16:10 +00:00

115 lines
5.2 KiB
PHP

@php
$diff = $getState() ?? [];
$summary = $diff['summary'] ?? [];
$added = is_array($diff['added'] ?? null) ? $diff['added'] : [];
$removed = is_array($diff['removed'] ?? null) ? $diff['removed'] : [];
$changed = is_array($diff['changed'] ?? null) ? $diff['changed'] : [];
$renderRow = static function (array $row): array {
return [
'include_exclude' => (string) ($row['include_exclude'] ?? 'include'),
'target_label' => (string) ($row['target_label'] ?? 'Unknown target'),
'filter_type' => (string) ($row['filter_type'] ?? 'none'),
'filter_id' => $row['filter_id'] ?? null,
'intent' => $row['intent'] ?? null,
'mode' => $row['mode'] ?? null,
];
};
@endphp
<div class="space-y-4">
<x-filament::section
heading="Assignments diff"
:description="$summary['message'] ?? sprintf('%d added, %d removed, %d changed', $summary['added'] ?? 0, $summary['removed'] ?? 0, $summary['changed'] ?? 0)"
>
<div class="flex flex-wrap gap-2">
<x-filament::badge color="success">
{{ (int) ($summary['added'] ?? 0) }} added
</x-filament::badge>
<x-filament::badge color="danger">
{{ (int) ($summary['removed'] ?? 0) }} removed
</x-filament::badge>
<x-filament::badge color="warning">
{{ (int) ($summary['changed'] ?? 0) }} changed
</x-filament::badge>
@if (($summary['truncated'] ?? false) === true)
<x-filament::badge color="gray">
Truncated to {{ (int) ($summary['limit'] ?? 0) }} items
</x-filament::badge>
@endif
</div>
</x-filament::section>
@if ($changed !== [])
<x-filament::section heading="Changed" collapsible>
<div class="space-y-3">
@foreach ($changed as $row)
@php
$to = is_array($row['to'] ?? null) ? $renderRow($row['to']) : $renderRow([]);
$from = is_array($row['from'] ?? null) ? $renderRow($row['from']) : $renderRow([]);
@endphp
<div class="rounded-lg border border-gray-200/70 bg-white p-4 dark:border-gray-700 dark:bg-gray-900">
<div class="font-medium">
{{ $to['target_label'] }}
</div>
<div class="mt-2 grid gap-2 text-sm md:grid-cols-2">
<div>
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">From</div>
<div class="mt-1 space-y-1">
<div>Type: {{ $from['include_exclude'] }}</div>
<div>Filter: {{ $from['filter_type'] }}@if($from['filter_id']) ({{ $from['filter_id'] }})@endif</div>
</div>
</div>
<div>
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">To</div>
<div class="mt-1 space-y-1">
<div>Type: {{ $to['include_exclude'] }}</div>
<div>Filter: {{ $to['filter_type'] }}@if($to['filter_id']) ({{ $to['filter_id'] }})@endif</div>
</div>
</div>
</div>
</div>
@endforeach
</div>
</x-filament::section>
@endif
@if ($added !== [])
<x-filament::section heading="Added" collapsible :collapsed="true">
<div class="space-y-2">
@foreach ($added as $row)
@php $row = $renderRow(is_array($row) ? $row : []); @endphp
<div class="rounded-lg border border-gray-200/70 bg-white p-3 text-sm dark:border-gray-700 dark:bg-gray-900">
<div class="font-medium">{{ $row['target_label'] }}</div>
<div class="mt-1 text-gray-600 dark:text-gray-300">
{{ $row['include_exclude'] }} · filter: {{ $row['filter_type'] }}
</div>
</div>
@endforeach
</div>
</x-filament::section>
@endif
@if ($removed !== [])
<x-filament::section heading="Removed" collapsible :collapsed="true">
<div class="space-y-2">
@foreach ($removed as $row)
@php $row = $renderRow(is_array($row) ? $row : []); @endphp
<div class="rounded-lg border border-gray-200/70 bg-white p-3 text-sm dark:border-gray-700 dark:bg-gray-900">
<div class="font-medium">{{ $row['target_label'] }}</div>
<div class="mt-1 text-gray-600 dark:text-gray-300">
{{ $row['include_exclude'] }} · filter: {{ $row['filter_type'] }}
</div>
</div>
@endforeach
</div>
</x-filament::section>
@endif
</div>