766 lines
30 KiB
PHP
766 lines
30 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\CustomerHealth;
|
|
|
|
use App\Models\Finding;
|
|
use App\Models\FindingException;
|
|
use App\Models\OperationRun;
|
|
use App\Models\PlatformUser;
|
|
use App\Models\ProductUsageEvent;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\Tenant;
|
|
use App\Models\TenantOnboardingSession;
|
|
use App\Models\Workspace;
|
|
use App\Support\Onboarding\OnboardingLifecycleState;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\Auth\PlatformCapabilities;
|
|
use App\Support\ProductTelemetry\ProductUsageEventCatalog;
|
|
use App\Support\Providers\ProviderConsentStatus;
|
|
use App\Support\Providers\ProviderVerificationStatus;
|
|
use App\Support\System\SystemDirectoryLinks;
|
|
use App\Support\System\SystemOperationRunLinks;
|
|
use App\Support\SystemConsole\StuckRunClassifier;
|
|
use App\Support\SystemConsole\SystemConsoleWindow;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Collection;
|
|
|
|
final class WorkspaceHealthSummaryQuery
|
|
{
|
|
public function __construct(
|
|
private readonly CustomerHealthDimensionCatalog $dimensionCatalog,
|
|
private readonly StuckRunClassifier $stuckRunClassifier,
|
|
) {}
|
|
|
|
/**
|
|
* @return Collection<int, array{
|
|
* workspace_id: int,
|
|
* workspace_name: string,
|
|
* overall_level: string,
|
|
* dimensions: array<string, array{label: string, level: string, windowed: bool}>,
|
|
* dominant_dimension_keys: list<string>,
|
|
* non_ok_dimension_count: int,
|
|
* next_link: array{label: string, url: string}
|
|
* }>
|
|
*/
|
|
public function summaries(SystemConsoleWindow|string|null $window = null, ?CarbonImmutable $now = null): Collection
|
|
{
|
|
$resolvedWindow = $this->resolveWindow($window);
|
|
$now ??= CarbonImmutable::now();
|
|
$startAt = $resolvedWindow->startAt($now);
|
|
|
|
$workspaces = Workspace::query()
|
|
->whereNull('archived_at')
|
|
->orderBy('name')
|
|
->orderBy('id')
|
|
->get(['id', 'name']);
|
|
|
|
if ($workspaces->isEmpty()) {
|
|
return collect();
|
|
}
|
|
|
|
$workspaceIds = $workspaces
|
|
->pluck('id')
|
|
->map(static fn (mixed $workspaceId): int => (int) $workspaceId)
|
|
->all();
|
|
|
|
$activeTenants = Tenant::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->whereNull('deleted_at')
|
|
->where('status', '!=', Tenant::STATUS_ARCHIVED)
|
|
->orderBy('name')
|
|
->orderBy('id')
|
|
->get(['id', 'workspace_id', 'external_id', 'name', 'status']);
|
|
|
|
$tenantsByWorkspace = $activeTenants->groupBy(static fn (Tenant $tenant): int => (int) $tenant->workspace_id);
|
|
|
|
$latestOnboardingSessions = TenantOnboardingSession::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->where(function (Builder $query): void {
|
|
$this->constrainToActiveTenantTruth($query);
|
|
})
|
|
->orderByDesc('updated_at')
|
|
->orderByDesc('id')
|
|
->get(['id', 'workspace_id', 'tenant_id', 'lifecycle_state', 'updated_at', 'created_at'])
|
|
->groupBy(static fn (TenantOnboardingSession $session): int => (int) $session->workspace_id)
|
|
->map(static fn (Collection $sessions): ?TenantOnboardingSession => $sessions->first());
|
|
|
|
$providerConnectionsByWorkspace = ProviderConnection::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->where('is_default', true)
|
|
->where(function (Builder $query): void {
|
|
$this->constrainToActiveTenantTruth($query);
|
|
})
|
|
->orderByDesc('is_enabled')
|
|
->orderBy('id')
|
|
->get([
|
|
'id',
|
|
'workspace_id',
|
|
'tenant_id',
|
|
'is_enabled',
|
|
'consent_status',
|
|
'verification_status',
|
|
])
|
|
->groupBy(static fn (ProviderConnection $connection): int => (int) $connection->workspace_id);
|
|
|
|
$recentRunCounts = $this->groupedCounts(
|
|
OperationRun::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->where('created_at', '>=', $startAt)
|
|
->where(function (Builder $query): void {
|
|
$this->constrainToActiveTenantTruth($query);
|
|
})
|
|
);
|
|
|
|
$recentFailedRunCounts = $this->groupedCounts(
|
|
OperationRun::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->where('created_at', '>=', $startAt)
|
|
->where('status', OperationRunStatus::Completed->value)
|
|
->where('outcome', OperationRunOutcome::Failed->value)
|
|
->where(function (Builder $query): void {
|
|
$this->constrainToActiveTenantTruth($query);
|
|
})
|
|
);
|
|
|
|
$recentStuckRunCounts = $this->groupedCounts(
|
|
$this->stuckRunClassifier->apply(
|
|
OperationRun::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->where('created_at', '>=', $startAt)
|
|
->where(function (Builder $query): void {
|
|
$this->constrainToActiveTenantTruth($query);
|
|
}),
|
|
$now,
|
|
)
|
|
);
|
|
|
|
$activeHighSeverityFindingCounts = $this->groupedCounts(
|
|
Finding::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->whereIn('severity', Finding::highSeverityValues())
|
|
->whereIn('status', Finding::openStatusesForQuery())
|
|
->where(function (Builder $query): void {
|
|
$this->constrainToActiveTenantTruth($query);
|
|
})
|
|
);
|
|
|
|
$anyGovernanceFindingCounts = $this->groupedCounts(
|
|
Finding::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->where(function (Builder $query): void {
|
|
$this->constrainToActiveTenantTruth($query);
|
|
})
|
|
);
|
|
|
|
$overdueHighSeverityFindingCounts = $this->groupedCounts(
|
|
Finding::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->whereIn('severity', Finding::highSeverityValues())
|
|
->whereIn('status', Finding::openStatusesForQuery())
|
|
->whereNotNull('due_at')
|
|
->where('due_at', '<', $now)
|
|
->where(function (Builder $query): void {
|
|
$this->constrainToActiveTenantTruth($query);
|
|
})
|
|
);
|
|
|
|
$warningExceptionCounts = $this->groupedCounts(
|
|
FindingException::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->where(function (Builder $query): void {
|
|
$query
|
|
->whereIn('status', [
|
|
FindingException::STATUS_PENDING,
|
|
FindingException::STATUS_EXPIRING,
|
|
])
|
|
->orWhere('current_validity_state', FindingException::VALIDITY_EXPIRING);
|
|
})
|
|
->where(function (Builder $query): void {
|
|
$this->constrainToActiveTenantTruth($query);
|
|
})
|
|
);
|
|
|
|
$criticalExceptionCounts = $this->groupedCounts(
|
|
FindingException::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->where(function (Builder $query): void {
|
|
$query
|
|
->whereIn('status', [
|
|
FindingException::STATUS_EXPIRED,
|
|
FindingException::STATUS_REVOKED,
|
|
])
|
|
->orWhereIn('current_validity_state', [
|
|
FindingException::VALIDITY_EXPIRED,
|
|
FindingException::VALIDITY_REVOKED,
|
|
FindingException::VALIDITY_REJECTED,
|
|
FindingException::VALIDITY_MISSING_SUPPORT,
|
|
]);
|
|
})
|
|
->where(function (Builder $query): void {
|
|
$this->constrainToActiveTenantTruth($query);
|
|
})
|
|
);
|
|
|
|
$reviewPackRequestCounts = $this->groupedCounts(
|
|
ProductUsageEvent::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->where('event_name', ProductUsageEventCatalog::REVIEW_PACK_REQUESTED)
|
|
->where('occurred_at', '>=', $startAt)
|
|
->where(function (Builder $query): void {
|
|
$this->constrainToActiveTenantTruth($query);
|
|
})
|
|
);
|
|
|
|
$recentReviewPacks = ReviewPack::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->where('created_at', '>=', $startAt)
|
|
->where(function (Builder $query): void {
|
|
$this->constrainToActiveTenantTruth($query);
|
|
})
|
|
->orderByDesc('created_at')
|
|
->orderByDesc('id')
|
|
->get(['id', 'workspace_id', 'tenant_id', 'status', 'expires_at', 'created_at'])
|
|
->groupBy(static fn (ReviewPack $reviewPack): int => (int) $reviewPack->workspace_id)
|
|
->map(static fn (Collection $reviewPacks): ?ReviewPack => $reviewPacks->first());
|
|
|
|
$recentUsageEventCounts = $this->groupedCounts(
|
|
ProductUsageEvent::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->where('occurred_at', '>=', $startAt)
|
|
->where(function (Builder $query): void {
|
|
$this->constrainToActiveTenantTruth($query);
|
|
})
|
|
);
|
|
|
|
$historicalUsageEventCounts = $this->groupedCounts(
|
|
ProductUsageEvent::query()
|
|
->whereIn('workspace_id', $workspaceIds)
|
|
->where(function (Builder $query): void {
|
|
$this->constrainToActiveTenantTruth($query);
|
|
})
|
|
);
|
|
|
|
return $workspaces
|
|
->map(function (Workspace $workspace) use (
|
|
$tenantsByWorkspace,
|
|
$latestOnboardingSessions,
|
|
$providerConnectionsByWorkspace,
|
|
$recentRunCounts,
|
|
$recentFailedRunCounts,
|
|
$recentStuckRunCounts,
|
|
$activeHighSeverityFindingCounts,
|
|
$anyGovernanceFindingCounts,
|
|
$overdueHighSeverityFindingCounts,
|
|
$warningExceptionCounts,
|
|
$criticalExceptionCounts,
|
|
$reviewPackRequestCounts,
|
|
$recentReviewPacks,
|
|
$recentUsageEventCounts,
|
|
$historicalUsageEventCounts,
|
|
$resolvedWindow,
|
|
$now,
|
|
): array {
|
|
$workspaceId = (int) $workspace->getKey();
|
|
/** @var Collection<int, Tenant> $workspaceTenants */
|
|
$workspaceTenants = $tenantsByWorkspace->get($workspaceId, collect());
|
|
/** @var TenantOnboardingSession|null $latestOnboardingSession */
|
|
$latestOnboardingSession = $latestOnboardingSessions->get($workspaceId);
|
|
/** @var Collection<int, ProviderConnection> $providerConnections */
|
|
$providerConnections = $providerConnectionsByWorkspace->get($workspaceId, collect());
|
|
/** @var ReviewPack|null $latestReviewPack */
|
|
$latestReviewPack = $recentReviewPacks->get($workspaceId);
|
|
|
|
$dimensions = $this->buildDimensions(
|
|
tenants: $workspaceTenants,
|
|
latestOnboardingSession: $latestOnboardingSession,
|
|
providerConnections: $providerConnections,
|
|
recentRunCount: $this->countForWorkspace($recentRunCounts, $workspaceId),
|
|
recentFailedRunCount: $this->countForWorkspace($recentFailedRunCounts, $workspaceId),
|
|
recentStuckRunCount: $this->countForWorkspace($recentStuckRunCounts, $workspaceId),
|
|
activeHighSeverityFindingCount: $this->countForWorkspace($activeHighSeverityFindingCounts, $workspaceId),
|
|
anyGovernanceFindingCount: $this->countForWorkspace($anyGovernanceFindingCounts, $workspaceId),
|
|
overdueHighSeverityFindingCount: $this->countForWorkspace($overdueHighSeverityFindingCounts, $workspaceId),
|
|
warningExceptionCount: $this->countForWorkspace($warningExceptionCounts, $workspaceId),
|
|
criticalExceptionCount: $this->countForWorkspace($criticalExceptionCounts, $workspaceId),
|
|
reviewPackRequestCount: $this->countForWorkspace($reviewPackRequestCounts, $workspaceId),
|
|
latestReviewPack: $latestReviewPack,
|
|
recentUsageEventCount: $this->countForWorkspace($recentUsageEventCounts, $workspaceId),
|
|
historicalUsageEventCount: $this->countForWorkspace($historicalUsageEventCounts, $workspaceId),
|
|
now: $now,
|
|
);
|
|
|
|
$overallLevel = $this->dimensionCatalog->resolveOverallLevel(
|
|
array_map(static fn (array $dimension): string => $dimension['level'], $dimensions),
|
|
);
|
|
|
|
$dominantDimensionKeys = $this->dominantDimensionKeys($dimensions);
|
|
|
|
return [
|
|
'workspace_id' => $workspaceId,
|
|
'workspace_name' => (string) $workspace->name,
|
|
'overall_level' => $overallLevel,
|
|
'dimensions' => $dimensions,
|
|
'dominant_dimension_keys' => $dominantDimensionKeys,
|
|
'non_ok_dimension_count' => count(array_filter(
|
|
$dimensions,
|
|
static fn (array $dimension): bool => $dimension['level'] !== 'ok',
|
|
)),
|
|
'next_link' => $this->nextLink(
|
|
workspace: $workspace,
|
|
tenants: $workspaceTenants,
|
|
dominantDimensionKeys: $dominantDimensionKeys,
|
|
window: $resolvedWindow,
|
|
),
|
|
];
|
|
})
|
|
->values();
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* workspace_id: int,
|
|
* workspace_name: string,
|
|
* overall_level: string,
|
|
* dimensions: array<string, array{label: string, level: string, windowed: bool}>,
|
|
* dominant_dimension_keys: list<string>,
|
|
* non_ok_dimension_count: int,
|
|
* next_link: array{label: string, url: string}
|
|
* }|null
|
|
*/
|
|
public function summaryForWorkspace(Workspace|int $workspace, SystemConsoleWindow|string|null $window = null, ?CarbonImmutable $now = null): ?array
|
|
{
|
|
$workspaceId = $workspace instanceof Workspace ? (int) $workspace->getKey() : (int) $workspace;
|
|
|
|
/** @var array{
|
|
* workspace_id: int,
|
|
* workspace_name: string,
|
|
* overall_level: string,
|
|
* dimensions: array<string, array{label: string, level: string, windowed: bool}>,
|
|
* dominant_dimension_keys: list<string>,
|
|
* non_ok_dimension_count: int,
|
|
* next_link: array{label: string, url: string}
|
|
* }|null $summary
|
|
*/
|
|
$summary = $this->summaries($window, $now)->firstWhere('workspace_id', $workspaceId);
|
|
|
|
return $summary;
|
|
}
|
|
|
|
/**
|
|
* @return array{ok: int, warn: int, critical: int, unknown: int}
|
|
*/
|
|
public function healthCounts(SystemConsoleWindow|string|null $window = null, ?CarbonImmutable $now = null): array
|
|
{
|
|
$counts = [
|
|
'ok' => 0,
|
|
'warn' => 0,
|
|
'critical' => 0,
|
|
'unknown' => 0,
|
|
];
|
|
|
|
foreach ($this->summaries($window, $now) as $summary) {
|
|
$counts[$summary['overall_level']]++;
|
|
}
|
|
|
|
return $counts;
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, array{
|
|
* workspace_id: int,
|
|
* workspace_name: string,
|
|
* overall_level: string,
|
|
* dimensions: array<string, array{label: string, level: string, windowed: bool}>,
|
|
* dominant_dimension_keys: list<string>,
|
|
* non_ok_dimension_count: int,
|
|
* next_link: array{label: string, url: string}
|
|
* }>
|
|
*/
|
|
public function attentionNeeded(SystemConsoleWindow|string|null $window = null, int $limit = 10, ?CarbonImmutable $now = null): Collection
|
|
{
|
|
return $this->summaries($window, $now)
|
|
->filter(static fn (array $summary): bool => $summary['overall_level'] !== 'ok')
|
|
->sort(function (array $left, array $right): int {
|
|
$severityComparison = $this->levelRank($right['overall_level']) <=> $this->levelRank($left['overall_level']);
|
|
|
|
if ($severityComparison !== 0) {
|
|
return $severityComparison;
|
|
}
|
|
|
|
$nonOkComparison = $right['non_ok_dimension_count'] <=> $left['non_ok_dimension_count'];
|
|
|
|
if ($nonOkComparison !== 0) {
|
|
return $nonOkComparison;
|
|
}
|
|
|
|
$nameComparison = strcasecmp($left['workspace_name'], $right['workspace_name']);
|
|
|
|
if ($nameComparison !== 0) {
|
|
return $nameComparison;
|
|
}
|
|
|
|
return $left['workspace_id'] <=> $right['workspace_id'];
|
|
})
|
|
->values()
|
|
->take(max(1, $limit));
|
|
}
|
|
|
|
private function resolveWindow(SystemConsoleWindow|string|null $window): SystemConsoleWindow
|
|
{
|
|
if ($window instanceof SystemConsoleWindow) {
|
|
return $window;
|
|
}
|
|
|
|
return SystemConsoleWindow::fromNullable(is_string($window) ? $window : null);
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, Tenant> $tenants
|
|
* @param Collection<int, ProviderConnection> $providerConnections
|
|
* @return array<string, array{label: string, level: string, windowed: bool}>
|
|
*/
|
|
private function buildDimensions(
|
|
Collection $tenants,
|
|
?TenantOnboardingSession $latestOnboardingSession,
|
|
Collection $providerConnections,
|
|
int $recentRunCount,
|
|
int $recentFailedRunCount,
|
|
int $recentStuckRunCount,
|
|
int $activeHighSeverityFindingCount,
|
|
int $anyGovernanceFindingCount,
|
|
int $overdueHighSeverityFindingCount,
|
|
int $warningExceptionCount,
|
|
int $criticalExceptionCount,
|
|
int $reviewPackRequestCount,
|
|
?ReviewPack $latestReviewPack,
|
|
int $recentUsageEventCount,
|
|
int $historicalUsageEventCount,
|
|
CarbonImmutable $now,
|
|
): array {
|
|
$levels = [
|
|
CustomerHealthDimensionCatalog::ONBOARDING_READINESS => $this->onboardingReadinessLevel($tenants, $latestOnboardingSession),
|
|
CustomerHealthDimensionCatalog::PROVIDER_CONNECTION_HEALTH => $this->providerConnectionHealthLevel($providerConnections),
|
|
CustomerHealthDimensionCatalog::OPERATIONAL_STABILITY => $this->operationalStabilityLevel($recentRunCount, $recentFailedRunCount, $recentStuckRunCount),
|
|
CustomerHealthDimensionCatalog::GOVERNANCE_PRESSURE => $this->governancePressureLevel(
|
|
activeHighSeverityFindingCount: $activeHighSeverityFindingCount,
|
|
anyGovernanceFindingCount: $anyGovernanceFindingCount,
|
|
overdueHighSeverityFindingCount: $overdueHighSeverityFindingCount,
|
|
warningExceptionCount: $warningExceptionCount,
|
|
criticalExceptionCount: $criticalExceptionCount,
|
|
),
|
|
CustomerHealthDimensionCatalog::REVIEW_PACK_READINESS => $this->reviewPackReadinessLevel($reviewPackRequestCount, $latestReviewPack, $now),
|
|
CustomerHealthDimensionCatalog::ENGAGEMENT_FRESHNESS => $this->engagementFreshnessLevel($recentUsageEventCount, $historicalUsageEventCount),
|
|
];
|
|
|
|
$dimensions = [];
|
|
|
|
foreach ($this->dimensionCatalog->visibleDimensions() as $dimensionKey => $label) {
|
|
$dimensions[$dimensionKey] = [
|
|
'label' => $label,
|
|
'level' => $levels[$dimensionKey],
|
|
'windowed' => $this->dimensionCatalog->isWindowed($dimensionKey),
|
|
];
|
|
}
|
|
|
|
return $dimensions;
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, Tenant> $tenants
|
|
*/
|
|
private function onboardingReadinessLevel(Collection $tenants, ?TenantOnboardingSession $latestOnboardingSession): string
|
|
{
|
|
if ($latestOnboardingSession instanceof TenantOnboardingSession) {
|
|
return match ($latestOnboardingSession->lifecycleState()) {
|
|
OnboardingLifecycleState::ReadyForActivation,
|
|
OnboardingLifecycleState::Completed => 'ok',
|
|
OnboardingLifecycleState::Cancelled => 'critical',
|
|
OnboardingLifecycleState::Draft,
|
|
OnboardingLifecycleState::Verifying,
|
|
OnboardingLifecycleState::ActionRequired,
|
|
OnboardingLifecycleState::Bootstrapping => 'warn',
|
|
};
|
|
}
|
|
|
|
if ($tenants->isEmpty()) {
|
|
return 'unknown';
|
|
}
|
|
|
|
if ($tenants->contains(static fn (Tenant $tenant): bool => (string) $tenant->status === Tenant::STATUS_ACTIVE)) {
|
|
return 'ok';
|
|
}
|
|
|
|
return 'warn';
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, ProviderConnection> $providerConnections
|
|
*/
|
|
private function providerConnectionHealthLevel(Collection $providerConnections): string
|
|
{
|
|
if ($providerConnections->isEmpty()) {
|
|
return 'unknown';
|
|
}
|
|
|
|
if ($providerConnections->contains(function (ProviderConnection $connection): bool {
|
|
if (! $connection->is_enabled) {
|
|
return false;
|
|
}
|
|
|
|
$consentStatus = $this->normalizeBackedEnumValue($connection->consent_status);
|
|
$verificationStatus = $this->normalizeBackedEnumValue($connection->verification_status);
|
|
|
|
return in_array($consentStatus, [
|
|
ProviderConsentStatus::Required->value,
|
|
ProviderConsentStatus::Failed->value,
|
|
ProviderConsentStatus::Revoked->value,
|
|
], true) || in_array($verificationStatus, [
|
|
ProviderVerificationStatus::Blocked->value,
|
|
ProviderVerificationStatus::Error->value,
|
|
], true);
|
|
})) {
|
|
return 'critical';
|
|
}
|
|
|
|
if ($providerConnections->contains(function (ProviderConnection $connection): bool {
|
|
if (! $connection->is_enabled) {
|
|
return true;
|
|
}
|
|
|
|
$consentStatus = $this->normalizeBackedEnumValue($connection->consent_status);
|
|
$verificationStatus = $this->normalizeBackedEnumValue($connection->verification_status);
|
|
|
|
return in_array($consentStatus, [
|
|
ProviderConsentStatus::Unknown->value,
|
|
], true) || in_array($verificationStatus, [
|
|
ProviderVerificationStatus::Unknown->value,
|
|
ProviderVerificationStatus::Pending->value,
|
|
ProviderVerificationStatus::Degraded->value,
|
|
], true);
|
|
})) {
|
|
return 'warn';
|
|
}
|
|
|
|
if ($providerConnections->contains(function (ProviderConnection $connection): bool {
|
|
return $connection->is_enabled
|
|
&& $this->normalizeBackedEnumValue($connection->consent_status) === ProviderConsentStatus::Granted->value
|
|
&& $this->normalizeBackedEnumValue($connection->verification_status) === ProviderVerificationStatus::Healthy->value;
|
|
})) {
|
|
return 'ok';
|
|
}
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
private function operationalStabilityLevel(int $recentRunCount, int $recentFailedRunCount, int $recentStuckRunCount): string
|
|
{
|
|
if ($recentFailedRunCount > 0 || $recentStuckRunCount > 0) {
|
|
return 'critical';
|
|
}
|
|
|
|
if ($recentRunCount > 0) {
|
|
return 'ok';
|
|
}
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
private function governancePressureLevel(
|
|
int $activeHighSeverityFindingCount,
|
|
int $anyGovernanceFindingCount,
|
|
int $overdueHighSeverityFindingCount,
|
|
int $warningExceptionCount,
|
|
int $criticalExceptionCount,
|
|
): string {
|
|
if ($overdueHighSeverityFindingCount > 0 || $criticalExceptionCount > 0) {
|
|
return 'critical';
|
|
}
|
|
|
|
if ($activeHighSeverityFindingCount > 0 || $warningExceptionCount > 0) {
|
|
return 'warn';
|
|
}
|
|
|
|
if ($anyGovernanceFindingCount === 0 && $warningExceptionCount === 0 && $criticalExceptionCount === 0) {
|
|
return 'unknown';
|
|
}
|
|
|
|
return 'ok';
|
|
}
|
|
|
|
private function reviewPackReadinessLevel(int $reviewPackRequestCount, ?ReviewPack $latestReviewPack, CarbonImmutable $now): string
|
|
{
|
|
if ($reviewPackRequestCount === 0 && ! $latestReviewPack instanceof ReviewPack) {
|
|
return 'unknown';
|
|
}
|
|
|
|
if (! $latestReviewPack instanceof ReviewPack) {
|
|
return 'warn';
|
|
}
|
|
|
|
if (
|
|
(string) $latestReviewPack->status === ReviewPack::STATUS_READY
|
|
&& (! $latestReviewPack->expires_at instanceof CarbonImmutable || $latestReviewPack->expires_at->gt($now))
|
|
) {
|
|
return 'ok';
|
|
}
|
|
|
|
if (
|
|
in_array((string) $latestReviewPack->status, [
|
|
ReviewPack::STATUS_FAILED,
|
|
ReviewPack::STATUS_EXPIRED,
|
|
], true)
|
|
|| ($latestReviewPack->expires_at !== null && $latestReviewPack->expires_at->lte($now))
|
|
) {
|
|
return 'critical';
|
|
}
|
|
|
|
return 'warn';
|
|
}
|
|
|
|
private function engagementFreshnessLevel(int $recentUsageEventCount, int $historicalUsageEventCount): string
|
|
{
|
|
if ($recentUsageEventCount > 0) {
|
|
return 'ok';
|
|
}
|
|
|
|
if ($historicalUsageEventCount > 0) {
|
|
return 'warn';
|
|
}
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array{label: string, level: string, windowed: bool}> $dimensions
|
|
* @return list<string>
|
|
*/
|
|
private function dominantDimensionKeys(array $dimensions): array
|
|
{
|
|
$catalogOrder = array_flip($this->dimensionCatalog->names());
|
|
|
|
return collect($dimensions)
|
|
->reject(static fn (array $dimension): bool => $dimension['level'] === 'ok')
|
|
->keys()
|
|
->sort(function (string $left, string $right) use ($dimensions, $catalogOrder): int {
|
|
$severityComparison = $this->levelRank($dimensions[$right]['level']) <=> $this->levelRank($dimensions[$left]['level']);
|
|
|
|
if ($severityComparison !== 0) {
|
|
return $severityComparison;
|
|
}
|
|
|
|
return ($catalogOrder[$left] ?? PHP_INT_MAX) <=> ($catalogOrder[$right] ?? PHP_INT_MAX);
|
|
})
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, Tenant> $tenants
|
|
* @param list<string> $dominantDimensionKeys
|
|
* @return array{label: string, url: string}
|
|
*/
|
|
private function nextLink(Workspace $workspace, Collection $tenants, array $dominantDimensionKeys, SystemConsoleWindow $window): array
|
|
{
|
|
$dominantDimension = $dominantDimensionKeys[0] ?? null;
|
|
|
|
if ($dominantDimension === CustomerHealthDimensionCatalog::OPERATIONAL_STABILITY && $this->canOpenRunsLink()) {
|
|
return [
|
|
'label' => 'Open runs',
|
|
'url' => SystemOperationRunLinks::index(),
|
|
];
|
|
}
|
|
|
|
/** @var Tenant|null $tenant */
|
|
$tenant = $tenants->first();
|
|
|
|
if ($tenant instanceof Tenant) {
|
|
return [
|
|
'label' => 'Review health details',
|
|
'url' => $this->withWindowQuery(SystemDirectoryLinks::tenantDetail($tenant), $window),
|
|
];
|
|
}
|
|
|
|
return [
|
|
'label' => 'Review health details',
|
|
'url' => $this->withWindowQuery(SystemDirectoryLinks::workspaceDetail($workspace), $window),
|
|
];
|
|
}
|
|
|
|
private function withWindowQuery(string $url, SystemConsoleWindow $window): string
|
|
{
|
|
$separator = str_contains($url, '?') ? '&' : '?';
|
|
|
|
return $url.$separator.http_build_query(['window' => $window->value]);
|
|
}
|
|
|
|
private function canOpenRunsLink(): bool
|
|
{
|
|
$user = auth('platform')->user();
|
|
|
|
if (! $user instanceof PlatformUser) {
|
|
return false;
|
|
}
|
|
|
|
return $user->hasCapability(PlatformCapabilities::OPERATIONS_VIEW)
|
|
|| ($user->hasCapability(PlatformCapabilities::OPS_VIEW) && $user->hasCapability(PlatformCapabilities::RUNBOOKS_VIEW));
|
|
}
|
|
|
|
/**
|
|
* @param Builder<OperationRun>|Builder<ProductUsageEvent>|Builder<ReviewPack>|Builder<Finding>|Builder<FindingException> $query
|
|
* @return array<int, int>
|
|
*/
|
|
private function groupedCounts(Builder $query): array
|
|
{
|
|
return $query
|
|
->selectRaw('workspace_id, COUNT(*) as aggregate')
|
|
->groupBy('workspace_id')
|
|
->pluck('aggregate', 'workspace_id')
|
|
->mapWithKeys(static fn (mixed $count, mixed $workspaceId): array => [
|
|
(int) $workspaceId => (int) $count,
|
|
])
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param array<int, int> $counts
|
|
*/
|
|
private function countForWorkspace(array $counts, int $workspaceId): int
|
|
{
|
|
return (int) ($counts[$workspaceId] ?? 0);
|
|
}
|
|
|
|
private function levelRank(string $level): int
|
|
{
|
|
return match ($level) {
|
|
'critical' => 4,
|
|
'warn' => 3,
|
|
'unknown' => 2,
|
|
default => 1,
|
|
};
|
|
}
|
|
|
|
private function normalizeBackedEnumValue(mixed $value): string
|
|
{
|
|
if (is_object($value) && property_exists($value, 'value')) {
|
|
return (string) $value->value;
|
|
}
|
|
|
|
return (string) $value;
|
|
}
|
|
|
|
private function constrainToActiveTenantTruth(Builder $query): void
|
|
{
|
|
$query
|
|
->whereNull('tenant_id')
|
|
->orWhereHas('tenant', function (Builder $tenantQuery): void {
|
|
$tenantQuery
|
|
->whereNull('deleted_at')
|
|
->where('status', '!=', Tenant::STATUS_ARCHIVED);
|
|
});
|
|
}
|
|
} |