TenantAtlas/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php
ahmido 9a564d6bf2 feat: environment dashboard operator guidance consolidation (spec 352) (#423)
Implemented the consolidated operator guidance panel for the environment dashboard. Updated EnvironmentDashboardSummaryBuilder to prioritize and select guidance based on the operator guidance contract. Added comprehensive unit, feature, and browser tests to verify the guidance selection logic and UI rendering.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #423
2026-06-04 12:56:02 +00:00

2802 lines
117 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\EnvironmentDashboard;
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Filament\Resources\BackupScheduleResource;
use App\Filament\Resources\BackupSetResource;
use App\Filament\Resources\EnvironmentReviewResource;
use App\Filament\Resources\EvidenceSnapshotResource;
use App\Filament\Resources\FindingExceptionResource;
use App\Filament\Resources\FindingResource;
use App\Filament\Resources\ReviewPackResource;
use App\Models\EnvironmentReview;
use App\Models\EvidenceSnapshot;
use App\Models\Finding;
use App\Models\FindingException;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\ReviewPack;
use App\Models\User;
use App\Services\EnvironmentReviews\EnvironmentReviewReadinessGate;
use App\Services\Intune\ManagedEnvironmentRequiredPermissionsViewModelBuilder;
use App\Support\Auth\Capabilities;
use App\Support\BackupHealth\BackupHealthActionTarget;
use App\Support\BackupHealth\TenantBackupHealthAssessment;
use App\Support\BackupHealth\TenantBackupHealthResolver;
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use App\Support\Baselines\TenantGovernanceAggregate;
use App\Support\Baselines\TenantGovernanceAggregateResolver;
use App\Support\Links\RequiredPermissionsLinks;
use App\Support\ManagedEnvironmentLinks;
use App\Support\OperationCatalog;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OpsUx\ActiveRuns;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\ResolutionGuidance\Adapters\ReviewPackOutputResolutionAdapter;
use App\Support\RestoreSafety\RestoreSafetyResolver;
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
use App\Support\Tenants\TenantRecoveryTriagePresentation;
use App\Support\Verification\VerificationReportOverall;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
final class EnvironmentDashboardSummaryBuilder
{
public function __construct(
private readonly TenantGovernanceAggregateResolver $tenantGovernanceAggregateResolver,
private readonly TenantBackupHealthResolver $tenantBackupHealthResolver,
private readonly RestoreSafetyResolver $restoreSafetyResolver,
private readonly ManagedEnvironmentRequiredPermissionsViewModelBuilder $tenantRequiredPermissionsViewModelBuilder,
) {}
public function build(ManagedEnvironment $tenant, ?User $user = null): EnvironmentDashboardSummary
{
$tenant->loadMissing('workspace', 'providerConnections');
$user = $user ?? auth()->user();
$request = app()->bound('request') ? request() : null;
$cacheKey = sprintf(
'tenant_dashboard.summary.%d.%s',
(int) $tenant->getKey(),
$user instanceof User ? (string) $user->getKey() : 'guest',
);
if ($request?->attributes->has($cacheKey)) {
$cached = $request->attributes->get($cacheKey);
if ($cached instanceof EnvironmentDashboardSummary) {
return $cached;
}
}
$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->latestEnvironmentReview($tenant);
$latestReviewOutputReview = $this->latestReviewOutputReview($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,
);
$posture = $this->posture(
aggregate: $aggregate,
backupHealth: $backupHealth,
recoveryEvidence: $recoveryEvidence,
requiredPermissions: $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,
);
$activeOperationSummary = $this->activeOperationSummary($tenant, $user);
$operatorGuidance = $this->operatorGuidance(
tenant: $tenant,
user: $user,
posture: $posture,
recommendedActions: $recommendedActions,
latestReview: $latestReviewOutputReview,
latestReviewPack: $latestReviewPack,
);
$summary = new EnvironmentDashboardSummary(
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: $posture,
operatorGuidance: $operatorGuidance,
readinessDecision: $this->readinessDecision($operatorGuidance),
kpis: $this->kpis($tenant, $user, $aggregate, $requiredPermissions),
recommendedActions: $recommendedActions,
governanceStatus: $governanceStatus,
readinessCards: $readinessCards,
readinessDimensions: $this->readinessDimensions($governanceStatus, $readinessCards, $activeOperationSummary),
readinessProofPanel: $this->readinessProofPanel($governanceStatus, $readinessCards, $activeOperationSummary),
supportingSignals: $this->supportingSignals(
tenant: $tenant,
user: $user,
aggregate: $aggregate,
backupHealth: $backupHealth,
requiredPermissions: $requiredPermissions,
latestReview: $latestReview,
latestEvidenceSnapshot: $latestEvidenceSnapshot,
activeOperationSummary: $activeOperationSummary,
),
diagnosticsDisclosure: $this->diagnosticsDisclosure(),
activeOperationSummary: $activeOperationSummary,
recentOperations: $this->recentOperationCards($tenant, $recentOperations),
pollingInterval: ActiveRuns::pollingIntervalForTenant($tenant),
);
$request?->attributes->set($cacheKey, $summary);
return $summary;
}
/**
* @param array<string, mixed> $operatorGuidance
* @return array<string, mixed>
*/
private function readinessDecision(array $operatorGuidance): array
{
$actionUrl = is_string($operatorGuidance['actionUrl'] ?? null)
? (string) $operatorGuidance['actionUrl']
: null;
$actionDisabled = (bool) ($operatorGuidance['actionDisabled'] ?? true);
$secondaryActions = is_array($operatorGuidance['secondaryActions'] ?? null)
? array_values(array_filter($operatorGuidance['secondaryActions'], static fn (mixed $action): bool => is_array($action)))
: [];
$helperText = is_string($operatorGuidance['helperText'] ?? null)
? (string) $operatorGuidance['helperText']
: (($actionDisabled && blank($actionUrl) && $secondaryActions === [])
? $this->overviewText('operator_guidance_unavailable_helper')
: null);
return [
'question' => 'Is this environment ready, blocked, stale, or requiring review?',
'title' => (string) ($operatorGuidance['title'] ?? $this->overviewText('tenant_context_unavailable_headline')),
'statusLabel' => 'Status',
'status' => (string) ($operatorGuidance['status'] ?? $this->overviewText('status_unavailable')),
'tone' => (string) ($operatorGuidance['tone'] ?? 'gray'),
'reasonLabel' => $this->overviewText('label_reason'),
'reason' => (string) ($operatorGuidance['reason'] ?? $this->overviewText('tenant_context_unavailable_headline')),
'impactLabel' => $this->overviewText('label_why_this_matters'),
'impact' => (string) ($operatorGuidance['impact'] ?? $this->overviewText('tenant_context_unavailable_summary')),
'proofLabel' => 'Readiness proof',
'proof' => 'Evidence, operation, review, provider, backup, and baseline signals are summarized before diagnostics.',
'nextActionLabel' => $this->overviewText('label_recommended_next_action'),
'actionLabel' => (string) ($operatorGuidance['actionLabel'] ?? $this->overviewText('action_review_environment')),
'actionUrl' => $actionUrl,
'actionDisabled' => $actionDisabled,
'helperText' => $helperText,
'secondaryActions' => $secondaryActions,
];
}
/**
* @param array{status:string,tone:string,headline:string,summary:string} $posture
* @param list<array<string, mixed>> $recommendedActions
* @return array<string, mixed>
*/
private function operatorGuidance(
ManagedEnvironment $tenant,
?User $user,
array $posture,
array $recommendedActions,
?EnvironmentReview $latestReview,
?ReviewPack $latestReviewPack,
): array {
if ($providerGuidance = $this->providerOperatorGuidance($recommendedActions)) {
return $providerGuidance;
}
if ($reviewOutputGuidance = $this->reviewOutputOperatorGuidance($tenant, $user, $latestReview, $latestReviewPack)) {
return $reviewOutputGuidance;
}
foreach ([
'operations_requiring_attention',
'high_severity_findings',
'overdue_findings',
'risk_exceptions',
'recovery_posture',
'continue_review',
] as $key) {
$action = $this->recommendedActionByKey($recommendedActions, $key);
if (! is_array($action)) {
continue;
}
return $this->recommendedActionOperatorGuidance($action);
}
return $this->noUrgentOperatorGuidance($tenant, $user, $posture);
}
/**
* @param list<array<string, mixed>> $recommendedActions
* @return array<string, mixed>|null
*/
private function providerOperatorGuidance(array $recommendedActions): ?array
{
foreach (['required_permissions', 'delegated_permissions'] as $key) {
$action = $this->recommendedActionByKey($recommendedActions, $key);
if (! is_array($action)) {
continue;
}
return [
'key' => 'provider_readiness.'.$key,
'title' => $this->operatorGuidanceTitleForRecommendedAction($key),
'status' => $key === 'required_permissions'
? $this->overviewText('status_blocked')
: $this->overviewText('status_action_needed'),
'tone' => (string) ($action['tone'] ?? 'warning'),
'reason' => (string) ($action['reason'] ?? $this->overviewText('posture_action_needed_fallback_summary')),
'impact' => (string) ($action['impact'] ?? $this->overviewText('posture_action_needed_fallback_summary')),
'actionLabel' => (string) ($action['actionLabel'] ?? $this->overviewText('action_open_required_permissions')),
'actionUrl' => is_string($action['actionUrl'] ?? null) ? (string) $action['actionUrl'] : null,
'actionDisabled' => (bool) ($action['actionDisabled'] ?? blank($action['actionUrl'] ?? null)),
'helperText' => is_string($action['helperText'] ?? null) ? (string) $action['helperText'] : null,
'secondaryActions' => [],
'source' => [
'type' => 'recommended_action',
'key' => $key,
],
];
}
return null;
}
/**
* @return array<string, mixed>|null
*/
private function reviewOutputOperatorGuidance(
ManagedEnvironment $tenant,
?User $user,
?EnvironmentReview $latestReview,
?ReviewPack $latestReviewPack,
): ?array {
$resolutionCase = $this->reviewOutputResolutionCaseForDashboard($tenant, $user, $latestReview);
if (! is_array($resolutionCase)) {
return null;
}
$status = (string) ($resolutionCase['status'] ?? 'unknown');
if (in_array($status, ['ready', 'unknown'], true)) {
return null;
}
$primaryAction = $this->operatorGuidanceActionFromResolutionAction($resolutionCase['primary_action'] ?? null);
if (blank($primaryAction['actionUrl'] ?? null)) {
$workspaceAction = $this->customerWorkspaceAction($tenant, $user, $latestReviewPack);
$primaryAction = [
'actionLabel' => (string) ($workspaceAction['actionLabel'] ?? $this->overviewText('action_open_customer_workspace')),
'actionUrl' => is_string($workspaceAction['actionUrl'] ?? null) ? (string) $workspaceAction['actionUrl'] : null,
'actionDisabled' => (bool) ($workspaceAction['actionDisabled'] ?? true),
'helperText' => is_string($workspaceAction['helperText'] ?? null)
? (string) $workspaceAction['helperText']
: $this->overviewText('operator_guidance_unavailable_helper'),
];
}
return [
'key' => (string) ($resolutionCase['key'] ?? 'review_output.unknown'),
'title' => (string) ($resolutionCase['title'] ?? $this->overviewText('operator_guidance_review_output_title')),
'status' => $this->operatorGuidanceStatusFromResolutionCase($resolutionCase),
'tone' => $this->operatorGuidanceToneFromResolutionCase($resolutionCase),
'reason' => (string) ($resolutionCase['reason'] ?? $this->overviewText('reason_continue_review')),
'impact' => (string) ($resolutionCase['impact'] ?? $this->overviewText('impact_continue_review')),
'actionLabel' => (string) ($primaryAction['actionLabel'] ?? $this->overviewText('action_open_customer_workspace')),
'actionUrl' => is_string($primaryAction['actionUrl'] ?? null) ? (string) $primaryAction['actionUrl'] : null,
'actionDisabled' => (bool) ($primaryAction['actionDisabled'] ?? blank($primaryAction['actionUrl'] ?? null)),
'helperText' => is_string($primaryAction['helperText'] ?? null) ? (string) $primaryAction['helperText'] : null,
'secondaryActions' => $this->reviewOutputOperatorSecondaryActions(
resolutionCase: $resolutionCase,
tenant: $tenant,
user: $user,
latestReviewPack: $latestReviewPack,
primaryActionUrl: is_string($primaryAction['actionUrl'] ?? null) ? (string) $primaryAction['actionUrl'] : null,
),
'source' => [
'type' => 'review_output_resolution',
'key' => (string) ($resolutionCase['key'] ?? 'review_output.unknown'),
],
];
}
/**
* @param list<array<string, mixed>> $recommendedActions
* @return array<string, mixed>|null
*/
private function recommendedActionByKey(array $recommendedActions, string $key): ?array
{
return collect($recommendedActions)
->first(static fn (array $action): bool => ($action['key'] ?? null) === $key);
}
/**
* @param array<string, mixed> $action
* @return array<string, mixed>
*/
private function recommendedActionOperatorGuidance(array $action): array
{
$key = (string) ($action['key'] ?? 'dashboard_follow_up');
return [
'key' => 'recommended_action.'.$key,
'title' => $this->operatorGuidanceTitleForRecommendedAction($key),
'status' => $this->operatorGuidanceStatusForRecommendedAction($key),
'tone' => (string) ($action['tone'] ?? 'warning'),
'reason' => (string) ($action['reason'] ?? $this->overviewText('posture_action_needed_fallback_summary')),
'impact' => (string) ($action['impact'] ?? $this->overviewText('posture_action_needed_fallback_summary')),
'actionLabel' => (string) ($action['actionLabel'] ?? $this->overviewText('action_review_environment')),
'actionUrl' => is_string($action['actionUrl'] ?? null) ? (string) $action['actionUrl'] : null,
'actionDisabled' => (bool) ($action['actionDisabled'] ?? blank($action['actionUrl'] ?? null)),
'helperText' => is_string($action['helperText'] ?? null) ? (string) $action['helperText'] : null,
'secondaryActions' => [],
'source' => [
'type' => 'recommended_action',
'key' => $key,
],
];
}
/**
* @param array{status:string,tone:string,headline:string,summary:string} $posture
* @return array<string, mixed>
*/
private function noUrgentOperatorGuidance(ManagedEnvironment $tenant, ?User $user, array $posture): array
{
$reviewAction = $this->environmentReviewAction($tenant, $user, $this->overviewText('action_review_environment'));
return [
'key' => 'environment.no_urgent_action',
'title' => $this->overviewText('operator_guidance_no_urgent_title'),
'status' => (string) ($posture['status'] ?? $this->overviewText('status_calm')),
'tone' => (string) ($posture['tone'] ?? 'success'),
'reason' => (string) ($posture['headline'] ?? $this->overviewText('posture_calm_headline')),
'impact' => (string) ($posture['summary'] ?? $this->overviewText('posture_calm_summary')),
'actionLabel' => (string) ($reviewAction['actionLabel'] ?? $this->overviewText('action_review_environment')),
'actionUrl' => is_string($reviewAction['actionUrl'] ?? null) ? (string) $reviewAction['actionUrl'] : null,
'actionDisabled' => (bool) ($reviewAction['actionDisabled'] ?? blank($reviewAction['actionUrl'] ?? null)),
'helperText' => is_string($reviewAction['helperText'] ?? null) ? (string) $reviewAction['helperText'] : null,
'secondaryActions' => [],
'source' => [
'type' => 'dashboard_fallback',
],
];
}
/**
* @return array<string, mixed>|null
*/
private function reviewOutputResolutionCaseForDashboard(
ManagedEnvironment $tenant,
?User $user,
?EnvironmentReview $review,
): ?array {
if (! $review instanceof EnvironmentReview) {
return null;
}
$review->loadMissing([
'tenant',
'evidenceSnapshot',
'currentExportReviewPack.operationRun',
'operationRun',
'supersededByReview',
]);
$reviewUrl = $this->customerWorkspaceUrl($tenant, $user);
$evidenceUrl = $this->evidenceUrlForReviewOutput($tenant, $user, $review);
$operationUrl = $this->operationUrlForReviewOutput($review);
$successorReviewUrl = $this->successorReviewUrlForDashboard($tenant, $user, $review);
$guidance = ReviewPackOutputResolutionGuidance::fromReview($review, [
'download' => null,
'review' => $reviewUrl,
'evidence' => $evidenceUrl,
'operation' => $operationUrl,
]);
$resolutionCase = ReviewPackOutputResolutionAdapter::fromGuidance(
review: $review,
guidance: $guidance,
sourceSurface: 'environment_dashboard',
context: [
'urls' => [
'review' => $reviewUrl,
'evidence' => $evidenceUrl,
'operation' => $operationUrl,
'download' => null,
'successor_review' => $successorReviewUrl,
],
'execution' => [
'can_manage_review' => $this->canOpenTenantCapability($tenant, $user, Capabilities::ENVIRONMENT_REVIEW_MANAGE),
'successor_review_status' => $this->successorReviewStatusForDashboard($review),
],
],
);
return $this->decorateSuccessorDashboardResolutionCase($resolutionCase, $review);
}
/**
* @param array<string, mixed> $resolutionCase
* @return array<string, mixed>
*/
private function decorateSuccessorDashboardResolutionCase(array $resolutionCase, EnvironmentReview $review): array
{
if (data_get($resolutionCase, 'primary_action.key') !== 'open_successor_review') {
return $resolutionCase;
}
$successor = $this->successorReviewForDashboard($review);
if (! $successor instanceof EnvironmentReview || ! $successor->isMutable()) {
return $resolutionCase;
}
$canPublishSuccessor = app(EnvironmentReviewReadinessGate::class)->canPublish($successor);
return array_replace($resolutionCase, [
'title' => __('localization.review.draft_review_exists'),
'reason' => $canPublishSuccessor
? __('localization.review.draft_review_exists_ready_reason')
: __('localization.review.draft_review_exists_blocked_reason'),
'impact' => $canPublishSuccessor
? __('localization.review.draft_review_exists_ready_impact')
: __('localization.review.draft_review_exists_blocked_impact'),
]);
}
private function successorReviewForDashboard(EnvironmentReview $review): ?EnvironmentReview
{
if ($review->relationLoaded('supersededByReview')) {
return $review->supersededByReview instanceof EnvironmentReview
? $review->supersededByReview
: null;
}
if (! is_numeric($review->superseded_by_review_id)) {
return null;
}
return EnvironmentReview::query()
->with(['tenant', 'sections', 'evidenceSnapshot', 'currentExportReviewPack'])
->whereKey((int) $review->superseded_by_review_id)
->where('workspace_id', (int) $review->workspace_id)
->where('managed_environment_id', (int) $review->managed_environment_id)
->first();
}
private function successorReviewStatusForDashboard(EnvironmentReview $review): ?string
{
return $this->successorReviewForDashboard($review)?->status;
}
private function successorReviewUrlForDashboard(ManagedEnvironment $tenant, ?User $user, EnvironmentReview $review): ?string
{
if (! $this->canOpenTenantCapability($tenant, $user, Capabilities::ENVIRONMENT_REVIEW_VIEW)) {
return null;
}
if (! is_numeric($review->superseded_by_review_id)) {
return null;
}
return EnvironmentReviewResource::environmentScopedUrl('view', ['record' => (int) $review->superseded_by_review_id], $tenant);
}
private function customerWorkspaceUrl(ManagedEnvironment $tenant, ?User $user): ?string
{
$canOpenWorkspace = $user instanceof User
&& $user->canAccessTenant($tenant)
&& (
$user->can(Capabilities::ENVIRONMENT_REVIEW_VIEW, $tenant)
|| $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)
|| $user->can(Capabilities::EVIDENCE_VIEW, $tenant)
);
return $canOpenWorkspace ? CustomerReviewWorkspace::environmentFilterUrl($tenant) : null;
}
private function evidenceUrlForReviewOutput(ManagedEnvironment $tenant, ?User $user, EnvironmentReview $review): ?string
{
if (! $review->evidenceSnapshot instanceof EvidenceSnapshot) {
return $this->canOpenTenantCapability($tenant, $user, Capabilities::EVIDENCE_VIEW)
? EvidenceSnapshotResource::getUrl('index', tenant: $tenant)
: null;
}
if (! $this->canOpenTenantCapability($tenant, $user, Capabilities::EVIDENCE_VIEW)) {
return null;
}
return EvidenceSnapshotResource::getUrl('view', ['record' => $review->evidenceSnapshot], tenant: $tenant);
}
private function operationUrlForReviewOutput(EnvironmentReview $review): ?string
{
$operationRun = $review->currentExportReviewPack?->operationRun ?? $review->operationRun;
return $operationRun instanceof OperationRun
? OperationRunLinks::tenantlessView((int) $operationRun->getKey())
: null;
}
/**
* @param array<string, mixed>|null $action
* @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string}
*/
private function operatorGuidanceActionFromResolutionAction(?array $action): array
{
return [
'actionLabel' => is_string($action['label'] ?? null)
? (string) $action['label']
: $this->overviewText('action_review_environment'),
'actionUrl' => is_string($action['url'] ?? null) ? (string) $action['url'] : null,
'actionDisabled' => ! is_string($action['url'] ?? null) || trim((string) $action['url']) === '',
'helperText' => is_string($action['disabled_reason'] ?? null) ? (string) $action['disabled_reason'] : null,
];
}
/**
* @param array<string, mixed> $resolutionCase
* @return list<array{key:string,actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string}>
*/
private function reviewOutputOperatorSecondaryActions(
array $resolutionCase,
ManagedEnvironment $tenant,
?User $user,
?ReviewPack $latestReviewPack,
?string $primaryActionUrl,
): array {
$actions = collect(is_array($resolutionCase['secondary_actions'] ?? null) ? $resolutionCase['secondary_actions'] : [])
->filter(static fn (mixed $action): bool => is_array($action) && filled($action['url'] ?? null))
->map(function (array $action): array {
return [
'key' => (string) ($action['key'] ?? 'secondary_action'),
'actionLabel' => (string) ($action['label'] ?? $this->overviewText('action_review_environment')),
'actionUrl' => is_string($action['url'] ?? null) ? (string) $action['url'] : null,
'actionDisabled' => blank($action['url'] ?? null),
'helperText' => is_string($action['disabled_reason'] ?? null) ? (string) $action['disabled_reason'] : null,
];
})
->reject(static fn (array $action): bool => $primaryActionUrl !== null && $action['actionUrl'] === $primaryActionUrl)
->unique(static fn (array $action): string => $action['actionLabel'].'|'.$action['actionUrl'])
->values();
$workspaceAction = $this->customerWorkspaceAction($tenant, $user, $latestReviewPack);
if (
filled($workspaceAction['actionUrl'] ?? null)
&& $workspaceAction['actionUrl'] !== $primaryActionUrl
&& ! $actions->contains(static fn (array $action): bool => $action['actionUrl'] === $workspaceAction['actionUrl'])
) {
$actions->push([
'key' => 'customer_review_workspace',
'actionLabel' => $this->overviewText('action_open_customer_workspace'),
'actionUrl' => (string) $workspaceAction['actionUrl'],
'actionDisabled' => false,
'helperText' => null,
]);
}
return $actions
->take(3)
->all();
}
/**
* @param array<string, mixed> $resolutionCase
*/
private function operatorGuidanceStatusFromResolutionCase(array $resolutionCase): string
{
return match ((string) ($resolutionCase['status'] ?? 'action_required')) {
'blocked' => $this->overviewText('status_blocked'),
'ready' => $this->overviewText('status_calm'),
default => $this->overviewText('status_action_needed'),
};
}
/**
* @param array<string, mixed> $resolutionCase
*/
private function operatorGuidanceToneFromResolutionCase(array $resolutionCase): string
{
return match ((string) ($resolutionCase['severity'] ?? 'warning')) {
'critical' => 'danger',
'success' => 'success',
default => 'warning',
};
}
private function operatorGuidanceTitleForRecommendedAction(string $key): string
{
return match ($key) {
'required_permissions' => $this->overviewText('operator_guidance_provider_blocked_title'),
'delegated_permissions' => $this->overviewText('operator_guidance_provider_attention_title'),
'operations_requiring_attention' => $this->overviewText('operator_guidance_operations_title'),
'high_severity_findings', 'overdue_findings' => $this->overviewText('operator_guidance_findings_title'),
'risk_exceptions' => $this->overviewText('operator_guidance_risks_title'),
'recovery_posture' => $this->overviewText('operator_guidance_recovery_title'),
'continue_review' => $this->overviewText('operator_guidance_review_follow_up_title'),
default => $this->overviewText('operator_guidance_attention_title'),
};
}
private function operatorGuidanceStatusForRecommendedAction(string $key): string
{
return match ($key) {
'required_permissions' => $this->overviewText('status_blocked'),
'continue_review' => $this->overviewText('status_action_needed'),
default => $this->overviewText('status_action_needed'),
};
}
/**
* @param list<array<string, mixed>> $governanceStatus
* @param list<array<string, mixed>> $readinessCards
* @param array<string, mixed>|null $activeOperationSummary
* @return list<array<string, mixed>>
*/
private function readinessDimensions(array $governanceStatus, array $readinessCards, ?array $activeOperationSummary): array
{
$dimensions = [];
foreach ($governanceStatus as $status) {
$dimensions[] = [
'key' => (string) ($status['key'] ?? 'governance_status'),
'title' => (string) ($status['label'] ?? 'Governance signal'),
'status' => (string) ($status['value'] ?? $this->overviewText('status_unavailable')),
'tone' => (string) ($status['tone'] ?? 'gray'),
'description' => (string) ($status['description'] ?? ''),
'actionLabel' => $status['actionLabel'] ?? null,
'actionUrl' => $status['actionUrl'] ?? null,
'helperText' => $status['helperText'] ?? null,
];
}
foreach ($readinessCards as $card) {
$dimensions[] = [
'key' => (string) ($card['key'] ?? 'readiness_card'),
'title' => (string) ($card['title'] ?? 'Readiness signal'),
'status' => (string) ($card['status'] ?? $this->overviewText('status_unavailable')),
'tone' => (string) ($card['tone'] ?? 'gray'),
'description' => (string) ($card['body'] ?? ''),
'actionLabel' => $card['actionLabel'] ?? null,
'actionUrl' => $card['actionUrl'] ?? null,
'helperText' => $card['helperText'] ?? null,
];
}
if (is_array($activeOperationSummary)) {
$dimensions[] = [
'key' => 'operation_attention',
'title' => 'Operation proof',
'status' => (string) ($activeOperationSummary['count'] ?? $this->overviewText('status_unavailable')),
'tone' => (string) ($activeOperationSummary['tone'] ?? 'warning'),
'description' => 'Operations requiring attention must be reviewed before the environment is treated as calm.',
'actionLabel' => $activeOperationSummary['secondaryActionLabel'] ?? null,
'actionUrl' => $activeOperationSummary['secondaryActionUrl'] ?? null,
'helperText' => null,
];
}
return $dimensions;
}
/**
* @param list<array<string, mixed>> $governanceStatus
* @param list<array<string, mixed>> $readinessCards
* @param array<string, mixed>|null $activeOperationSummary
* @return list<array<string, mixed>>
*/
private function readinessProofPanel(array $governanceStatus, array $readinessCards, ?array $activeOperationSummary): array
{
$proofKeys = [
'baseline_compare',
'evidence_coverage',
'review_freshness',
'provider_permissions',
'backup_posture',
];
$items = collect($governanceStatus)
->filter(static fn (array $status): bool => in_array((string) ($status['key'] ?? ''), $proofKeys, true))
->map(static fn (array $status): array => [
'key' => (string) ($status['key'] ?? 'proof'),
'label' => (string) ($status['label'] ?? 'Proof path'),
'value' => (string) ($status['value'] ?? 'Unavailable'),
'tone' => (string) ($status['tone'] ?? 'gray'),
'description' => (string) ($status['description'] ?? ''),
'actionLabel' => $status['actionLabel'] ?? null,
'actionUrl' => $status['actionUrl'] ?? null,
'helperText' => $status['helperText'] ?? null,
])
->values()
->all();
$customerOutput = collect($readinessCards)
->first(static fn (array $card): bool => ($card['key'] ?? null) === 'customer_safe_output');
if (is_array($customerOutput)) {
$items[] = [
'key' => 'review_pack',
'label' => 'Review pack',
'value' => (string) ($customerOutput['status'] ?? 'Unavailable'),
'tone' => (string) ($customerOutput['tone'] ?? 'gray'),
'description' => (string) ($customerOutput['body'] ?? ''),
'actionLabel' => $customerOutput['actionLabel'] ?? null,
'actionUrl' => $customerOutput['actionUrl'] ?? null,
'helperText' => $customerOutput['helperText'] ?? null,
];
}
if (is_array($activeOperationSummary)) {
$items[] = [
'key' => 'operation_proof',
'label' => 'Operation proof',
'value' => (string) ($activeOperationSummary['count'] ?? 'Unavailable'),
'tone' => (string) ($activeOperationSummary['tone'] ?? 'warning'),
'description' => 'Latest operation proof is available through the operations detail path.',
'actionLabel' => $activeOperationSummary['secondaryActionLabel'] ?? null,
'actionUrl' => $activeOperationSummary['secondaryActionUrl'] ?? null,
'helperText' => null,
];
}
return $items;
}
/**
* @param array<string, mixed> $requiredPermissions
* @param array<string, mixed>|null $activeOperationSummary
* @return list<array<string, mixed>>
*/
private function supportingSignals(
ManagedEnvironment $tenant,
?User $user,
TenantGovernanceAggregate $aggregate,
TenantBackupHealthAssessment $backupHealth,
array $requiredPermissions,
?EnvironmentReview $latestReview,
?EvidenceSnapshot $latestEvidenceSnapshot,
?array $activeOperationSummary,
): array {
$overview = is_array($requiredPermissions['overview'] ?? null)
? $requiredPermissions['overview']
: [];
$providerPermissionsReady = $this->providerPermissionsTone($overview) === 'success';
$operationCount = (int) ($activeOperationSummary['count'] ?? 0);
$operationsAction = $operationCount > 0 && is_string($activeOperationSummary['secondaryActionUrl'] ?? null)
? [
'actionLabel' => (string) ($activeOperationSummary['secondaryActionLabel'] ?? $this->overviewText('action_open_operations_hub')),
'actionUrl' => (string) $activeOperationSummary['secondaryActionUrl'],
'actionDisabled' => false,
'helperText' => null,
]
: $this->operationsAction(
tenant: $tenant,
user: $user,
label: $this->overviewText('action_open_operations_hub'),
activeTab: 'active',
problemClass: null,
);
return [
$this->supportingSignal(
key: 'baseline_assignment',
label: 'Baseline assignment',
value: match ($aggregate->compareState) {
'no_tenant' => 'Unavailable',
'no_assignment' => 'Missing',
default => 'Ready',
},
tone: match ($aggregate->compareState) {
'no_assignment' => 'warning',
'no_tenant' => 'gray',
default => 'success',
},
action: $this->baselineCompareAction($tenant, $user, $this->overviewText('action_open_baseline_compare')),
),
$this->supportingSignal(
key: 'evidence_snapshot',
label: 'Evidence snapshot',
value: $latestEvidenceSnapshot instanceof EvidenceSnapshot ? 'Available' : 'Unavailable',
tone: $latestEvidenceSnapshot instanceof EvidenceSnapshot ? 'success' : 'warning',
action: $this->evidenceAction($tenant, $user, $this->overviewText('action_open_evidence'), $latestEvidenceSnapshot),
),
$this->supportingSignal(
key: 'review_freshness',
label: 'Review freshness',
value: $latestReview instanceof EnvironmentReview ? 'Ready' : 'Not ready',
tone: $latestReview instanceof EnvironmentReview ? 'success' : 'warning',
action: $this->environmentReviewAction($tenant, $user, 'Open reviews', $latestReview),
),
$this->supportingSignal(
key: 'provider_permissions',
label: 'Provider permissions',
value: $providerPermissionsReady ? 'Ready' : 'Missing',
tone: $providerPermissionsReady ? 'success' : 'danger',
action: $this->requiredPermissionsAction($tenant, $user, $this->overviewText('action_open_required_permissions')),
),
$this->supportingSignal(
key: 'backup_posture',
label: 'Backup posture',
value: $backupHealth->posture === TenantBackupHealthAssessment::POSTURE_HEALTHY ? 'Present' : 'Absent',
tone: $backupHealth->posture === TenantBackupHealthAssessment::POSTURE_HEALTHY ? 'success' : 'warning',
action: $this->backupHealthAction($tenant, $user, $this->overviewText('action_open_backup_posture'), $backupHealth),
),
$this->supportingSignal(
key: 'operations_follow_up',
label: 'Operations follow-up',
value: $operationCount === 1
? '1 requires review'
: ($operationCount > 1 ? $operationCount.' require review' : 'None require review'),
tone: $operationCount > 0 ? 'warning' : 'gray',
action: $operationsAction,
),
];
}
/**
* @param array<string, mixed> $action
* @return array<string, mixed>
*/
private function supportingSignal(string $key, string $label, string $value, string $tone, array $action): array
{
return [
'key' => $key,
'label' => $label,
'value' => $value,
'tone' => $tone,
'actionLabel' => is_string($action['actionLabel'] ?? null) ? (string) $action['actionLabel'] : null,
'actionUrl' => is_string($action['actionUrl'] ?? null) ? (string) $action['actionUrl'] : null,
'actionDisabled' => (bool) ($action['actionDisabled'] ?? false),
'helperText' => is_string($action['helperText'] ?? null) ? (string) $action['helperText'] : null,
];
}
/**
* @return array<string, mixed>
*/
private function diagnosticsDisclosure(): array
{
return [
'label' => 'Diagnostics - Collapsed',
'summary' => 'Support diagnostics stay closed by default and require the existing diagnostics capability.',
'tone' => 'gray',
];
}
private function primaryProviderConnection(ManagedEnvironment $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<OperationRun> $recentOperations
*/
private function latestActivityLabel(
?ProviderConnection $primaryProviderConnection,
?EnvironmentReview $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<OperationRun> $recentOperations
*/
private function latestActivityTimestamp(
?ProviderConnection $primaryProviderConnection,
?EnvironmentReview $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 latestEnvironmentReview(ManagedEnvironment $tenant): ?EnvironmentReview
{
return EnvironmentReview::query()
->with(['tenant', 'evidenceSnapshot', 'currentExportReviewPack'])
->where('managed_environment_id', (int) $tenant->getKey())
->latest('generated_at')
->latest('id')
->first();
}
private function latestReviewOutputReview(ManagedEnvironment $tenant): ?EnvironmentReview
{
$review = EnvironmentReview::query()
->with(['tenant', 'evidenceSnapshot', 'currentExportReviewPack', 'supersededByReview'])
->where('managed_environment_id', (int) $tenant->getKey())
->where(function (Builder $query): void {
$query
->whereNotNull('current_export_review_pack_id')
->orWhereIn('status', ['published', 'superseded']);
})
->latest('published_at')
->latest('generated_at')
->latest('id')
->first();
return $review instanceof EnvironmentReview
? $review
: $this->latestEnvironmentReview($tenant);
}
private function latestReviewPack(ManagedEnvironment $tenant): ?ReviewPack
{
return ReviewPack::query()
->with(['tenant', 'environmentReview'])
->where('managed_environment_id', (int) $tenant->getKey())
->latest('generated_at')
->latest('id')
->first();
}
private function latestEvidenceSnapshot(ManagedEnvironment $tenant): ?EvidenceSnapshot
{
return EvidenceSnapshot::query()
->where('managed_environment_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(ManagedEnvironment $tenant): array
{
$counts = FindingException::query()
->where('managed_environment_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<OperationRun>
*/
private function recentOperations(ManagedEnvironment $tenant): array
{
return OperationRun::query()
->where('managed_environment_id', (int) $tenant->getKey())
->latest('created_at')
->latest('id')
->limit(4)
->get()
->all();
}
/**
* @param array<string, mixed> $requiredPermissions
* @param list<array<string, mixed>> $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<string, mixed> $requiredPermissions
* @return list<array<string, mixed>>
*/
private function kpis(ManagedEnvironment $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) $this->operationsRequiringAttentionQuery($tenant)->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_open_operations_hub'),
activeTab: $operationsNeedingFollowUp > 0 ? OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP : 'active',
problemClass: $operationsNeedingFollowUp > 0 ? OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP : null,
),
),
];
}
/**
* @param list<int>|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<int>|null $chart
*/
private function activeOperationsKpiDescription(int $count, ?array $chart): string
{
if ($count <= 0) {
return $this->overviewText('kpi_active_operations_tendency_none');
}
if ($count === 1) {
return $this->overviewText('kpi_active_operations_tendency_one');
}
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<int>|null
*/
private function highSeverityFindingsChart(ManagedEnvironment $tenant): ?array
{
$window = $this->sevenDayWindow();
$byDay = Finding::query()
->where('managed_environment_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<int>|null
*/
private function operationsFollowUpChart(ManagedEnvironment $tenant): ?array
{
$window = $this->sevenDayWindow();
$byDay = $this->operationsRequiringAttentionQuery($tenant)
->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<string, int|string> $byDay
* @return list<int>|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<string, mixed> $requiredPermissions
* @param array{active:int,expiring:int,expired:int,pending:int,total:int} $exceptionStats
* @return list<array<string, mixed>>
*/
private function recommendedActions(
ManagedEnvironment $tenant,
?User $user,
TenantGovernanceAggregate $aggregate,
TenantBackupHealthAssessment $backupHealth,
array $recoveryEvidence,
array $requiredPermissions,
?EnvironmentReview $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_review_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_review_permissions')),
);
} elseif ($missingDelegatedPermissions > 0) {
$candidates[] = $this->actionCandidate(
priority: 20,
key: 'delegated_permissions',
title: $this->overviewText('action_review_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_review_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),
);
}
$operationsRequiringAttention = $this->operationsRequiringAttentionRuns($tenant);
if ($operationsRequiringAttention->isNotEmpty()) {
$dominantProblemClass = $this->dominantAttentionProblemClass($operationsRequiringAttention);
$candidates[] = $this->actionCandidate(
priority: 35,
key: 'operations_requiring_attention',
title: $this->overviewText('action_review_operations_requiring_attention'),
reason: $this->overviewText('reason_operations_requiring_attention'),
impact: $this->overviewText('impact_operations_requiring_attention'),
tone: 'danger',
action: $this->operationsAction(
tenant: $tenant,
user: $user,
label: $this->overviewText('action_review_operations'),
activeTab: $dominantProblemClass,
problemClass: $dominantProblemClass,
),
);
}
if ($latestReview instanceof EnvironmentReview && $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<string, mixed> $requiredPermissions
* @return list<array<string, mixed>>
*/
private function governanceStatus(
ManagedEnvironment $tenant,
?User $user,
TenantGovernanceAggregate $aggregate,
TenantBackupHealthAssessment $backupHealth,
array $requiredPermissions,
?EnvironmentReview $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' => $this->baselineCompareSignalValue($aggregate),
'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->environmentReviewAction($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),
],
];
}
private function baselineCompareSignalValue(TenantGovernanceAggregate $aggregate): string
{
if ($aggregate->compareState === 'no_assignment') {
return 'Baseline missing';
}
return match ($aggregate->stateFamily) {
'positive' => 'Ready',
'caution' => 'Needs review',
'stale' => 'Stale',
'action_required' => 'Action required',
'in_progress' => 'In progress',
default => $this->overviewText('status_unavailable'),
};
}
/**
* @param array<string, mixed> $requiredPermissions
* @param array{active:int,expiring:int,expired:int,pending:int,total:int} $exceptionStats
* @return list<array<string, mixed>>
*/
private function readinessCards(
ManagedEnvironment $tenant,
?User $user,
?ProviderConnection $primaryProviderConnection,
array $requiredPermissions,
?EnvironmentReview $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<string, mixed>
*/
private function currentReviewCard(ManagedEnvironment $tenant, ?User $user, ?EnvironmentReview $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 EnvironmentReview
? $this->reviewValue($latestReview)
: $this->overviewText('readiness_current_review_empty_status'),
'tone' => $latestReview instanceof EnvironmentReview ? $this->reviewTone($latestReview) : 'gray',
'body' => $latestReview instanceof EnvironmentReview
? $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->environmentReviewAction($tenant, $user, $this->reviewSurfaceActionLabel($tenant, $user, $latestReview), $latestReview),
];
}
/**
* @param array{active:int,expiring:int,expired:int,pending:int,total:int} $exceptionStats
* @return array<string, mixed>
*/
private function riskExceptionsCard(ManagedEnvironment $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<string, mixed> $requiredPermissions
* @return array<string, mixed>
*/
private function providerHealthCard(
ManagedEnvironment $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<string, mixed>
*/
private function customerSafeOutputCard(
ManagedEnvironment $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<OperationRun> $recentOperations
* @return list<array<string, mixed>>
*/
private function recentOperationCards(ManagedEnvironment $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();
}
/**
* @return array<string, mixed>|null
*/
private function activeOperationSummary(ManagedEnvironment $tenant, ?User $user): ?array
{
if (! $user instanceof User || ! $user->canAccessTenant($tenant)) {
return null;
}
$qualifyingRuns = $this->operationsRequiringAttentionRuns($tenant);
if ($qualifyingRuns->isEmpty()) {
return null;
}
$dominantProblemClass = $this->dominantAttentionProblemClass($qualifyingRuns);
return [
'title' => $this->overviewText('operations_attention_title'),
'count' => $qualifyingRuns->count(),
'tone' => 'warning',
'secondaryActionLabel' => $this->overviewText('action_open_operations_hub'),
'secondaryActionUrl' => OperationRunLinks::index(
$tenant,
activeTab: $dominantProblemClass,
problemClass: $dominantProblemClass,
),
'items' => $this->attentionOperationItems($qualifyingRuns, $tenant),
];
}
private function compareActiveOperationSummaryRuns(OperationRun $left, OperationRun $right): int
{
$priorityComparison = $this->activeOperationSummaryPriority($left) <=> $this->activeOperationSummaryPriority($right);
if ($priorityComparison !== 0) {
return $priorityComparison;
}
$timestampComparison = $this->activeOperationSummaryTimestamp($right) <=> $this->activeOperationSummaryTimestamp($left);
if ($timestampComparison !== 0) {
return $timestampComparison;
}
return ((int) $right->getKey()) <=> ((int) $left->getKey());
}
private function activeOperationSummaryPriority(OperationRun $run): int
{
return match ($run->problemClass()) {
OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP => 0,
OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION => 1,
default => 2,
};
}
private function activeOperationSummaryTimestamp(OperationRun $run): int
{
return ($run->completed_at ?? $run->started_at ?? $run->created_at)?->getTimestamp() ?? 0;
}
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',
'environment.review_pack.generate' => 'heroicon-m-document-arrow-down',
'environment.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<string, mixed> $action
* @return array<string, mixed>
*/
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<string, mixed> $action
* @return array<string, mixed>
*/
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', 'operations_requiring_attention', 'continue_review' => 'heroicon-o-arrow-path-rounded-square',
'risk_exceptions' => 'heroicon-o-exclamation-triangle',
default => 'heroicon-o-exclamation-triangle',
};
}
/**
* @param array<string, mixed> $parameters
* @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string}
*/
private function tenantFindingsAction(ManagedEnvironment $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, 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(ManagedEnvironment $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', 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 environmentReviewAction(ManagedEnvironment $tenant, ?User $user, string $label, ?EnvironmentReview $review = null): array
{
$canOpen = $this->canOpenTenantCapability($tenant, $user, Capabilities::ENVIRONMENT_REVIEW_VIEW);
$url = null;
if ($canOpen) {
$url = $review instanceof EnvironmentReview
? EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)
: EnvironmentReviewResource::environmentScopedUrl('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(ManagedEnvironment $tenant, ?User $user, EnvironmentReview $review): array
{
$canContinue = $this->canOpenTenantCapability($tenant, $user, Capabilities::ENVIRONMENT_REVIEW_MANAGE);
return $this->actionPayload(
label: $this->overviewText('action_continue_review'),
url: $canContinue ? EnvironmentReviewResource::environmentScopedUrl('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(ManagedEnvironment $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], tenant: $tenant)
: EvidenceSnapshotResource::getUrl('index', 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(ManagedEnvironment $tenant, ?User $user, ?ReviewPack $reviewPack): array
{
$canOpenWorkspace = $user instanceof User
&& $user->canAccessTenant($tenant)
&& (
$user->can(Capabilities::ENVIRONMENT_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], tenant: $tenant)
: CustomerReviewWorkspace::environmentFilterUrl($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(ManagedEnvironment $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(ManagedEnvironment $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'),
);
}
private function operationsRequiringAttentionQuery(ManagedEnvironment $tenant): Builder
{
return OperationRun::query()
->where('managed_environment_id', (int) $tenant->getKey())
->where('workspace_id', (int) $tenant->workspace_id)
->dashboardNeedsFollowUp();
}
/**
* @return Collection<int, OperationRun>
*/
private function operationsRequiringAttentionRuns(ManagedEnvironment $tenant): Collection
{
return $this->operationsRequiringAttentionQuery($tenant)
->get()
->sort(fn (OperationRun $left, OperationRun $right): int => $this->compareActiveOperationSummaryRuns($left, $right))
->values();
}
/**
* @param Collection<int, OperationRun> $runs
* @return list<array<string, mixed>>
*/
private function attentionOperationItems(Collection $runs, ManagedEnvironment $tenant): array
{
return $runs
->take(3)
->map(function (OperationRun $run) use ($tenant): array {
$statusSpec = BadgeRenderer::spec(BadgeDomain::OperationRunStatus, [
'status' => (string) $run->status,
'freshness_state' => $run->freshnessState()->value,
]);
$outcomeSpec = BadgeRenderer::spec(BadgeDomain::OperationRunOutcome, [
'outcome' => (string) $run->outcome,
'status' => (string) $run->status,
'freshness_state' => $run->freshnessState()->value,
]);
return [
'id' => (int) $run->getKey(),
'identifier' => OperationRunLinks::identifier($run),
'type' => OperationCatalog::label((string) $run->type),
'title' => $this->attentionOperationTitle($run),
'icon' => $this->recentOperationIcon((string) $run->type),
'attentionLabel' => $this->attentionOperationBadgeLabel($run),
'problemClass' => $run->problemClass(),
'problemClassLabel' => OperationUxPresenter::problemClassLabel($run),
'statusLabel' => $statusSpec->label,
'statusTone' => $statusSpec->color,
'outcomeLabel' => $outcomeSpec->label,
'outcomeTone' => $outcomeSpec->color,
'outcomeSentence' => $this->attentionOperationOutcomeSentence($run),
'reason' => $this->attentionOperationReason($run),
'impact' => $this->attentionOperationImpact($run),
'timingLabel' => $this->attentionOperationTimingLabel($run),
'createdAt' => $run->completed_at?->diffForHumans() ?? $run->created_at?->diffForHumans(),
'primaryActionLabel' => $this->overviewText('action_review_operation'),
'primaryActionUrl' => OperationRunLinks::view($run, $tenant),
];
})
->values()
->all();
}
/**
* @param Collection<int, OperationRun> $runs
*/
private function dominantAttentionProblemClass(Collection $runs): string
{
return $runs->contains(fn (OperationRun $run): bool => $run->problemClass() === OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP)
? OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP
: OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION;
}
private function attentionOperationTitle(OperationRun $run): string
{
return OperationCatalog::label((string) $run->type);
}
private function attentionOperationBadgeLabel(OperationRun $run): string
{
return $run->problemClass() === OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION
? $this->overviewText('operations_attention_badge_stale')
: $this->overviewText('operations_attention_badge_follow_up');
}
private function attentionOperationOutcomeSentence(OperationRun $run): string
{
if ($run->problemClass() === OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION) {
return $this->overviewText('operations_attention_outcome_stale');
}
if ($this->isProviderConsentBlockedRun($run)) {
return $this->overviewText('operations_attention_outcome_provider_consent_required');
}
return match ((string) $run->outcome) {
OperationRunOutcome::Blocked->value => $this->overviewText('operations_attention_outcome_blocked'),
OperationRunOutcome::PartiallySucceeded->value => $this->overviewText('operations_attention_outcome_partial'),
OperationRunOutcome::Failed->value => $this->overviewText('operations_attention_outcome_failed'),
default => $this->overviewText('operations_attention_outcome_generic'),
};
}
private function attentionOperationReason(OperationRun $run): string
{
if ($this->isProviderConsentBlockedRun($run)) {
return $this->overviewText('operations_attention_reason_provider_consent_required');
}
$operatorExplanation = OperationUxPresenter::governanceOperatorExplanation($run);
$reason = trim((string) ($operatorExplanation?->dominantCauseExplanation ?? ''));
if ($reason !== '') {
return $reason;
}
$failureDetail = trim((string) (OperationUxPresenter::surfaceFailureDetail($run) ?? ''));
if ($failureDetail !== '') {
return $failureDetail;
}
return $run->problemClass() === OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION
? $this->overviewText('operations_attention_reason_stale')
: $this->overviewText('operations_attention_reason_fallback');
}
private function attentionOperationImpact(OperationRun $run): string
{
if ($this->isProviderConsentBlockedRun($run)) {
return $this->overviewText('operations_attention_impact_provider_consent_required');
}
return $run->problemClass() === OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION
? $this->overviewText('operations_attention_impact_stale')
: $this->overviewText('operations_attention_impact_follow_up');
}
private function attentionOperationTimingLabel(OperationRun $run): ?string
{
if ($run->completed_at instanceof Carbon) {
return $this->overviewText('operations_attention_timing_completed', [
'time' => $run->completed_at->diffForHumans(),
]);
}
$reference = $run->started_at ?? $run->created_at;
if (! $reference instanceof Carbon) {
return null;
}
return $this->overviewText('operations_attention_timing_started', [
'time' => $reference->diffForHumans(),
]);
}
private function isProviderConsentBlockedRun(OperationRun $run): bool
{
return OperationCatalog::canonicalCode((string) $run->type) === OperationCatalog::TYPE_PERMISSION_POSTURE_CHECK
&& (string) $run->outcome === OperationRunOutcome::Blocked->value;
}
/**
* @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string}
*/
private function baselineCompareAction(ManagedEnvironment $tenant, ?User $user, string $label): array
{
$canOpen = $this->canOpenTenantCapability($tenant, $user, Capabilities::TENANT_VIEW);
return $this->actionPayload(
label: $label,
url: $canOpen ? ManagedEnvironmentLinks::baselineCompareUrl($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(ManagedEnvironment $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], tenant: $tenant)
: BackupSetResource::getUrl('index', tenant: $tenant),
BackupHealthActionTarget::SURFACE_BACKUP_SETS_INDEX => BackupSetResource::getUrl('index', tenant: $tenant),
BackupHealthActionTarget::SURFACE_BACKUP_SCHEDULES_INDEX => BackupScheduleResource::getUrl('index', 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(ManagedEnvironment $tenant, ?User $user, string $capability): bool
{
return $user instanceof User
&& $user->canAccessTenant($tenant)
&& $user->can($capability, $tenant);
}
private function reviewSurfaceActionLabel(ManagedEnvironment $tenant, ?User $user, ?EnvironmentReview $review): string
{
if (! $review instanceof EnvironmentReview) {
return $this->overviewText('action_open_reviews');
}
return $this->canOpenTenantCapability($tenant, $user, Capabilities::ENVIRONMENT_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(?EnvironmentReview $review): string
{
if (! $review instanceof EnvironmentReview) {
return $this->overviewText('status_not_ready');
}
return BadgeRenderer::spec(BadgeDomain::EnvironmentReviewStatus, (string) $review->status)->label;
}
private function reviewTone(?EnvironmentReview $review): string
{
if (! $review instanceof EnvironmentReview) {
return 'warning';
}
return BadgeRenderer::spec(BadgeDomain::EnvironmentReviewStatus, (string) $review->status)->color;
}
private function reviewDescription(?EnvironmentReview $review): string
{
if (! $review instanceof EnvironmentReview) {
return $this->overviewText('review_unavailable_description');
}
$timestamp = $review->published_at ?? $review->generated_at;
return $this->overviewText('review_updated_prefix', ['time' => $timestamp?->diffForHumans()]);
}
/**
* @return list<array<string, int|string>>
*/
private function reviewProgress(?EnvironmentReview $review): array
{
if (! $review instanceof EnvironmentReview) {
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<string, mixed> $summary
* @return array<string, int|string>|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<string, mixed> $summary
* @return array<string, int|string>|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<string, mixed> $counts
* @param array<string, mixed> $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<string, mixed> $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<string, mixed> $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<string, mixed> $counts
* @param array<string, mixed> $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<array{label:string,value:string}>
*/
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<string, scalar|null> $replace
*/
private function overviewText(string $key, array $replace = []): string
{
return (string) __('localization.dashboard.overview.'.$key, $replace);
}
}