loadMissing('workspace', 'providerConnections'); $user = $user ?? auth()->user(); $primaryProviderConnection = $this->primaryProviderConnection($tenant); $aggregate = $this->tenantGovernanceAggregateResolver->forTenant($tenant); $backupHealth = $this->tenantBackupHealthResolver->assess($tenant); $recoveryEvidence = $this->restoreSafetyResolver->dashboardRecoveryEvidence($tenant); $requiredPermissions = $this->tenantRequiredPermissionsViewModelBuilder->build($tenant, [ 'status' => 'missing', ]); $latestReview = $this->latestTenantReview($tenant); $latestReviewPack = $this->latestReviewPack($tenant); $latestEvidenceSnapshot = $this->latestEvidenceSnapshot($tenant); $exceptionStats = $this->exceptionStats($tenant); $recentOperations = $this->recentOperations($tenant); $recommendedActions = $this->recommendedActions( tenant: $tenant, user: $user, aggregate: $aggregate, backupHealth: $backupHealth, recoveryEvidence: $recoveryEvidence, requiredPermissions: $requiredPermissions, latestReview: $latestReview, latestReviewPack: $latestReviewPack, exceptionStats: $exceptionStats, ); return new TenantDashboardSummary( context: [ 'workspace' => (string) ($tenant->workspace?->name ?? $this->overviewText('context_workspace')), 'tenant' => (string) $tenant->name, 'provider' => $this->providerChipLabel($primaryProviderConnection), 'providerKey' => $this->providerChipKey($primaryProviderConnection), 'latestActivity' => $this->latestActivityLabel( primaryProviderConnection: $primaryProviderConnection, latestReview: $latestReview, latestReviewPack: $latestReviewPack, latestEvidenceSnapshot: $latestEvidenceSnapshot, recentOperations: $recentOperations, ), ], posture: $this->posture( aggregate: $aggregate, backupHealth: $backupHealth, recoveryEvidence: $recoveryEvidence, requiredPermissions: $requiredPermissions, recommendedActions: $recommendedActions, ), kpis: $this->kpis($tenant, $user, $aggregate, $requiredPermissions), recommendedActions: $recommendedActions, governanceStatus: $this->governanceStatus( tenant: $tenant, user: $user, aggregate: $aggregate, backupHealth: $backupHealth, requiredPermissions: $requiredPermissions, latestReview: $latestReview, latestEvidenceSnapshot: $latestEvidenceSnapshot, ), readinessCards: $this->readinessCards( tenant: $tenant, user: $user, primaryProviderConnection: $primaryProviderConnection, requiredPermissions: $requiredPermissions, latestReview: $latestReview, latestReviewPack: $latestReviewPack, latestEvidenceSnapshot: $latestEvidenceSnapshot, exceptionStats: $exceptionStats, ), recentOperations: $this->recentOperationCards($tenant, $recentOperations), pollingInterval: ActiveRuns::pollingIntervalForTenant($tenant), ); } private function primaryProviderConnection(Tenant $tenant): ?ProviderConnection { $connections = $tenant->providerConnections; return $connections->first(static fn (ProviderConnection $connection): bool => $connection->is_enabled && $connection->is_default) ?? $connections->first(static fn (ProviderConnection $connection): bool => $connection->is_enabled) ?? $connections->first(); } private function providerChipLabel(?ProviderConnection $connection): ?string { if (! $connection instanceof ProviderConnection || blank($connection->provider)) { return null; } return Str::headline((string) $connection->provider); } private function providerChipKey(?ProviderConnection $connection): ?string { if (! $connection instanceof ProviderConnection || blank($connection->provider)) { return null; } return Str::lower((string) $connection->provider); } /** * @param list $recentOperations */ private function latestActivityLabel( ?ProviderConnection $primaryProviderConnection, ?TenantReview $latestReview, ?ReviewPack $latestReviewPack, ?EvidenceSnapshot $latestEvidenceSnapshot, array $recentOperations, ): ?string { $timestamp = $this->latestActivityTimestamp( primaryProviderConnection: $primaryProviderConnection, latestReview: $latestReview, latestReviewPack: $latestReviewPack, latestEvidenceSnapshot: $latestEvidenceSnapshot, recentOperations: $recentOperations, ); return $timestamp?->diffForHumans(); } /** * @param list $recentOperations */ private function latestActivityTimestamp( ?ProviderConnection $primaryProviderConnection, ?TenantReview $latestReview, ?ReviewPack $latestReviewPack, ?EvidenceSnapshot $latestEvidenceSnapshot, array $recentOperations, ): ?Carbon { $candidates = []; foreach ($recentOperations as $operation) { $timestamp = $operation->completed_at ?? $operation->created_at; if ($timestamp instanceof Carbon) { $candidates[] = $timestamp; } } foreach ([ $primaryProviderConnection?->last_health_check_at, $primaryProviderConnection?->consent_last_checked_at, $latestReview?->published_at ?? $latestReview?->generated_at, $latestReviewPack?->generated_at, $latestEvidenceSnapshot?->generated_at, ] as $timestamp) { if ($timestamp instanceof Carbon) { $candidates[] = $timestamp; } } if ($candidates === []) { return null; } usort($candidates, static fn (Carbon $left, Carbon $right): int => $right->getTimestamp() <=> $left->getTimestamp()); return $candidates[0]; } private function latestTenantReview(Tenant $tenant): ?TenantReview { return TenantReview::query() ->with(['tenant', 'evidenceSnapshot', 'currentExportReviewPack']) ->where('tenant_id', (int) $tenant->getKey()) ->latest('generated_at') ->latest('id') ->first(); } private function latestReviewPack(Tenant $tenant): ?ReviewPack { return ReviewPack::query() ->with(['tenant', 'tenantReview']) ->where('tenant_id', (int) $tenant->getKey()) ->latest('generated_at') ->latest('id') ->first(); } private function latestEvidenceSnapshot(Tenant $tenant): ?EvidenceSnapshot { return EvidenceSnapshot::query() ->where('tenant_id', (int) $tenant->getKey()) ->latest('generated_at') ->latest('id') ->first(); } /** * @return array{active:int,expiring:int,expired:int,pending:int,total:int} */ private function exceptionStats(Tenant $tenant): array { $counts = FindingException::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('workspace_id', (int) $tenant->workspace_id) ->selectRaw('count(*) as total') ->selectRaw("count(*) filter (where status = 'active') as active") ->selectRaw("count(*) filter (where status = 'expiring') as expiring") ->selectRaw("count(*) filter (where status = 'expired') as expired") ->selectRaw("count(*) filter (where status = 'pending') as pending") ->first(); return [ 'active' => (int) ($counts?->active ?? 0), 'expiring' => (int) ($counts?->expiring ?? 0), 'expired' => (int) ($counts?->expired ?? 0), 'pending' => (int) ($counts?->pending ?? 0), 'total' => (int) ($counts?->total ?? 0), ]; } /** * @return list */ private function recentOperations(Tenant $tenant): array { return OperationRun::query() ->where('tenant_id', (int) $tenant->getKey()) ->latest('created_at') ->latest('id') ->limit(4) ->get() ->all(); } /** * @param array $requiredPermissions * @param list> $recommendedActions * @return array{status:string,tone:string,headline:string,summary:string} */ private function posture( TenantGovernanceAggregate $aggregate, TenantBackupHealthAssessment $backupHealth, array $recoveryEvidence, array $requiredPermissions, array $recommendedActions, ): array { $counts = is_array($requiredPermissions['overview']['counts'] ?? null) ? $requiredPermissions['overview']['counts'] : []; $missingApplicationPermissions = (int) ($counts['missing_application'] ?? 0); if ($missingApplicationPermissions > 0) { return [ 'status' => $this->overviewText('status_blocked'), 'tone' => 'danger', 'headline' => $this->overviewText('posture_blocked_headline'), 'summary' => $this->overviewText('posture_blocked_summary'), ]; } if ($recommendedActions !== [] || $aggregate->stateFamily !== 'positive' || $backupHealth->posture !== TenantBackupHealthAssessment::POSTURE_HEALTHY || TenantRecoveryTriagePresentation::recoveryEvidenceState($recoveryEvidence) !== TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_NO_RECENT_ISSUES_VISIBLE) { $topAction = $recommendedActions[0] ?? null; return [ 'status' => $this->overviewText('status_action_needed'), 'tone' => (string) ($topAction['tone'] ?? 'warning'), 'headline' => is_string($topAction['reason'] ?? null) ? (string) $topAction['reason'] : $aggregate->headline, 'summary' => is_string($topAction['impact'] ?? null) ? (string) $topAction['impact'] : ($aggregate->supportingMessage ?? $this->overviewText('posture_action_needed_fallback_summary')), ]; } return [ 'status' => $this->overviewText('status_calm'), 'tone' => 'success', 'headline' => $this->overviewText('posture_calm_headline'), 'summary' => $this->overviewText('posture_calm_summary'), ]; } /** * @param array $requiredPermissions * @return list> */ private function kpis(Tenant $tenant, ?User $user, TenantGovernanceAggregate $aggregate, array $requiredPermissions): array { $counts = is_array($requiredPermissions['overview']['counts'] ?? null) ? $requiredPermissions['overview']['counts'] : []; $missingApplicationPermissions = (int) ($counts['missing_application'] ?? 0); $missingDelegatedPermissions = (int) ($counts['missing_delegated'] ?? 0); $highSeverityChart = $this->highSeverityFindingsChart($tenant); $operationsFollowUpChart = $this->operationsFollowUpChart($tenant); $operationsNeedingFollowUp = (int) OperationRun::query() ->where('tenant_id', (int) $tenant->getKey()) ->where(function ($query): void { $query->terminalFollowUp()->orWhere(fn ($inner) => $inner->activeStaleAttention()); }) ->count(); return [ $this->metricCard( key: 'high_severity_findings', label: $this->overviewText('kpi_high_severity_label'), value: $aggregate->highSeverityActiveFindingsCount, description: $this->highSeverityKpiDescription($aggregate->highSeverityActiveFindingsCount, $highSeverityChart), tone: $aggregate->highSeverityActiveFindingsCount > 0 ? 'danger' : 'gray', icon: $this->trendDirectionIcon($aggregate->highSeverityActiveFindingsCount > 0), chart: $highSeverityChart, action: $this->tenantFindingsAction($tenant, $user, $this->overviewText('action_review_findings'), [ 'tab' => 'needs_action', 'high_severity' => 1, ]), ), $this->metricCard( key: 'overdue_findings', label: $this->overviewText('kpi_overdue_label'), value: $aggregate->overdueOpenFindingsCount, description: $this->overdueKpiDescription($aggregate->overdueOpenFindingsCount), tone: $aggregate->overdueOpenFindingsCount > 0 ? 'warning' : 'gray', icon: $this->trendDirectionIcon($aggregate->overdueOpenFindingsCount > 0), action: $this->tenantFindingsAction($tenant, $user, $this->overviewText('action_open_overdue_findings'), [ 'tab' => 'overdue', ]), ), $this->metricCard( key: 'missing_permissions', label: $this->overviewText('kpi_missing_permissions_label'), value: $missingApplicationPermissions + $missingDelegatedPermissions, description: $this->missingPermissionsKpiDescription($missingApplicationPermissions, $missingDelegatedPermissions), tone: $missingApplicationPermissions > 0 ? 'danger' : ($missingDelegatedPermissions > 0 ? 'warning' : 'gray'), icon: $this->trendDirectionIcon(($missingApplicationPermissions + $missingDelegatedPermissions) > 0), action: $this->requiredPermissionsAction($tenant, $user, $this->overviewText('action_open_required_permissions')), ), $this->metricCard( key: 'active_operations', label: $this->overviewText('kpi_active_operations_label'), value: $operationsNeedingFollowUp, description: $this->activeOperationsKpiDescription($operationsNeedingFollowUp, $operationsFollowUpChart), tone: $operationsNeedingFollowUp > 0 ? 'warning' : 'gray', icon: $this->trendDirectionIcon($operationsNeedingFollowUp > 0), chart: $operationsFollowUpChart, action: $this->operationsAction( tenant: $tenant, user: $user, label: $this->overviewText('action_view_all_operations'), activeTab: $operationsNeedingFollowUp > 0 ? OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP : 'active', problemClass: $operationsNeedingFollowUp > 0 ? OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP : null, ), ), ]; } /** * @param list|null $chart */ private function highSeverityKpiDescription(int $count, ?array $chart): string { if ($count <= 0) { return $this->overviewText('kpi_high_severity_tendency_none'); } $windowCount = $chart === null ? 0 : array_sum($chart); if ($windowCount > 0) { return $this->overviewText('kpi_high_severity_tendency_window', [ 'count' => $count, 'window' => $windowCount, ]); } return $this->overviewText('kpi_high_severity_tendency', ['count' => $count]); } private function overdueKpiDescription(int $count): string { if ($count <= 0) { return $this->overviewText('kpi_overdue_tendency_none'); } return $this->overviewText('kpi_overdue_tendency', ['count' => $count]); } private function missingPermissionsKpiDescription(int $missingApplicationPermissions, int $missingDelegatedPermissions): string { $totalMissingPermissions = $missingApplicationPermissions + $missingDelegatedPermissions; if ($totalMissingPermissions <= 0) { return $this->overviewText('kpi_missing_permissions_tendency_none'); } if ($missingApplicationPermissions > 0 && $missingDelegatedPermissions > 0) { return $this->overviewText('kpi_missing_permissions_tendency_split', [ 'app' => $missingApplicationPermissions, 'delegated' => $missingDelegatedPermissions, ]); } if ($missingApplicationPermissions > 0) { return $this->overviewText('kpi_missing_permissions_tendency_app_only', [ 'count' => $missingApplicationPermissions, ]); } return $this->overviewText('kpi_missing_permissions_tendency_delegated_only', [ 'count' => $missingDelegatedPermissions, ]); } /** * @param list|null $chart */ private function activeOperationsKpiDescription(int $count, ?array $chart): string { if ($count <= 0) { return $this->overviewText('kpi_active_operations_tendency_none'); } $windowCount = $chart === null ? 0 : array_sum($chart); if ($windowCount > 0) { return $this->overviewText('kpi_active_operations_tendency_window', [ 'count' => $count, 'window' => $windowCount, ]); } return $this->overviewText('kpi_active_operations_tendency', ['count' => $count]); } private function trendDirectionIcon(bool $hasAttention): string { return $hasAttention ? 'heroicon-m-arrow-trending-up' : 'heroicon-m-arrow-trending-down'; } /** * @return list|null */ private function highSeverityFindingsChart(Tenant $tenant): ?array { $window = $this->sevenDayWindow(); $byDay = Finding::query() ->where('tenant_id', (int) $tenant->getKey()) ->whereIn('severity', Finding::highSeverityValues()) ->where(function (Builder $query) use ($window): void { $query ->whereBetween('first_seen_at', [$window['start'], $window['end']]) ->orWhere(function (Builder $fallbackQuery) use ($window): void { $fallbackQuery ->whereNull('first_seen_at') ->whereBetween('created_at', [$window['start'], $window['end']]); }); }) ->selectRaw('date(coalesce(first_seen_at, created_at)) as chart_date, count(*) as aggregate') ->groupByRaw('date(coalesce(first_seen_at, created_at))') ->pluck('aggregate', 'chart_date') ->all(); return $this->normalizeSevenDayChart($byDay, $window['start']); } /** * @return list|null */ private function operationsFollowUpChart(Tenant $tenant): ?array { $window = $this->sevenDayWindow(); $byDay = OperationRun::query() ->where('tenant_id', (int) $tenant->getKey()) ->dashboardNeedsFollowUp() ->where(function (Builder $query) use ($window): void { $query ->whereBetween('completed_at', [$window['start'], $window['end']]) ->orWhere(function (Builder $activeQuery) use ($window): void { $activeQuery ->whereNull('completed_at') ->whereBetween('created_at', [$window['start'], $window['end']]); }); }) ->selectRaw('date(coalesce(completed_at, created_at)) as chart_date, count(*) as aggregate') ->groupByRaw('date(coalesce(completed_at, created_at))') ->pluck('aggregate', 'chart_date') ->all(); return $this->normalizeSevenDayChart($byDay, $window['start']); } /** * @return array{start: Carbon, end: Carbon} */ private function sevenDayWindow(): array { $end = now()->endOfDay(); return [ 'start' => $end->copy()->startOfDay()->subDays(6), 'end' => $end, ]; } /** * @param array $byDay * @return list|null */ private function normalizeSevenDayChart(array $byDay, Carbon $windowStart): ?array { $series = []; for ($day = $windowStart->copy(); $day->lte(now()); $day->addDay()) { $series[] = (int) ($byDay[$day->toDateString()] ?? 0); } return array_sum($series) > 0 ? $series : null; } /** * @param array $requiredPermissions * @param array{active:int,expiring:int,expired:int,pending:int,total:int} $exceptionStats * @return list> */ private function recommendedActions( Tenant $tenant, ?User $user, TenantGovernanceAggregate $aggregate, TenantBackupHealthAssessment $backupHealth, array $recoveryEvidence, array $requiredPermissions, ?TenantReview $latestReview, ?ReviewPack $latestReviewPack, array $exceptionStats, ): array { $counts = is_array($requiredPermissions['overview']['counts'] ?? null) ? $requiredPermissions['overview']['counts'] : []; $overview = is_array($requiredPermissions['overview'] ?? null) ? $requiredPermissions['overview'] : []; $candidates = []; $missingApplicationPermissions = (int) ($counts['missing_application'] ?? 0); $missingDelegatedPermissions = (int) ($counts['missing_delegated'] ?? 0); if ($missingApplicationPermissions > 0) { $candidates[] = $this->actionCandidate( priority: 10, key: 'required_permissions', title: $this->overviewText('action_open_required_permissions'), reason: $this->overviewText('reason_missing_application_permissions', ['count' => $missingApplicationPermissions]), impact: $this->overviewText('impact_missing_application_permissions'), tone: 'danger', action: $this->requiredPermissionsAction($tenant, $user, $this->overviewText('action_open_required_permissions')), ); } elseif ($missingDelegatedPermissions > 0) { $candidates[] = $this->actionCandidate( priority: 20, key: 'delegated_permissions', title: $this->overviewText('action_open_required_permissions'), reason: $this->overviewText('reason_missing_delegated_permissions', ['count' => $missingDelegatedPermissions]), impact: $this->overviewText('impact_missing_delegated_permissions'), tone: 'warning', action: $this->requiredPermissionsAction($tenant, $user, $this->overviewText('action_open_required_permissions')), ); } if ($aggregate->highSeverityActiveFindingsCount > 0) { $candidates[] = $this->actionCandidate( priority: 30, key: 'high_severity_findings', title: $this->overviewText('action_review_findings'), reason: $this->overviewText('reason_high_severity_findings', ['count' => $aggregate->highSeverityActiveFindingsCount]), impact: $this->overviewText('impact_high_severity_findings'), tone: 'danger', action: $this->tenantFindingsAction($tenant, $user, $this->overviewText('action_review_findings'), [ 'tab' => 'needs_action', 'high_severity' => 1, ]), ); } if ($aggregate->overdueOpenFindingsCount > 0) { $candidates[] = $this->actionCandidate( priority: 40, key: 'overdue_findings', title: $this->overviewText('action_review_findings'), reason: $this->overviewText('reason_overdue_findings', ['count' => $aggregate->overdueOpenFindingsCount]), impact: $this->overviewText('impact_overdue_findings'), tone: 'warning', action: $this->tenantFindingsAction($tenant, $user, $this->overviewText('action_open_overdue_findings'), [ 'tab' => 'overdue', ]), ); } $exceptionNeedsAction = $exceptionStats['pending'] + $exceptionStats['expiring'] + $exceptionStats['expired']; if ($exceptionNeedsAction > 0 || $aggregate->lapsedGovernanceCount > 0 || $aggregate->expiringGovernanceCount > 0) { $candidates[] = $this->actionCandidate( priority: 50, key: 'risk_exceptions', title: $this->overviewText('action_review_risks'), reason: $this->overviewText('reason_risk_exceptions', ['count' => max($exceptionNeedsAction, $aggregate->lapsedGovernanceCount + $aggregate->expiringGovernanceCount)]), impact: $this->overviewText('impact_risk_exceptions'), tone: $aggregate->lapsedGovernanceCount > 0 || $exceptionStats['expired'] > 0 ? 'danger' : 'warning', action: $this->riskExceptionsAction($tenant, $user, $this->overviewText('action_review_risks')), ); } if ($backupHealth->posture !== TenantBackupHealthAssessment::POSTURE_HEALTHY || TenantRecoveryTriagePresentation::recoveryEvidenceState($recoveryEvidence) !== TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_NO_RECENT_ISSUES_VISIBLE) { $candidates[] = $this->actionCandidate( priority: 60, key: 'recovery_posture', title: $this->overviewText('action_review_recovery_posture'), reason: TenantRecoveryTriagePresentation::backupPostureDescription($backupHealth) ?? $backupHealth->headline, impact: $this->overviewText('impact_recovery_posture'), tone: $backupHealth->tone(), action: $this->backupHealthAction($tenant, $user, $this->overviewText('action_open_backup_posture'), $backupHealth), ); } $terminalFollowUpRuns = (int) OperationRun::query() ->where('tenant_id', (int) $tenant->getKey()) ->terminalFollowUp() ->count(); if ($terminalFollowUpRuns > 0) { $candidates[] = $this->actionCandidate( priority: 70, key: 'terminal_operations', title: $this->overviewText('action_view_all_operations'), reason: $this->overviewText('reason_terminal_operations', ['count' => $terminalFollowUpRuns]), impact: $this->overviewText('impact_terminal_operations'), tone: 'danger', action: $this->operationsAction( tenant: $tenant, user: $user, label: $this->overviewText('action_view_all_operations'), activeTab: OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP, problemClass: OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP, ), ); } if ($latestReview instanceof TenantReview && $latestReviewPack?->status !== 'ready') { $candidates[] = $this->actionCandidate( priority: 80, key: 'continue_review', title: $this->overviewText('action_continue_review'), reason: $this->overviewText('reason_continue_review'), impact: $this->overviewText('impact_continue_review'), tone: 'info', action: $this->continueReviewAction($tenant, $user, $latestReview), ); } return collect($candidates) ->sortBy('priority') ->take(3) ->values() ->all(); } /** * @param array $requiredPermissions * @return list> */ private function governanceStatus( Tenant $tenant, ?User $user, TenantGovernanceAggregate $aggregate, TenantBackupHealthAssessment $backupHealth, array $requiredPermissions, ?TenantReview $latestReview, ?EvidenceSnapshot $latestEvidenceSnapshot, ): array { $overview = is_array($requiredPermissions['overview'] ?? null) ? $requiredPermissions['overview'] : []; $counts = is_array($overview['counts'] ?? null) ? $overview['counts'] : []; $freshness = is_array($overview['freshness'] ?? null) ? $overview['freshness'] : []; return [ [ 'key' => 'baseline_compare', 'label' => $this->overviewText('governance_baseline_compare_label'), 'icon' => $this->governanceStatusIcon('baseline_compare'), 'value' => $aggregate->headline, 'tone' => $aggregate->tone, 'description' => $aggregate->supportingMessage ?? $this->overviewText('governance_baseline_compare_description'), ...$this->baselineCompareAction($tenant, $user, $this->overviewText('action_open_baseline_compare')), ], [ 'key' => 'evidence_coverage', 'label' => $this->overviewText('governance_evidence_coverage_label'), 'icon' => $this->governanceStatusIcon('evidence_coverage'), 'value' => $this->evidenceCoverageValue($latestEvidenceSnapshot), 'tone' => $this->evidenceCoverageTone($latestEvidenceSnapshot), 'description' => $this->evidenceCoverageDescription($latestEvidenceSnapshot), ...$this->evidenceAction($tenant, $user, $this->overviewText('action_open_evidence'), $latestEvidenceSnapshot), ], [ 'key' => 'review_freshness', 'label' => $this->overviewText('governance_review_freshness_label'), 'icon' => $this->governanceStatusIcon('review_freshness'), 'value' => $this->reviewValue($latestReview), 'tone' => $this->reviewTone($latestReview), 'description' => $this->reviewDescription($latestReview), ...$this->tenantReviewAction($tenant, $user, $this->reviewSurfaceActionLabel($tenant, $user, $latestReview), $latestReview), ], [ 'key' => 'provider_permissions', 'label' => $this->overviewText('governance_provider_permissions_label'), 'icon' => $this->governanceStatusIcon('provider_permissions'), 'value' => $this->providerPermissionsValue($overview), 'tone' => $this->providerPermissionsTone($overview), 'description' => $this->providerPermissionsDescription($counts, $freshness), ...$this->requiredPermissionsAction($tenant, $user, $this->overviewText('action_open_required_permissions')), ], [ 'key' => 'backup_posture', 'label' => $this->overviewText('governance_backup_posture_label'), 'icon' => $this->governanceStatusIcon('backup_posture'), 'value' => TenantRecoveryTriagePresentation::backupPostureLabel($backupHealth), 'tone' => $backupHealth->tone(), 'description' => TenantRecoveryTriagePresentation::backupPostureDescription($backupHealth) ?? $this->overviewText('governance_backup_posture_unavailable_description'), ...$this->backupHealthAction($tenant, $user, $this->overviewText('action_open_backup_posture'), $backupHealth), ], ]; } /** * @param array $requiredPermissions * @param array{active:int,expiring:int,expired:int,pending:int,total:int} $exceptionStats * @return list> */ private function readinessCards( Tenant $tenant, ?User $user, ?ProviderConnection $primaryProviderConnection, array $requiredPermissions, ?TenantReview $latestReview, ?ReviewPack $latestReviewPack, ?EvidenceSnapshot $latestEvidenceSnapshot, array $exceptionStats, ): array { return [ $this->currentReviewCard($tenant, $user, $latestReview), $this->riskExceptionsCard($tenant, $user, $exceptionStats), $this->providerHealthCard($tenant, $user, $primaryProviderConnection, $requiredPermissions), $this->customerSafeOutputCard($tenant, $user, $latestReviewPack, $latestEvidenceSnapshot), ]; } /** * @return array */ private function currentReviewCard(Tenant $tenant, ?User $user, ?TenantReview $latestReview): array { $timestamp = $latestReview?->published_at ?? $latestReview?->generated_at ?? $latestReview?->updated_at; return [ 'key' => 'current_review', 'title' => $this->overviewText('readiness_current_review_title'), 'status' => $latestReview instanceof TenantReview ? $this->reviewValue($latestReview) : $this->overviewText('readiness_current_review_empty_status'), 'tone' => $latestReview instanceof TenantReview ? $this->reviewTone($latestReview) : 'gray', 'body' => $latestReview instanceof TenantReview ? $this->reviewDescription($latestReview) : $this->overviewText('readiness_current_review_empty_description'), 'progress' => $this->reviewProgress($latestReview), 'meta' => $this->cardMeta( $this->metaItem( $this->overviewText('readiness_current_review_updated_label'), $this->relativeTime($timestamp), ), ), ...$this->tenantReviewAction($tenant, $user, $this->reviewSurfaceActionLabel($tenant, $user, $latestReview), $latestReview), ]; } /** * @param array{active:int,expiring:int,expired:int,pending:int,total:int} $exceptionStats * @return array */ private function riskExceptionsCard(Tenant $tenant, ?User $user, array $exceptionStats): array { return [ 'key' => 'risk_exceptions', 'title' => $this->overviewText('readiness_risk_exceptions_title'), 'status' => $this->riskExceptionValue($exceptionStats), 'tone' => $this->riskExceptionTone($exceptionStats), 'body' => $this->riskExceptionDescription($exceptionStats), 'meta' => $this->cardMeta( $this->metaItem($this->overviewText('readiness_risk_exceptions_active_label'), (string) $exceptionStats['active']), $this->metaItem($this->overviewText('readiness_risk_exceptions_expiring_label'), (string) $exceptionStats['expiring']), $this->metaItem($this->overviewText('readiness_risk_exceptions_pending_label'), (string) $exceptionStats['pending']), ), ...$this->riskExceptionsAction($tenant, $user, $this->overviewText('action_review_risks')), ]; } /** * @param array $requiredPermissions * @return array */ private function providerHealthCard( Tenant $tenant, ?User $user, ?ProviderConnection $primaryProviderConnection, array $requiredPermissions, ): array { $overview = is_array($requiredPermissions['overview'] ?? null) ? $requiredPermissions['overview'] : []; $counts = is_array($overview['counts'] ?? null) ? $overview['counts'] : []; $freshness = is_array($overview['freshness'] ?? null) ? $overview['freshness'] : []; return [ 'key' => 'provider_health', 'title' => $this->overviewText('readiness_provider_health_title'), 'headline' => $this->providerHealthHeadline($primaryProviderConnection), 'status' => $this->providerHealthStatus($primaryProviderConnection), 'tone' => $this->providerHealthTone($primaryProviderConnection), 'body' => $this->providerHealthDescription($primaryProviderConnection, $counts, $freshness), 'meta' => $this->cardMeta( $this->metaItem( $this->overviewText('readiness_provider_health_permissions_label'), (string) ((int) ($counts['missing_application'] ?? 0) + (int) ($counts['missing_delegated'] ?? 0)), ), $this->metaItem( $this->overviewText('readiness_provider_health_last_check_label'), $this->relativeTime($primaryProviderConnection?->last_health_check_at), ), $this->metaItem( $this->overviewText('readiness_provider_health_snapshot_label'), $this->relativeTimeFromString($freshness['last_refreshed_at'] ?? null), ), ), ...$this->requiredPermissionsAction($tenant, $user, $this->overviewText('action_open_required_permissions')), ]; } /** * @return array */ private function customerSafeOutputCard( Tenant $tenant, ?User $user, ?ReviewPack $latestReviewPack, ?EvidenceSnapshot $latestEvidenceSnapshot, ): array { return [ 'key' => 'customer_safe_output', 'title' => $this->overviewText('readiness_customer_safe_output_title'), 'status' => $this->customerSafeOutputStatus($latestReviewPack, $latestEvidenceSnapshot), 'tone' => $this->customerSafeOutputTone($latestReviewPack, $latestEvidenceSnapshot), 'body' => $this->customerSafeOutputDescription($latestReviewPack, $latestEvidenceSnapshot), 'meta' => $this->cardMeta( $this->metaItem( $this->overviewText('readiness_customer_safe_output_evidence_label'), $this->relativeTime($latestEvidenceSnapshot?->generated_at) ?? $this->overviewText('status_unavailable'), ), $this->metaItem( $this->overviewText('readiness_customer_safe_output_review_pack_label'), $this->relativeTime($latestReviewPack?->generated_at) ?? $this->overviewText('status_unavailable'), ), ), ...$this->customerWorkspaceAction($tenant, $user, $latestReviewPack), ]; } /** * @param list $recentOperations * @return list> */ private function recentOperationCards(Tenant $tenant, array $recentOperations): array { return collect($recentOperations) ->map(function (OperationRun $operation) use ($tenant): array { $statusSpec = BadgeRenderer::spec(BadgeDomain::OperationRunStatus, [ 'status' => (string) $operation->status, 'freshness_state' => $operation->freshnessState()->value, ]); $outcomeSpec = BadgeRenderer::spec(BadgeDomain::OperationRunOutcome, [ 'outcome' => (string) $operation->outcome, 'status' => (string) $operation->status, 'freshness_state' => $operation->freshnessState()->value, ]); return [ 'id' => (int) $operation->getKey(), 'identifier' => OperationRunLinks::identifier($operation), 'type' => OperationCatalog::label((string) $operation->type), 'icon' => $this->recentOperationIcon((string) $operation->type), 'statusLabel' => $statusSpec->label, 'statusTone' => $statusSpec->color, 'outcomeLabel' => $outcomeSpec->label, 'outcomeTone' => $outcomeSpec->color, 'summary' => OperationUxPresenter::lifecycleAttentionSummary($operation) ?? OperationUxPresenter::surfaceGuidance($operation) ?? $this->overviewText('recent_operation_fallback_summary'), 'url' => OperationRunLinks::view($operation, $tenant), 'createdAt' => $operation->created_at?->diffForHumans(), ]; }) ->values() ->all(); } private function governanceStatusIcon(string $key): string { return match ($key) { 'baseline_compare' => 'heroicon-m-arrows-right-left', 'evidence_coverage' => 'heroicon-m-document-check', 'review_freshness' => 'heroicon-m-clipboard-document-check', 'provider_permissions' => 'heroicon-m-key', 'backup_posture' => 'heroicon-m-archive-box', default => 'heroicon-m-arrow-path-rounded-square', }; } private function recentOperationIcon(string $operationType): string { return match (OperationCatalog::canonicalCode($operationType)) { 'inventory.sync' => 'heroicon-m-arrow-path', 'tenant.review_pack.generate' => 'heroicon-m-document-arrow-down', 'tenant.review.compose' => 'heroicon-m-document-text', 'tenant.evidence.snapshot.generate' => 'heroicon-m-document-duplicate', 'baseline.compare' => 'heroicon-m-arrows-right-left', 'provider.connection.check', 'rbac.health_check' => 'heroicon-m-shield-check', 'permission.posture.check' => 'heroicon-m-key', default => 'heroicon-m-arrow-path-rounded-square', }; } /** * @param array $action * @return array */ private function metricCard( string $key, string $label, int $value, string $description, string $tone, string $icon, array $action, ?array $chart = null, ): array { return array_merge([ 'key' => $key, 'label' => $label, 'value' => $value, 'description' => $description, 'tone' => $tone, 'icon' => $icon, 'chart' => $chart, ], $action); } /** * @param array $action * @return array */ private function actionCandidate(int $priority, string $key, string $title, string $reason, string $impact, string $tone, array $action): array { return array_merge([ 'priority' => $priority, 'key' => $key, 'icon' => $this->recommendedActionIcon($key), 'title' => $title, 'reason' => $reason, 'impact' => $impact, 'tone' => $tone, ], $action); } private function recommendedActionIcon(string $key): string { return match ($key) { 'required_permissions', 'delegated_permissions', 'high_severity_findings' => 'heroicon-m-shield-exclamation', 'overdue_findings' => 'heroicon-o-clock', 'recovery_posture', 'terminal_operations', 'continue_review' => 'heroicon-o-arrow-path-rounded-square', 'risk_exceptions' => 'heroicon-o-exclamation-triangle', default => 'heroicon-o-exclamation-triangle', }; } /** * @param array $parameters * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string} */ private function tenantFindingsAction(Tenant $tenant, ?User $user, string $label, array $parameters = []): array { $canOpen = $this->canOpenTenantCapability($tenant, $user, Capabilities::TENANT_FINDINGS_VIEW); return $this->actionPayload( label: $label, url: $canOpen ? FindingResource::getUrl('index', $parameters, panel: 'tenant', tenant: $tenant) : null, helperText: $canOpen ? null : $this->overviewText('helper_findings_requires_permissions'), ); } /** * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string} */ private function riskExceptionsAction(Tenant $tenant, ?User $user, string $label): array { $canOpen = $this->canOpenTenantCapability($tenant, $user, Capabilities::FINDING_EXCEPTION_VIEW); return $this->actionPayload( label: $label, url: $canOpen ? FindingExceptionResource::getUrl('index', panel: 'tenant', tenant: $tenant) : null, helperText: $canOpen ? null : $this->overviewText('helper_risk_exceptions_requires_permissions'), ); } /** * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string} */ private function tenantReviewAction(Tenant $tenant, ?User $user, string $label, ?TenantReview $review = null): array { $canOpen = $this->canOpenTenantCapability($tenant, $user, Capabilities::TENANT_REVIEW_VIEW); $url = null; if ($canOpen) { $url = $review instanceof TenantReview ? TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant) : TenantReviewResource::tenantScopedUrl('index', tenant: $tenant); } return $this->actionPayload( label: $label, url: $url, helperText: $canOpen ? null : $this->overviewText('helper_review_requires_permissions'), ); } /** * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string} */ private function continueReviewAction(Tenant $tenant, ?User $user, TenantReview $review): array { $canContinue = $this->canOpenTenantCapability($tenant, $user, Capabilities::TENANT_REVIEW_MANAGE); return $this->actionPayload( label: $this->overviewText('action_continue_review'), url: $canContinue ? TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant) : null, helperText: $canContinue ? null : $this->overviewText('helper_continue_review_requires_manage'), ); } /** * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string} */ private function evidenceAction(Tenant $tenant, ?User $user, string $label, ?EvidenceSnapshot $snapshot = null): array { $canOpen = $this->canOpenTenantCapability($tenant, $user, Capabilities::EVIDENCE_VIEW); $url = null; if ($canOpen) { $url = $snapshot instanceof EvidenceSnapshot ? EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], panel: 'tenant', tenant: $tenant) : EvidenceSnapshotResource::getUrl('index', panel: 'tenant', tenant: $tenant); } return $this->actionPayload( label: $label, url: $url, helperText: $canOpen ? null : $this->overviewText('helper_evidence_requires_permissions'), ); } /** * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string} */ private function customerWorkspaceAction(Tenant $tenant, ?User $user, ?ReviewPack $reviewPack): array { $canOpenWorkspace = $user instanceof User && $user->canAccessTenant($tenant) && ( $user->can(Capabilities::TENANT_REVIEW_VIEW, $tenant) || $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant) || $user->can(Capabilities::EVIDENCE_VIEW, $tenant) ); $label = $reviewPack instanceof ReviewPack && (string) $reviewPack->status === 'ready' ? $this->overviewText('action_open_review_pack') : $this->overviewText('action_view_export_artifacts'); $url = null; if ($canOpenWorkspace) { $url = $reviewPack instanceof ReviewPack && $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant) ? ReviewPackResource::getUrl('view', ['record' => $reviewPack], panel: 'tenant', tenant: $tenant) : CustomerReviewWorkspace::tenantPrefilterUrl($tenant); } return $this->actionPayload( label: $label, url: $url, helperText: $canOpenWorkspace ? null : $this->overviewText('helper_customer_workspace_requires_permissions'), ); } /** * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string} */ private function requiredPermissionsAction(Tenant $tenant, ?User $user, string $label): array { $canOpen = $user instanceof User && $user->canAccessTenant($tenant); return $this->actionPayload( label: $label, url: $canOpen ? RequiredPermissionsLinks::requiredPermissions($tenant) : null, helperText: $canOpen ? null : $this->overviewText('helper_required_permissions_unavailable'), ); } /** * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string} */ private function operationsAction(Tenant $tenant, ?User $user, string $label, ?string $activeTab = null, ?string $problemClass = null): array { $canOpen = $user instanceof User && $user->canAccessTenant($tenant); return $this->actionPayload( label: $label, url: $canOpen ? OperationRunLinks::index($tenant, activeTab: $activeTab, problemClass: $problemClass) : null, helperText: $canOpen ? null : $this->overviewText('helper_operations_unavailable'), ); } /** * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string} */ private function baselineCompareAction(Tenant $tenant, ?User $user, string $label): array { $canOpen = $this->canOpenTenantCapability($tenant, $user, Capabilities::TENANT_VIEW); return $this->actionPayload( label: $label, url: $canOpen ? BaselineCompareLanding::getUrl(panel: 'tenant', tenant: $tenant) : null, helperText: $canOpen ? null : $this->overviewText('helper_baseline_compare_requires_permissions'), ); } /** * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string} */ private function backupHealthAction(Tenant $tenant, ?User $user, string $label, TenantBackupHealthAssessment $backupHealth): array { $canOpen = $this->canOpenTenantCapability($tenant, $user, Capabilities::TENANT_VIEW); if (! $canOpen) { return $this->actionPayload( label: $label, url: null, helperText: $this->overviewText('helper_backup_posture_requires_permissions'), ); } $target = $backupHealth->primaryActionTarget; if (! $target instanceof BackupHealthActionTarget) { return $this->actionPayload(label: $label, url: null, helperText: null); } $url = match ($target->surface) { BackupHealthActionTarget::SURFACE_BACKUP_SET_VIEW => $target->recordId !== null ? BackupSetResource::getUrl('view', ['record' => $target->recordId], panel: 'tenant', tenant: $tenant) : BackupSetResource::getUrl('index', panel: 'tenant', tenant: $tenant), BackupHealthActionTarget::SURFACE_BACKUP_SETS_INDEX => BackupSetResource::getUrl('index', panel: 'tenant', tenant: $tenant), BackupHealthActionTarget::SURFACE_BACKUP_SCHEDULES_INDEX => BackupScheduleResource::getUrl('index', panel: 'tenant', tenant: $tenant), default => null, }; return $this->actionPayload( label: $label, url: $url, helperText: $url === null ? null : null, ); } /** * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string} */ private function actionPayload(string $label, ?string $url, ?string $helperText): array { return [ 'actionLabel' => $label, 'actionUrl' => $url, 'actionDisabled' => $url === null && $helperText !== null, 'helperText' => $helperText, ]; } private function canOpenTenantCapability(Tenant $tenant, ?User $user, string $capability): bool { return $user instanceof User && $user->canAccessTenant($tenant) && $user->can($capability, $tenant); } private function reviewSurfaceActionLabel(Tenant $tenant, ?User $user, ?TenantReview $review): string { if (! $review instanceof TenantReview) { return $this->overviewText('action_open_reviews'); } return $this->canOpenTenantCapability($tenant, $user, Capabilities::TENANT_REVIEW_MANAGE) ? $this->overviewText('action_continue_review') : $this->overviewText('action_open_review'); } private function evidenceCoverageValue(?EvidenceSnapshot $snapshot): string { if (! $snapshot instanceof EvidenceSnapshot) { return $this->overviewText('status_unavailable'); } return BadgeRenderer::spec(BadgeDomain::EvidenceCompleteness, (string) $snapshot->completeness_state)->label; } private function evidenceCoverageTone(?EvidenceSnapshot $snapshot): string { if (! $snapshot instanceof EvidenceSnapshot) { return 'warning'; } return BadgeRenderer::spec(BadgeDomain::EvidenceCompleteness, (string) $snapshot->completeness_state)->color; } private function evidenceCoverageDescription(?EvidenceSnapshot $snapshot): string { if (! $snapshot instanceof EvidenceSnapshot) { return $this->overviewText('evidence_unavailable_description'); } return $this->overviewText('evidence_generated_prefix', ['time' => $snapshot->generated_at?->diffForHumans()]); } private function reviewValue(?TenantReview $review): string { if (! $review instanceof TenantReview) { return $this->overviewText('status_not_ready'); } return BadgeRenderer::spec(BadgeDomain::TenantReviewStatus, (string) $review->status)->label; } private function reviewTone(?TenantReview $review): string { if (! $review instanceof TenantReview) { return 'warning'; } return BadgeRenderer::spec(BadgeDomain::TenantReviewStatus, (string) $review->status)->color; } private function reviewDescription(?TenantReview $review): string { if (! $review instanceof TenantReview) { return $this->overviewText('review_unavailable_description'); } $timestamp = $review->published_at ?? $review->generated_at; return $this->overviewText('review_updated_prefix', ['time' => $timestamp?->diffForHumans()]); } /** * @return list> */ private function reviewProgress(?TenantReview $review): array { if (! $review instanceof TenantReview) { return []; } $summary = is_array($review->summary) ? $review->summary : []; $progress = []; $findingsProgress = $this->reviewFindingsProgress($summary); if ($findingsProgress !== null) { $progress[] = $findingsProgress; } $completionProgress = $this->reviewCompletionProgress($summary); if ($completionProgress !== null) { $progress[] = $completionProgress; } return $progress; } /** * @param array $summary * @return array|null */ private function reviewFindingsProgress(array $summary): ?array { $findingCount = (int) ($summary['finding_count'] ?? 0); $reportBuckets = is_array($summary['finding_report_buckets'] ?? null) ? $summary['finding_report_buckets'] : null; if ($findingCount <= 0 || $reportBuckets === null) { return null; } $reviewedCount = min( $findingCount, max(0, array_sum(array_map(static fn (mixed $count): int => max(0, (int) $count), $reportBuckets))), ); return $this->progressItem( key: 'findings_with_outcome', label: $this->overviewText('readiness_current_review_findings_progress_label'), current: $reviewedCount, total: $findingCount, tone: $reviewedCount === $findingCount ? 'success' : 'primary', ); } /** * @param array $summary * @return array|null */ private function reviewCompletionProgress(array $summary): ?array { $sectionCount = (int) ($summary['section_count'] ?? 0); $sectionStateCounts = is_array($summary['section_state_counts'] ?? null) ? $summary['section_state_counts'] : null; if ($sectionCount <= 0 || $sectionStateCounts === null) { return null; } $completeCount = min( $sectionCount, max(0, (int) ($sectionStateCounts['complete'] ?? 0)), ); return $this->progressItem( key: 'review_completion', label: $this->overviewText('readiness_current_review_completion_progress_label'), current: $completeCount, total: $sectionCount, tone: $completeCount === $sectionCount ? 'success' : 'warning', ); } /** * @return array{key:string,label:string,current:int,total:int,percent:int,valueLabel:string,tone:string} */ private function progressItem(string $key, string $label, int $current, int $total, string $tone = 'primary'): array { $current = min($total, max(0, $current)); $percent = (int) round(($current / $total) * 100); return [ 'key' => $key, 'label' => $label, 'current' => $current, 'total' => $total, 'percent' => $percent, 'valueLabel' => sprintf('%d/%d (%d%%)', $current, $total, $percent), 'tone' => $tone, ]; } private function providerHealthHeadline(?ProviderConnection $connection): ?string { if (! $connection instanceof ProviderConnection) { return null; } $displayName = trim((string) ($connection->display_name ?? '')); if ($displayName !== '') { return $displayName; } if ($this->providerChipKey($connection) === 'microsoft') { return 'Microsoft Graph'; } return $this->providerChipLabel($connection); } private function providerHealthStatus(?ProviderConnection $connection): string { if (! $connection instanceof ProviderConnection) { return $this->overviewText('readiness_provider_health_empty_status'); } return BadgeRenderer::spec(BadgeDomain::ProviderVerificationStatus, $connection->verification_status)->label; } private function providerHealthTone(?ProviderConnection $connection): string { if (! $connection instanceof ProviderConnection) { return 'gray'; } return BadgeRenderer::spec(BadgeDomain::ProviderVerificationStatus, $connection->verification_status)->color; } /** * @param array $counts * @param array $freshness */ private function providerHealthDescription(?ProviderConnection $connection, array $counts, array $freshness): string { if (! $connection instanceof ProviderConnection) { return $this->overviewText('readiness_provider_health_empty_description'); } $missingApplication = (int) ($counts['missing_application'] ?? 0); $missingDelegated = (int) ($counts['missing_delegated'] ?? 0); if ($missingApplication > 0 || $missingDelegated > 0 || ($freshness['is_stale'] ?? false) === true) { return $this->providerPermissionsDescription($counts, $freshness); } return match ($this->providerHealthState($connection)) { 'healthy' => $this->overviewText('provider_permissions_complete_description'), 'degraded' => $this->overviewText('readiness_provider_health_degraded_description'), 'blocked' => $this->overviewText('readiness_provider_health_blocked_description'), 'error' => $this->overviewText('readiness_provider_health_error_description'), 'pending' => $this->overviewText('readiness_provider_health_pending_description'), default => $this->overviewText('readiness_provider_health_unknown_description'), }; } /** * @param array $overview */ private function providerPermissionsValue(array $overview): string { return match ((string) ($overview['overall'] ?? VerificationReportOverall::NeedsAttention->value)) { VerificationReportOverall::Ready->value => $this->overviewText('provider_permissions_ready'), VerificationReportOverall::Blocked->value => $this->overviewText('provider_permissions_blocked'), default => $this->overviewText('provider_permissions_needs_attention'), }; } /** * @param array $overview */ private function providerPermissionsTone(array $overview): string { return match ((string) ($overview['overall'] ?? 'needs_attention')) { VerificationReportOverall::Blocked->value => 'danger', VerificationReportOverall::Ready->value => 'success', default => 'warning', }; } /** * @param array $counts * @param array $freshness */ private function providerPermissionsDescription(array $counts, array $freshness): string { $missingApplication = (int) ($counts['missing_application'] ?? 0); $missingDelegated = (int) ($counts['missing_delegated'] ?? 0); $summary = match (true) { $missingApplication > 0 => $this->overviewText('reason_missing_application_permissions', ['count' => $missingApplication]), $missingDelegated > 0 => $this->overviewText('reason_missing_delegated_permissions', ['count' => $missingDelegated]), default => $this->overviewText('provider_permissions_complete_description'), }; if (($freshness['is_stale'] ?? false) === true) { return $summary.' '.$this->overviewText('provider_permissions_stale_suffix'); } return $summary; } private function reviewPackValue(?ReviewPack $reviewPack, ?EvidenceSnapshot $snapshot): string { if ($reviewPack instanceof ReviewPack) { return BadgeRenderer::spec(BadgeDomain::ReviewPackStatus, (string) $reviewPack->status)->label; } if ($snapshot instanceof EvidenceSnapshot) { return $this->overviewText('status_evidence_available'); } return $this->overviewText('status_not_ready'); } private function reviewPackTone(?ReviewPack $reviewPack, ?EvidenceSnapshot $snapshot): string { if ($reviewPack instanceof ReviewPack) { return BadgeRenderer::spec(BadgeDomain::ReviewPackStatus, (string) $reviewPack->status)->color; } if ($snapshot instanceof EvidenceSnapshot) { return 'info'; } return 'warning'; } private function reviewPackDescription(?ReviewPack $reviewPack, ?EvidenceSnapshot $snapshot): string { if ($reviewPack instanceof ReviewPack) { return $this->overviewText('review_pack_updated_prefix', ['time' => $reviewPack->generated_at?->diffForHumans()]); } if ($snapshot instanceof EvidenceSnapshot) { return $this->overviewText('review_pack_evidence_available_description'); } return $this->overviewText('review_pack_unavailable_description'); } private function customerSafeOutputStatus(?ReviewPack $reviewPack, ?EvidenceSnapshot $snapshot): string { if ($reviewPack instanceof ReviewPack) { return $this->reviewPackValue($reviewPack, $snapshot); } if ($snapshot instanceof EvidenceSnapshot) { return $this->overviewText('status_evidence_available'); } return $this->overviewText('readiness_customer_safe_output_empty_status'); } private function customerSafeOutputTone(?ReviewPack $reviewPack, ?EvidenceSnapshot $snapshot): string { if ($reviewPack instanceof ReviewPack) { return $this->reviewPackTone($reviewPack, $snapshot); } if ($snapshot instanceof EvidenceSnapshot) { return 'info'; } return 'gray'; } private function customerSafeOutputDescription(?ReviewPack $reviewPack, ?EvidenceSnapshot $snapshot): string { if ($reviewPack instanceof ReviewPack) { return $this->reviewPackDescription($reviewPack, $snapshot); } if ($snapshot instanceof EvidenceSnapshot) { return $this->overviewText('review_pack_evidence_available_description'); } return $this->overviewText('readiness_customer_safe_output_empty_description'); } /** * @param array{active:int,expiring:int,expired:int,pending:int,total:int} $exceptionStats */ private function riskExceptionValue(array $exceptionStats): string { $needsAction = $exceptionStats['pending'] + $exceptionStats['expiring'] + $exceptionStats['expired']; if ($needsAction > 0) { return $this->overviewText('risk_exceptions_need_action_status', ['count' => $needsAction]); } if ($exceptionStats['active'] > 0) { return $this->overviewText('risk_exceptions_active_status', ['count' => $exceptionStats['active']]); } return $this->overviewText('status_calm'); } /** * @param array{active:int,expiring:int,expired:int,pending:int,total:int} $exceptionStats */ private function riskExceptionTone(array $exceptionStats): string { if ($exceptionStats['expired'] > 0) { return 'danger'; } if ($exceptionStats['pending'] > 0 || $exceptionStats['expiring'] > 0) { return 'warning'; } if ($exceptionStats['active'] > 0) { return 'info'; } return 'success'; } /** * @param array{active:int,expiring:int,expired:int,pending:int,total:int} $exceptionStats */ private function riskExceptionDescription(array $exceptionStats): string { $needsAction = $exceptionStats['pending'] + $exceptionStats['expiring'] + $exceptionStats['expired']; if ($needsAction > 0) { return $this->overviewText('risk_exceptions_pending_description'); } if ($exceptionStats['active'] > 0) { return $this->overviewText('risk_exceptions_active_description'); } return $this->overviewText('risk_exceptions_calm_description'); } private function providerHealthState(?ProviderConnection $connection): string { if (! $connection instanceof ProviderConnection) { return 'unknown'; } $status = $connection->verification_status; if ($status instanceof \BackedEnum) { return (string) $status->value; } return trim((string) ($status ?? 'unknown')) ?: 'unknown'; } private function relativeTime(?\DateTimeInterface $timestamp): ?string { return $timestamp?->diffForHumans(); } private function relativeTimeFromString(mixed $timestamp): ?string { if (! is_string($timestamp) || trim($timestamp) === '') { return null; } try { return Carbon::parse($timestamp)->diffForHumans(); } catch (\Throwable) { return null; } } /** * @param array{label:string,value:string}|null ...$items * @return list */ private function cardMeta(?array ...$items): array { return array_values(array_filter($items, static fn (?array $item): bool => $item !== null)); } /** * @return array{label:string,value:string}|null */ private function metaItem(string $label, ?string $value): ?array { if (! is_string($value) || trim($value) === '') { return null; } return [ 'label' => $label, 'value' => $value, ]; } /** * @param array $replace */ private function overviewText(string $key, array $replace = []): string { return (string) __('localization.dashboard.overview.'.$key, $replace); } }