From 4c174c717b104a038857e25491cc24b35ea99d55 Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Sun, 29 Mar 2026 23:12:40 +0200 Subject: [PATCH] fix: resolve post-suite state regressions --- app/Filament/Resources/FindingResource.php | 5 +++- app/Services/Auth/CapabilityResolver.php | 20 ++++--------- .../TenantReviewLifecycleService.php | 28 ++++++++++++++++++- .../Resolvers/BackupSetReferenceResolver.php | 2 +- 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/app/Filament/Resources/FindingResource.php b/app/Filament/Resources/FindingResource.php index 4071afb3..124598d3 100644 --- a/app/Filament/Resources/FindingResource.php +++ b/app/Filament/Resources/FindingResource.php @@ -131,7 +131,7 @@ public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView) ->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions support filtered findings operations (legacy acknowledge-all-matching remains until bulk workflow migration).') ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value) - ->satisfy(ActionSurfaceSlot::ListRowMoreMenu, 'Secondary row actions are grouped under "More".') + ->satisfy(ActionSurfaceSlot::ListRowMoreMenu, 'Secondary workflow actions are grouped under "More"; the only inline row action is the related-record drill-down.') ->satisfy(ActionSurfaceSlot::ListBulkMoreGroup, 'Bulk actions are grouped under "More".') ->exempt(ActionSurfaceSlot::ListEmptyState, 'Findings are generated by drift detection and intentionally have no create CTA.') ->satisfy(ActionSurfaceSlot::DetailHeader, 'View page exposes capability-gated workflow actions for finding lifecycle management.'); @@ -1643,6 +1643,7 @@ public static function reopenAction(): Actions\Action */ private static function runWorkflowMutation(Finding $record, string $successTitle, callable $callback): void { + $pageRecord = $record; $record = static::resolveProtectedFindingRecordOrFail($record); $tenant = static::resolveTenantContextForCurrentPanel(); $user = auth()->user(); @@ -1671,6 +1672,8 @@ private static function runWorkflowMutation(Finding $record, string $successTitl try { $callback($record, $tenant, $user); + + $pageRecord->refresh(); } catch (InvalidArgumentException $e) { Notification::make() ->title('Workflow action failed') diff --git a/app/Services/Auth/CapabilityResolver.php b/app/Services/Auth/CapabilityResolver.php index a88ec831..86c94c29 100644 --- a/app/Services/Auth/CapabilityResolver.php +++ b/app/Services/Auth/CapabilityResolver.php @@ -108,7 +108,9 @@ private function getMembership(User $user, Tenant $tenant): ?array /** * Prime membership cache for a set of tenants in one query. * - * Used to avoid N+1 queries for bulk selection authorization. + * Used to avoid N+1 queries for bulk selection authorization while still + * reflecting membership changes that may have happened earlier in the same + * request or test process. * * @param array $tenantIds */ @@ -120,26 +122,14 @@ public function primeMemberships(User $user, array $tenantIds): void return; } - $missingTenantIds = []; - foreach ($tenantIds as $tenantId) { - $cacheKey = "membership_{$user->id}_{$tenantId}"; - if (! array_key_exists($cacheKey, $this->resolvedMemberships)) { - $missingTenantIds[] = $tenantId; - } - } - - if ($missingTenantIds === []) { - return; - } - $memberships = TenantMembership::query() ->where('user_id', $user->id) - ->whereIn('tenant_id', $missingTenantIds) + ->whereIn('tenant_id', $tenantIds) ->get(['tenant_id', 'role', 'source', 'source_ref']); $byTenantId = $memberships->keyBy('tenant_id'); - foreach ($missingTenantIds as $tenantId) { + foreach ($tenantIds as $tenantId) { $cacheKey = "membership_{$user->id}_{$tenantId}"; $membership = $byTenantId->get($tenantId); $this->resolvedMemberships[$cacheKey] = $membership?->toArray(); diff --git a/app/Services/TenantReviews/TenantReviewLifecycleService.php b/app/Services/TenantReviews/TenantReviewLifecycleService.php index 14f06173..595dd1e1 100644 --- a/app/Services/TenantReviews/TenantReviewLifecycleService.php +++ b/app/Services/TenantReviews/TenantReviewLifecycleService.php @@ -5,12 +5,15 @@ namespace App\Services\TenantReviews; use App\Models\EvidenceSnapshot; +use App\Models\ReviewPack; use App\Models\Tenant; use App\Models\TenantReview; use App\Models\User; use App\Services\Audit\WorkspaceAuditLogger; use App\Support\Audit\AuditActionId; use App\Support\TenantReviewStatus; +use App\Support\Ui\DerivedState\DerivedStateFamily; +use App\Support\Ui\DerivedState\RequestScopedDerivedStateStore; use Illuminate\Support\Facades\DB; use InvalidArgumentException; @@ -20,6 +23,7 @@ public function __construct( private readonly TenantReviewReadinessGate $readinessGate, private readonly TenantReviewService $reviewService, private readonly WorkspaceAuditLogger $auditLogger, + private readonly RequestScopedDerivedStateStore $derivedStateStore, ) {} public function publish(TenantReview $review, User $user): TenantReview @@ -64,6 +68,8 @@ public function publish(TenantReview $review, User $user): TenantReview tenant: $tenant, ); + $this->invalidateArtifactTruthCache($review); + return $review->refresh()->load(['tenant', 'sections', 'currentExportReviewPack']); } @@ -104,6 +110,8 @@ public function archive(TenantReview $review, User $user): TenantReview tenant: $tenant, ); + $this->invalidateArtifactTruthCache($review); + return $review->refresh()->load(['tenant', 'sections', 'currentExportReviewPack']); } @@ -126,7 +134,7 @@ public function createNextReview(TenantReview $review, User $user, ?EvidenceSnap throw new InvalidArgumentException('An eligible evidence snapshot is required to create the next review.'); } - return DB::transaction(function () use ($review, $user, $snapshot, $tenant): TenantReview { + $nextReview = DB::transaction(function () use ($review, $user, $snapshot, $tenant): TenantReview { $nextReview = $this->reviewService->create($tenant, $snapshot, $user); if ((int) $nextReview->getKey() !== (int) $review->getKey()) { @@ -156,5 +164,23 @@ public function createNextReview(TenantReview $review, User $user, ?EvidenceSnap return $nextReview->refresh()->load(['tenant', 'evidenceSnapshot', 'sections', 'operationRun', 'initiator', 'publisher']); }); + + $this->invalidateArtifactTruthCache($review); + $this->invalidateArtifactTruthCache($nextReview); + + return $nextReview; + } + + private function invalidateArtifactTruthCache(TenantReview $review): void + { + $this->derivedStateStore->invalidateModel(DerivedStateFamily::ArtifactTruth, $review, 'tenant_review'); + + $review->loadMissing('currentExportReviewPack'); + + $pack = $review->currentExportReviewPack; + + if ($pack instanceof ReviewPack) { + $this->derivedStateStore->invalidateModel(DerivedStateFamily::ArtifactTruth, $pack, 'review_pack'); + } } } diff --git a/app/Support/References/Resolvers/BackupSetReferenceResolver.php b/app/Support/References/Resolvers/BackupSetReferenceResolver.php index d5e28541..69c09201 100644 --- a/app/Support/References/Resolvers/BackupSetReferenceResolver.php +++ b/app/Support/References/Resolvers/BackupSetReferenceResolver.php @@ -58,7 +58,7 @@ public function resolve(ReferenceDescriptor $descriptor): ResolvedReference secondaryLabel: 'Backup set #'.$backupSet->getKey(), linkTarget: new ReferenceLinkTarget( targetKind: ReferenceClass::BackupSet->value, - url: BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $backupSet->tenant), + url: BackupSetResource::getUrl('view', ['record' => $backupSet], panel: 'tenant', tenant: $backupSet->tenant), actionLabel: 'View backup set', contextBadge: 'Tenant', ),