|null */ private ?array $authorizedTenants = null; /** * @var array|null */ private ?array $visibleFindingTenants = null; /** * @var array|null */ private ?array $reviewTenants = null; /** * @var array|null */ private ?array $inboxPayload = null; /** * @var array|null */ private ?array $unfilteredInboxPayload = null; /** * @var array|null */ private ?array $decisionWorkbench = null; private ?Workspace $workspace = null; private ?bool $visibleAlertsFamily = null; private ?bool $visibleFindingExceptionsFamily = null; public ?int $tenantId = null; public ?string $family = null; public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly, ActionSurfaceType::ReadOnlyRegistryReport) ->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions keep the workspace decision surface calm and read-only.') ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value) ->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'The governance inbox routes into existing source surfaces instead of exposing row-level secondary actions.') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The governance inbox does not expose bulk actions.') ->satisfy(ActionSurfaceSlot::ListEmptyState, 'Empty states stay calm and capability-safe when no visible attention exists.') ->exempt(ActionSurfaceSlot::DetailHeader, 'The governance inbox owns no local detail surface in v1.'); } public function mount(): void { $this->authorizeWorkspaceMembership(); $this->applyRequestedTenantPrefilter(); $this->family = $this->resolveRequestedFamily(); $this->ensureAtLeastOneVisibleFamily(); $this->ensureRequestedFamilyIsVisible(); } /** * @return array */ public function appliedScope(): array { $selectedTenant = $this->selectedTenant(); $availableFamilies = collect($this->availableFamilies()) ->keyBy('key'); return [ 'workspace_label' => $this->workspace()?->name, 'tenant_label' => $selectedTenant?->name, 'tenant_prefilter_source' => $selectedTenant instanceof ManagedEnvironment ? 'explicit_filter' : 'none', 'family_key' => $this->family, 'family_label' => $this->family !== null ? ($availableFamilies->get($this->family)['label'] ?? Str::headline($this->family)) : 'All attention', 'total_count' => (int) ($this->inboxPayload()['total_count'] ?? 0), ]; } /** * @return list */ public function availableFamilies(): array { return $this->inboxPayload()['available_families'] ?? []; } /** * @return list> */ public function sections(): array { return $this->inboxPayload()['sections'] ?? []; } /** * @return array{ * question: string, * selected_item: array|null, * summary_cards: list, * diagnostics: array{label: string, state: string, body: string} * } */ public function decisionWorkbench(): array { if (is_array($this->decisionWorkbench)) { return $this->decisionWorkbench; } $entries = $this->workbenchEntries(); $selectedItem = $entries ->sortBy([ fn (array $entry): int => (int) ($entry['urgency_rank'] ?? 999), fn (array $entry): string => (string) ($entry['headline'] ?? ''), ]) ->first(); return $this->decisionWorkbench = [ 'question' => 'What decision clears the highest-priority item?', 'selected_item' => is_array($selectedItem) ? $this->normalizeWorkbenchItem($selectedItem) : null, 'summary_cards' => $this->summaryCards($entries), 'diagnostics' => [ 'label' => 'Diagnostics', 'state' => 'Collapsed', 'body' => 'Source diagnostics and raw support details stay on authorized source surfaces. This workbench shows decision, evidence, and proof state only.', ], ]; } /** * @return array */ public function calmEmptyState(): array { if ($this->tenantFilterAloneExcludesRows()) { return [ 'title' => 'This environment filter is hiding other visible attention', 'body' => 'The current environment scope is calm, but other visible environments in this workspace still have governance attention.', 'action_label' => 'Clear environment filter', 'action_url' => $this->pageUrl(['environment_id' => null, 'family' => null]), ]; } return [ 'title' => 'No governance decisions need attention', 'body' => 'The current workspace scope has no repo-backed governance decisions requiring action.', 'action_label' => null, 'action_url' => null, ]; } public function hasTenantPrefilter(): bool { return $this->selectedTenant() instanceof ManagedEnvironment; } public function isActiveFamily(?string $familyKey): bool { return $this->family === $familyKey; } public function pageUrl(array $overrides = []): string { $selectedTenant = $this->selectedTenant(); $resolvedTenant = array_key_exists('environment_id', $overrides) ? $overrides['environment_id'] : ($selectedTenant?->getKey() !== null ? (string) $selectedTenant->getKey() : null); $resolvedFamily = array_key_exists('family', $overrides) ? $overrides['family'] : $this->family; return static::getUrl( panel: 'admin', parameters: array_filter([ 'environment_id' => (is_string($resolvedTenant) || is_numeric($resolvedTenant)) && (string) $resolvedTenant !== '' ? (string) $resolvedTenant : null, 'family' => is_string($resolvedFamily) && $resolvedFamily !== '' ? $resolvedFamily : null, ], static fn (mixed $value): bool => $value !== null && $value !== ''), ); } public function navigationContext(): CanonicalNavigationContext { return CanonicalNavigationContext::forGovernanceInbox( canonicalRouteName: static::getRouteName(Filament::getPanel('admin')), tenantId: $this->tenantId, backLinkUrl: $this->pageUrl(), familyKey: $this->family, ); } /** * @return \Illuminate\Support\Collection> */ private function workbenchEntries(): \Illuminate\Support\Collection { return collect($this->sections()) ->flatMap(function (array $section): array { $entries = is_array($section['entries'] ?? null) ? $section['entries'] : []; return array_map(function (array $entry) use ($section): array { $entry['section_key'] = (string) ($section['key'] ?? $entry['family_key'] ?? 'governance'); $entry['section_label'] = (string) ($section['label'] ?? 'Governance item'); return $entry; }, $entries); }) ->values(); } /** * @param array $item * @return array */ private function normalizeWorkbenchItem(array $item): array { return [ 'section_label' => (string) ($item['section_label'] ?? 'Governance item'), 'environment_label' => filled($item['tenant_label'] ?? null) ? (string) $item['tenant_label'] : 'Workspace-wide', 'title' => (string) ($item['headline'] ?? 'Governance item'), 'status_label' => (string) ($item['status_label'] ?? 'Needs attention'), 'decision_label' => (string) ($item['decision_label'] ?? 'Review governance item'), 'reason_label' => (string) ($item['reason_label'] ?? 'Reason unavailable'), 'impact_label' => (string) ($item['impact_label'] ?? 'Impact unavailable'), 'owner_label' => (string) ($item['owner_label'] ?? 'Owner unavailable'), 'owner_state' => (string) ($item['owner_state'] ?? 'unavailable'), 'due_label' => (string) ($item['due_label'] ?? 'Due date unavailable'), 'due_state' => (string) ($item['due_state'] ?? 'unavailable'), 'evidence_label' => (string) ($item['evidence_label'] ?? 'Evidence unavailable'), 'evidence_state' => (string) ($item['evidence_state'] ?? 'unavailable'), 'evidence_path_label' => (string) ($item['evidence_path_label'] ?? 'Proof path unavailable'), 'evidence_path_url' => filled($item['evidence_path_url'] ?? null) ? (string) $item['evidence_path_url'] : null, 'exception_label' => (string) ($item['exception_label'] ?? 'Accepted-risk state unavailable'), 'exception_state' => (string) ($item['exception_state'] ?? 'unavailable'), 'primary_action_label' => (string) ($item['primary_action_label'] ?? 'Open source'), 'primary_action_url' => filled($item['primary_action_url'] ?? null) ? (string) $item['primary_action_url'] : (filled($item['destination_url'] ?? null) ? (string) $item['destination_url'] : null), 'source_url' => filled($item['destination_url'] ?? null) ? (string) $item['destination_url'] : null, ]; } /** * @param \Illuminate\Support\Collection> $entries * @return list */ private function summaryCards(\Illuminate\Support\Collection $entries): array { $totalCount = (int) ($this->inboxPayload()['total_count'] ?? 0); $selectedSection = $entries ->sortBy([ fn (array $entry): int => (int) ($entry['urgency_rank'] ?? 999), fn (array $entry): string => (string) ($entry['headline'] ?? ''), ]) ->first()['section_label'] ?? 'None'; $ownerGaps = $entries ->filter(fn (array $entry): bool => in_array((string) ($entry['owner_state'] ?? ''), ['missing', 'unavailable'], true)) ->count(); $evidenceGaps = $entries ->filter(fn (array $entry): bool => in_array((string) ($entry['evidence_state'] ?? ''), ['missing', 'unavailable'], true)) ->count(); return [ [ 'label' => 'Visible decisions', 'value' => (string) $totalCount, 'description' => 'Repo-backed attention items in the current scope.', ], [ 'label' => 'Priority family', 'value' => (string) $selectedSection, 'description' => 'Highest-ranked visible preview item.', ], [ 'label' => 'Owner gaps in preview', 'value' => (string) $ownerGaps, 'description' => 'Preview items with missing or unavailable ownership.', ], [ 'label' => 'Evidence gaps in preview', 'value' => (string) $evidenceGaps, 'description' => 'Preview items without linked proof in the workbench.', ], ]; } private function authorizeWorkspaceMembership(): void { $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User) { abort(403); } if (! $workspace instanceof Workspace) { throw new NotFoundHttpException; } $resolver = app(WorkspaceCapabilityResolver::class); if (! $resolver->isMember($user, $workspace)) { throw new NotFoundHttpException; } } private function ensureAtLeastOneVisibleFamily(): void { if ( $this->hasVisibleOperationsFamily() || $this->visibleFindingTenants() !== [] || $this->hasVisibleFindingExceptionsFamily() || $this->reviewTenants() !== [] || $this->hasVisibleAlertsFamily() ) { return; } abort(403); } private function ensureRequestedFamilyIsVisible(): void { if ($this->family === null) { return; } if (in_array($this->family, collect($this->availableFamilies())->pluck('key')->all(), true)) { return; } throw new NotFoundHttpException; } private function hasVisibleOperationsFamily(): bool { return $this->authorizedTenants() !== []; } private function hasVisibleAlertsFamily(): bool { if (is_bool($this->visibleAlertsFamily)) { return $this->visibleAlertsFamily; } $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return $this->visibleAlertsFamily = false; } return $this->visibleAlertsFamily = app(WorkspaceCapabilityResolver::class)->can($user, $workspace, Capabilities::ALERTS_VIEW); } private function hasVisibleFindingExceptionsFamily(): bool { if (is_bool($this->visibleFindingExceptionsFamily)) { return $this->visibleFindingExceptionsFamily; } if ($this->authorizedTenants() === []) { return $this->visibleFindingExceptionsFamily = false; } $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return $this->visibleFindingExceptionsFamily = false; } return $this->visibleFindingExceptionsFamily = app(WorkspaceCapabilityResolver::class) ->can($user, $workspace, Capabilities::FINDING_EXCEPTION_APPROVE); } /** * @return array */ private function visibleFindingTenants(): array { if ($this->visibleFindingTenants !== null) { return $this->visibleFindingTenants; } $user = auth()->user(); $tenants = $this->authorizedTenants(); if (! $user instanceof User || $tenants === []) { return $this->visibleFindingTenants = []; } $resolver = app(CapabilityResolver::class); $resolver->primeMemberships( $user, array_map(static fn (ManagedEnvironment $tenant): int => (int) $tenant->getKey(), $tenants), ); return $this->visibleFindingTenants = array_values(array_filter( $tenants, fn (ManagedEnvironment $tenant): bool => $resolver->can($user, $tenant, Capabilities::TENANT_FINDINGS_VIEW), )); } /** * @return array */ private function reviewTenants(): array { if ($this->reviewTenants !== null) { return $this->reviewTenants; } $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return $this->reviewTenants = []; } $service = app(EnvironmentReviewRegisterService::class); if (! $service->canAccessWorkspace($user, $workspace)) { return $this->reviewTenants = []; } return $this->reviewTenants = $service->authorizedTenants($user, $workspace); } /** * @return array */ private function authorizedTenants(): array { if ($this->authorizedTenants !== null) { return $this->authorizedTenants; } $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return $this->authorizedTenants = []; } return $this->authorizedTenants = $user->accessibleManagedEnvironmentsQuery((int) $workspace->getKey()) ->where('managed_environments.lifecycle_status', 'active') ->orderBy('managed_environments.name') ->get(['managed_environments.id', 'managed_environments.name', 'managed_environments.slug', 'managed_environments.workspace_id']) ->all(); } private function applyRequestedTenantPrefilter(): void { $workspace = $this->workspace(); if (! $workspace instanceof Workspace) { return; } $filter = WorkspaceHubEnvironmentFilter::fromRequest(request(), $workspace); if (! $filter instanceof WorkspaceHubEnvironmentFilter) { return; } $environmentId = $filter->environmentId(); foreach ($this->authorizedTenants() as $tenant) { if ((int) $tenant->getKey() === $environmentId) { $this->tenantId = $environmentId; return; } } throw new NotFoundHttpException; } private function resolveRequestedFamily(): ?string { $family = request()->query('family'); if (! is_string($family)) { return null; } return in_array($family, [ 'assigned_findings', 'intake_findings', 'finding_exceptions', 'stale_operations', 'alert_delivery_failures', 'review_follow_up', ], true) ? $family : null; } private function workspace(): ?Workspace { if ($this->workspace instanceof Workspace) { return $this->workspace; } $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); if (! is_int($workspaceId)) { return null; } return $this->workspace = Workspace::query()->whereKey($workspaceId)->first(); } /** * @return array */ private function inboxPayload(): array { if (is_array($this->inboxPayload)) { return $this->inboxPayload; } $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return $this->inboxPayload = [ 'sections' => [], 'available_families' => [], 'family_counts' => [], 'total_count' => 0, ]; } return $this->inboxPayload = app(GovernanceInboxSectionBuilder::class)->build( user: $user, workspace: $workspace, authorizedTenants: $this->authorizedTenants(), visibleFindingTenants: $this->visibleFindingTenants(), reviewTenants: $this->reviewTenants(), canViewAlerts: $this->hasVisibleAlertsFamily(), canViewFindingExceptions: $this->hasVisibleFindingExceptionsFamily(), selectedTenant: $this->selectedTenant(), selectedFamily: $this->family, navigationContext: $this->navigationContext(), ); } /** * @return array */ private function unfilteredInboxPayload(): array { if (is_array($this->unfilteredInboxPayload)) { return $this->unfilteredInboxPayload; } $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return $this->unfilteredInboxPayload = [ 'sections' => [], 'available_families' => [], 'family_counts' => [], 'total_count' => 0, ]; } return $this->unfilteredInboxPayload = app(GovernanceInboxSectionBuilder::class)->build( user: $user, workspace: $workspace, authorizedTenants: $this->authorizedTenants(), visibleFindingTenants: $this->visibleFindingTenants(), reviewTenants: $this->reviewTenants(), canViewAlerts: $this->hasVisibleAlertsFamily(), canViewFindingExceptions: $this->hasVisibleFindingExceptionsFamily(), selectedTenant: null, selectedFamily: null, navigationContext: $this->navigationContext(), ); } private function selectedTenant(): ?ManagedEnvironment { if (! is_int($this->tenantId)) { return null; } foreach ($this->authorizedTenants() as $tenant) { if ((int) $tenant->getKey() === $this->tenantId) { return $tenant; } } return null; } private function tenantFilterAloneExcludesRows(): bool { if (! is_int($this->tenantId) || $this->family !== null) { return false; } if ($this->sections() !== []) { return false; } return (int) ($this->unfilteredInboxPayload()['total_count'] ?? 0) > 0; } }