TenantAtlas/apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php
ahmido 8cffdbdb2c feat: governance inbox final operator workflow (spec 346) (#418)
Implemented the final operator workflow for the Governance Inbox. This includes refactoring the inbox page, updating finding resources, adding UI enforcement policies, updating related blade views, and adding comprehensive tests for operator workflow and scope contracts.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #418
2026-06-02 14:58:39 +00:00

1239 lines
43 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Pages\Governance;
use App\Filament\Concerns\CleansAdminTenantQueryParameter;
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Auth\CapabilityResolver;
use App\Services\Auth\WorkspaceCapabilityResolver;
use App\Services\EnvironmentReviews\EnvironmentReviewRegisterService;
use App\Support\Auth\Capabilities;
use App\Support\GovernanceDecisions\GovernanceDecisionRegisterBuilder;
use App\Support\GovernanceInbox\GovernanceInboxSectionBuilder;
use App\Support\ManagedEnvironmentLinks;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
use App\Support\Navigation\WorkspaceHubNavigation;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
use App\Support\Workspaces\WorkspaceContext;
use BackedEnum;
use Filament\Facades\Filament;
use Filament\Pages\Page;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use UnitEnum;
class GovernanceInbox extends Page
{
use CleansAdminTenantQueryParameter;
private const int LANE_PREVIEW_LIMIT = 5;
/**
* @var list<array{key: string, label: string, description: string, empty_state: string}>
*/
private const array LANE_DEFINITIONS = [
[
'key' => 'needs_triage',
'label' => 'Needs triage',
'description' => 'Unassigned findings that still need a first operator path.',
'empty_state' => 'No unassigned findings need first triage in the current scope.',
],
[
'key' => 'requires_decision',
'label' => 'Requires decision',
'description' => 'Assigned governance work and review follow-up that still need operator judgment.',
'empty_state' => 'No operator-decision items are open in the current scope.',
],
[
'key' => 'risk_exception_review',
'label' => 'Risk / exception review',
'description' => 'Accepted-risk and exception records that need approval, renewal, closure, or support review.',
'empty_state' => 'No accepted-risk or exception records need review in the current scope.',
],
[
'key' => 'evidence_required',
'label' => 'Evidence required',
'description' => 'Governance items that are still missing the linked proof needed for follow-through.',
'empty_state' => 'No visible governance items are blocked by missing evidence in the current scope.',
],
[
'key' => 'blocked',
'label' => 'Blocked',
'description' => 'Technical follow-up is still blocked on failed runs or delivery issues.',
'empty_state' => 'No blocked governance follow-up is visible in the current scope.',
],
];
protected static bool $isDiscovered = false;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-inbox-stack';
protected static string|UnitEnum|null $navigationGroup = 'Governance';
protected static ?string $navigationLabel = 'Governance inbox';
protected static ?int $navigationSort = 5;
protected static ?string $title = 'Governance Inbox';
protected static ?string $slug = 'governance/inbox';
protected string $view = 'filament.pages.governance.governance-inbox';
/**
* @var array<int, ManagedEnvironment>|null
*/
private ?array $authorizedTenants = null;
/**
* @var array<int, ManagedEnvironment>|null
*/
private ?array $visibleFindingTenants = null;
/**
* @var array<int, ManagedEnvironment>|null
*/
private ?array $reviewTenants = null;
/**
* @var array<string, mixed>|null
*/
private ?array $inboxPayload = null;
/**
* @var array<string, mixed>|null
*/
private ?array $unfilteredInboxPayload = null;
/**
* @var array<string, mixed>|null
*/
private ?array $lanePayload = null;
/**
* @var array<string, mixed>|null
*/
private ?array $recentlyResolvedPayload = null;
private ?Workspace $workspace = null;
private ?bool $visibleAlertsFamily = null;
private ?bool $visibleFindingExceptionsFamily = null;
public ?int $tenantId = null;
public ?string $family = null;
public function getSubheading(): ?string
{
return 'Daily operator queue for governance follow-up, accepted risk, evidence gaps, and review handoff.';
}
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly, ActionSurfaceType::ReadOnlyRegistryReport)
->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions keep the workspace governance queue 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 menus.')
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The governance inbox does not expose bulk actions.')
->satisfy(ActionSurfaceSlot::ListEmptyState, 'Empty states stay calm and explain what operator work appears here.')
->exempt(ActionSurfaceSlot::DetailHeader, 'The governance inbox owns no local detail surface or mutation workflow.');
}
public static function getNavigationGroup(): string
{
return WorkspaceHubNavigation::workspaceWideGroup(__('localization.navigation.governance'));
}
public static function getNavigationUrl(): string
{
return WorkspaceHubNavigation::environmentFilteredUrl(static::getUrl(panel: 'admin'));
}
public function mount(): void
{
$this->authorizeWorkspaceMembership();
$this->applyRequestedTenantPrefilter();
$this->family = $this->resolveRequestedFamily();
$this->ensureAtLeastOneVisibleFamily();
$this->ensureRequestedFamilyIsVisible();
}
/**
* @return array<string, mixed>
*/
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 source families',
'total_count' => (int) ($this->inboxPayload()['total_count'] ?? 0),
];
}
/**
* @return list<array{key: string, label: string, count: int}>
*/
public function availableFamilies(): array
{
return collect($this->inboxPayload()['available_families'] ?? [])
->map(fn (array $family): array => $this->normalizeFamily($family))
->values()
->all();
}
/**
* @return list<array<string, mixed>>
*/
public function sections(): array
{
return collect($this->inboxPayload()['sections'] ?? [])
->map(fn (array $section): array => $this->normalizeSection($section))
->values()
->all();
}
/**
* @return array{
* total_open_items: int,
* headline: string,
* counts: list<array{key: string, label: string, count: int, description: string, state: string, chip_label: string}>,
* active_counts: list<array{key: string, label: string, count: int, description: string, state: string, chip_label: string}>,
* clear_counts: list<array{key: string, label: string, count: int, description: string, state: string, chip_label: string}>,
* primary_action: array{label: string, url: string}|null,
* next_recommended_item: array<string, mixed>|null,
* review_ready_supported: bool,
* }
*/
public function operatorSummary(): array
{
return $this->lanePayload()['summary'];
}
/**
* @return list<array<string, mixed>>
*/
public function laneGroups(): array
{
return $this->lanePayload()['lanes'];
}
/**
* @return array<string, mixed>|null
*/
public function recentlyResolved(): ?array
{
if (is_array($this->recentlyResolvedPayload)) {
return $this->recentlyResolvedPayload === [] ? null : $this->recentlyResolvedPayload;
}
if (! $this->hasVisibleFindingExceptionsFamily()) {
$this->recentlyResolvedPayload = [];
return null;
}
if ($this->family !== null && $this->family !== 'finding_exceptions') {
$this->recentlyResolvedPayload = [];
return null;
}
$workspace = $this->workspace();
if (! $workspace instanceof Workspace) {
$this->recentlyResolvedPayload = [];
return null;
}
$visibleTenants = $this->selectedTenant() instanceof ManagedEnvironment
? [$this->selectedTenant()]
: $this->authorizedTenants();
$payload = app(GovernanceDecisionRegisterBuilder::class)->build(
workspace: $workspace,
visibleTenants: $visibleTenants,
registerState: 'recently_closed',
);
$count = (int) ($payload['counts']['recently_closed'] ?? 0);
if ($count === 0) {
$this->recentlyResolvedPayload = [];
return null;
}
$selectedTenant = $this->selectedTenant();
$openUrl = DecisionRegister::getUrl(
panel: 'admin',
parameters: array_filter([
'environment_id' => $selectedTenant?->getKey(),
'register_state' => 'recently_closed',
], static fn (mixed $value): bool => $value !== null && $value !== ''),
);
$rows = collect($payload['rows'] ?? [])
->take(3)
->map(function (array $row) use ($openUrl): array {
return [
'title' => filled($row['tenant_name'] ?? null)
? (string) $row['tenant_name']
: 'Recently closed decision',
'reason' => filled($row['closure_reason'] ?? null)
? (string) $row['closure_reason']
: 'Decision closed in the register.',
'next_action_label' => 'Open decision register',
'next_action_url' => $openUrl,
];
})
->values()
->all();
return $this->recentlyResolvedPayload = [
'label' => 'Recently resolved',
'count' => $count,
'summary' => sprintf(
'%d recently closed decision%s remain available in the Decision Register for reference and audit follow-through.',
$count,
$count === 1 ? '' : 's',
),
'open_url' => $openUrl,
'open_label' => 'Open recently closed decisions',
'rows' => $rows,
];
}
/**
* @return array{label: string, state: string, body: string}
*/
public function diagnosticsPanel(): array
{
return [
'label' => 'Diagnostics / source detail',
'state' => 'Collapsed',
'body' => 'Raw diagnostics, payloads, and support detail stay on authorized source surfaces. This inbox keeps the operator queue, proof links, and next actions on the first screen.',
];
}
/**
* @return array<string, mixed>
*/
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 work that needs follow-up.',
'action_label' => 'Clear environment filter',
'action_url' => $this->pageUrl(['environment_id' => null, 'family' => null]),
];
}
if ($this->familyFilterAloneExcludesRows()) {
return [
'title' => 'This source focus is hiding other governance work',
'body' => 'The current source-family focus is calm, but other repo-backed governance items remain open in this workspace.',
'action_label' => 'Show all source families',
'action_url' => $this->pageUrl(['family' => null]),
];
}
return [
'title' => 'No governance items need attention.',
'body' => 'Findings, decisions, accepted-risk reviews, evidence gaps, and review follow-ups will appear here when they need operator attention.',
'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<int, array<string, mixed>>
*/
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();
}
/**
* @return array<string, mixed>
*/
private function lanePayload(): array
{
if (is_array($this->lanePayload)) {
return $this->lanePayload;
}
$lanes = collect(self::LANE_DEFINITIONS)
->mapWithKeys(fn (array $definition): array => [
$definition['key'] => [
...$definition,
'anchor_id' => 'lane-'.$definition['key'],
'count' => 0,
'items' => [],
],
])
->all();
foreach ($this->workbenchEntries() as $entry) {
$operatorItem = $this->buildOperatorItem($entry);
$laneKey = (string) $operatorItem['lane_key'];
if (! array_key_exists($laneKey, $lanes)) {
continue;
}
$lanes[$laneKey]['items'][] = $operatorItem;
$lanes[$laneKey]['count']++;
}
foreach ($lanes as $key => $lane) {
$lanes[$key]['items'] = collect($lane['items'])
->sortBy([
fn (array $item): int => (int) ($item['urgency_rank'] ?? 999),
fn (array $item): string => (string) ($item['title'] ?? ''),
])
->take(self::LANE_PREVIEW_LIMIT)
->values()
->all();
}
$visibleLanes = collect(self::LANE_DEFINITIONS)
->map(fn (array $definition): array => $lanes[$definition['key']])
->filter(fn (array $lane): bool => (int) $lane['count'] > 0)
->values()
->all();
$summaryCounts = collect(self::LANE_DEFINITIONS)
->map(fn (array $definition): array => [
'key' => $definition['key'],
'label' => $definition['label'],
'count' => (int) ($lanes[$definition['key']]['count'] ?? 0),
'description' => $definition['description'],
'state' => (int) ($lanes[$definition['key']]['count'] ?? 0) > 0 ? 'active' : 'clear',
'chip_label' => (int) ($lanes[$definition['key']]['count'] ?? 0) > 0
? (string) ((int) ($lanes[$definition['key']]['count'] ?? 0))
: 'Clear',
])
->values()
->all();
$totalOpenItems = collect($summaryCounts)->sum('count');
$nextRecommendedItem = $this->nextRecommendedItem($visibleLanes);
return $this->lanePayload = [
'lanes' => $visibleLanes,
'summary' => [
'total_open_items' => $totalOpenItems,
'headline' => $totalOpenItems === 1
? '1 open governance item needs attention'
: sprintf('%d open governance items need attention', $totalOpenItems),
'counts' => $summaryCounts,
'active_counts' => collect($summaryCounts)->where('state', 'active')->values()->all(),
'clear_counts' => collect($summaryCounts)->where('state', 'clear')->values()->all(),
'primary_action' => $nextRecommendedItem['primary_action'] ?? null,
'next_recommended_item' => $nextRecommendedItem,
'review_ready_supported' => false,
],
];
}
/**
* @param array<string, mixed> $entry
* @return array<string, mixed>
*/
private function buildOperatorItem(array $entry): array
{
$laneKey = $this->classifyLane($entry);
$tenant = $this->tenantForEntry($entry);
$primaryActionUrl = filled($entry['primary_action_url'] ?? null)
? (string) $entry['primary_action_url']
: (filled($entry['destination_url'] ?? null) ? (string) $entry['destination_url'] : null);
$primaryAction = $this->normalizeAction([
'label' => (string) ($entry['primary_action_label'] ?? 'Open source'),
'url' => $primaryActionUrl,
]);
return [
'lane_key' => $laneKey,
'lane_label' => $this->laneDefinition($laneKey)['label'],
'title' => (string) ($entry['headline'] ?? 'Governance item'),
'status_label' => (string) ($entry['status_label'] ?? 'Needs attention'),
'reason_heading' => $laneKey === 'blocked' ? 'Blocker' : 'Reason',
'reason_label' => (string) ($entry['reason_label'] ?? 'Reason unavailable'),
'impact_label' => (string) ($entry['impact_label'] ?? 'Impact unavailable'),
'source_label' => (string) ($entry['section_label'] ?? 'Source record'),
'environment_label' => filled($entry['tenant_label'] ?? null)
? (string) $entry['tenant_label']
: 'Workspace-wide',
'context_label' => filled($entry['subline'] ?? null)
? (string) $entry['subline']
: null,
'owner_label' => (string) ($entry['owner_label'] ?? 'Owner unavailable'),
'due_label' => (string) ($entry['due_label'] ?? 'Due date unavailable'),
'evidence_label' => (string) ($entry['evidence_label'] ?? 'Evidence unavailable'),
'exception_label' => (string) ($entry['exception_label'] ?? 'Accepted-risk state unavailable'),
'primary_action' => $primaryAction,
'secondary_actions' => $this->secondaryActionsForEntry($entry, $tenant, $primaryActionUrl),
'linked_records' => $this->linkedRecordsForEntry($entry, $tenant),
'urgency_rank' => (int) ($entry['urgency_rank'] ?? 999),
];
}
/**
* @param array<string, mixed> $entry
*/
private function classifyLane(array $entry): string
{
$familyKey = (string) ($entry['family_key'] ?? '');
return match ($familyKey) {
'intake_findings' => 'needs_triage',
'finding_exceptions' => 'risk_exception_review',
'stale_operations', 'alert_delivery_failures' => 'blocked',
'assigned_findings' => (($entry['evidence_state'] ?? null) === 'missing')
? 'evidence_required'
: 'requires_decision',
'review_follow_up' => 'requires_decision',
default => 'requires_decision',
};
}
/**
* @return array{key: string, label: string, description: string, empty_state: string}
*/
private function laneDefinition(string $laneKey): array
{
foreach (self::LANE_DEFINITIONS as $definition) {
if ($definition['key'] === $laneKey) {
return $definition;
}
}
return self::LANE_DEFINITIONS[1];
}
/**
* @param list<array<string, mixed>> $visibleLanes
*/
private function nextRecommendedItem(array $visibleLanes): ?array
{
foreach ($visibleLanes as $lane) {
$items = is_array($lane['items'] ?? null) ? $lane['items'] : [];
foreach ($items as $item) {
if (! is_array($item)) {
continue;
}
$primaryAction = $this->normalizeAction($item['primary_action'] ?? null);
if ($primaryAction === null) {
continue;
}
return [
'headline' => $this->recommendedActionHeadline($item, $primaryAction),
'lane_key' => (string) ($lane['key'] ?? $item['lane_key'] ?? 'requires_decision'),
'lane_label' => (string) ($lane['label'] ?? $item['lane_label'] ?? 'Requires decision'),
'lane_url' => filled($lane['anchor_id'] ?? null) ? '#'.$lane['anchor_id'] : null,
'title' => (string) ($item['title'] ?? 'Governance item'),
'status_label' => (string) ($item['status_label'] ?? 'Needs attention'),
'environment_label' => (string) ($item['environment_label'] ?? 'Workspace-wide'),
'reason_heading' => (string) ($item['reason_heading'] ?? 'Reason'),
'reason_label' => (string) ($item['reason_label'] ?? 'Reason unavailable'),
'impact_label' => (string) ($item['impact_label'] ?? 'Impact unavailable'),
'primary_action' => $primaryAction,
];
}
}
return null;
}
/**
* @param array<string, mixed> $item
* @param array{label: string, url: string} $primaryAction
*/
private function recommendedActionHeadline(array $item, array $primaryAction): string
{
$title = (string) ($item['title'] ?? 'Governance item');
$actionLabel = (string) ($primaryAction['label'] ?? 'Open source');
$lowerAction = Str::lower($actionLabel);
if (Str::startsWith($title, 'Finding #') && Str::contains($lowerAction, 'finding')) {
$verb = trim((string) Str::of($actionLabel)->before(' finding'));
if ($verb !== '') {
return Str::ucfirst($verb).' '.$title;
}
}
if (Str::startsWith($title, 'Terminal follow-up operation') && Str::contains($lowerAction, 'operation')) {
return 'Open terminal operation proof';
}
return $actionLabel.': '.$title;
}
/**
* @param array<string, mixed> $entry
* @return list<array{label: string, url: string}>
*/
private function secondaryActionsForEntry(array $entry, ?ManagedEnvironment $tenant, ?string $primaryActionUrl): array
{
$familyKey = (string) ($entry['family_key'] ?? '');
$actions = [];
$environmentId = is_numeric($entry['managed_environment_id'] ?? null)
? (int) $entry['managed_environment_id']
: null;
if (in_array($familyKey, ['assigned_findings', 'intake_findings', 'finding_exceptions'], true)) {
$this->appendUniqueLink(
$actions,
'Open decision register',
$this->decisionRegisterUrlFor($tenant),
[$primaryActionUrl],
);
}
if (in_array($familyKey, ['assigned_findings', 'intake_findings', 'review_follow_up'], true)) {
$this->appendUniqueLink(
$actions,
'Open evidence overview',
$this->evidenceOverviewUrlFor($tenant),
[$primaryActionUrl],
);
}
if ($familyKey === 'review_follow_up' && $tenant instanceof ManagedEnvironment) {
$this->appendUniqueLink(
$actions,
'Open Customer Review Workspace',
CustomerReviewWorkspace::environmentFilterUrl($tenant),
[$primaryActionUrl],
);
}
if (in_array($familyKey, ['stale_operations', 'alert_delivery_failures', 'assigned_findings', 'intake_findings'], true) && $tenant instanceof ManagedEnvironment) {
$this->appendUniqueLink(
$actions,
'Open environment',
ManagedEnvironmentLinks::viewUrl($tenant),
[$primaryActionUrl],
);
}
if ($familyKey === 'finding_exceptions') {
$this->appendUniqueLink(
$actions,
'Open source finding',
$entry['destination_url'] ?? null,
[$primaryActionUrl],
);
}
if ($environmentId === null && filled($entry['destination_url'] ?? null)) {
$this->appendUniqueLink(
$actions,
'Open source',
$entry['destination_url'] ?? null,
[$primaryActionUrl],
);
}
return array_slice($actions, 0, 3);
}
/**
* @param array<string, mixed> $entry
* @return list<array{label: string, url: string}>
*/
private function linkedRecordsForEntry(array $entry, ?ManagedEnvironment $tenant): array
{
$familyKey = (string) ($entry['family_key'] ?? '');
$records = [];
$this->appendUniqueLink($records, 'Source record', $entry['destination_url'] ?? null);
$this->appendUniqueLink($records, 'Evidence path', $entry['evidence_path_url'] ?? null);
if (in_array($familyKey, ['assigned_findings', 'intake_findings', 'finding_exceptions'], true)) {
$this->appendUniqueLink($records, 'Decision register', $this->decisionRegisterUrlFor($tenant));
}
if ($familyKey === 'review_follow_up' && $tenant instanceof ManagedEnvironment) {
$this->appendUniqueLink($records, 'Customer Review Workspace', CustomerReviewWorkspace::environmentFilterUrl($tenant));
}
if ($familyKey === 'stale_operations') {
$this->appendUniqueLink($records, 'Operation proof', $entry['destination_url'] ?? null);
}
return array_slice($records, 0, 4);
}
/**
* @param list<array{label: string, url: string}> $links
* @param list<string|null> $ignoredUrls
*/
private function appendUniqueLink(array &$links, string $label, mixed $url, array $ignoredUrls = []): void
{
if (! is_string($url) || $url === '' || in_array($url, $ignoredUrls, true)) {
return;
}
foreach ($links as $existingLink) {
if (($existingLink['url'] ?? null) === $url) {
return;
}
}
$links[] = [
'label' => $label,
'url' => $url,
];
}
/**
* @param array<string, mixed> $family
* @return array{key: string, label: string, count: int}
*/
private function normalizeFamily(array $family): array
{
$key = (string) ($family['key'] ?? 'governance');
return [
'key' => $key,
'label' => filled($family['label'] ?? null)
? (string) $family['label']
: Str::headline($key),
'count' => (int) ($family['count'] ?? 0),
];
}
/**
* @param array<string, mixed> $section
* @return array<string, mixed>
*/
private function normalizeSection(array $section): array
{
$key = (string) ($section['key'] ?? 'governance');
$label = filled($section['label'] ?? null)
? (string) $section['label']
: Str::headline($key);
return [
'key' => $key,
'label' => $label,
'count' => (int) ($section['count'] ?? 0),
'summary' => filled($section['summary'] ?? null)
? (string) $section['summary']
: 'No summary is available for this source family.',
'dominant_action' => $this->normalizeAction([
'label' => (string) ($section['dominant_action_label'] ?? 'Open '.$label),
'url' => $section['dominant_action_url'] ?? null,
]),
'dominant_action_label' => (string) ($section['dominant_action_label'] ?? 'Open '.$label),
'dominant_action_url' => is_string($section['dominant_action_url'] ?? null)
? (string) $section['dominant_action_url']
: null,
'entries' => collect($section['entries'] ?? [])
->filter(fn (mixed $entry): bool => is_array($entry))
->map(fn (array $entry): array => $this->normalizeSourceEntry($entry))
->values()
->all(),
'empty_state' => filled($section['empty_state'] ?? null)
? (string) $section['empty_state']
: 'No source records are visible in the current scope.',
];
}
/**
* @param array<string, mixed> $entry
* @return array<string, mixed>
*/
private function normalizeSourceEntry(array $entry): array
{
return [
...$entry,
'headline' => filled($entry['headline'] ?? null)
? (string) $entry['headline']
: 'Governance item',
'status_label' => filled($entry['status_label'] ?? null)
? (string) $entry['status_label']
: 'Needs attention',
'destination_url' => is_string($entry['destination_url'] ?? null)
? (string) $entry['destination_url']
: null,
];
}
/**
* @return array{label: string, url: string}|null
*/
private function normalizeAction(mixed $action): ?array
{
if (! is_array($action)) {
return null;
}
$url = $action['url'] ?? null;
if (! is_string($url) || $url === '') {
return null;
}
$label = filled($action['label'] ?? null)
? (string) $action['label']
: 'Open source';
return [
'label' => $label,
'url' => $url,
];
}
private function tenantForEntry(array $entry): ?ManagedEnvironment
{
$environmentId = is_numeric($entry['managed_environment_id'] ?? null)
? (int) $entry['managed_environment_id']
: null;
if (! is_int($environmentId)) {
return null;
}
foreach ($this->authorizedTenants() as $tenant) {
if ((int) $tenant->getKey() === $environmentId) {
return $tenant;
}
}
return null;
}
private function decisionRegisterUrlFor(?ManagedEnvironment $tenant = null): string
{
return DecisionRegister::getUrl(
panel: 'admin',
parameters: array_filter([
'environment_id' => $tenant?->getKey(),
], static fn (mixed $value): bool => $value !== null && $value !== ''),
);
}
private function evidenceOverviewUrlFor(?ManagedEnvironment $tenant = null): string
{
return route('admin.evidence.overview', array_filter([
'environment_id' => $tenant?->getKey(),
], static fn (mixed $value): bool => $value !== null && $value !== ''));
}
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<int, ManagedEnvironment>
*/
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<int, ManagedEnvironment>
*/
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<int, ManagedEnvironment>
*/
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<string, mixed>
*/
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<string, mixed>
*/
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->laneGroups() !== []) {
return false;
}
return (int) ($this->unfilteredInboxPayload()['total_count'] ?? 0) > 0;
}
private function familyFilterAloneExcludesRows(): bool
{
if ($this->family === null) {
return false;
}
if ($this->laneGroups() !== []) {
return false;
}
return (int) ($this->unfilteredInboxPayload()['total_count'] ?? 0) > 0;
}
}