$severityCounts */ private function __construct( public readonly string $state, public readonly ?string $message, public readonly ?string $profileName, public readonly ?int $profileId, public readonly ?int $snapshotId, public readonly ?int $operationRunId, public readonly ?int $findingsCount, public readonly array $severityCounts, public readonly ?string $lastComparedHuman, public readonly ?string $lastComparedIso, public readonly ?string $failureReason, ) {} public static function forTenant(?Tenant $tenant): self { if (! $tenant instanceof Tenant) { return self::empty('no_tenant', 'No tenant selected.'); } $assignment = BaselineTenantAssignment::query() ->where('tenant_id', $tenant->getKey()) ->first(); if (! $assignment instanceof BaselineTenantAssignment) { return self::empty( 'no_assignment', 'This tenant has no baseline assignment. A workspace manager can assign a baseline profile to this tenant.', ); } $profile = $assignment->baselineProfile; if (! $profile instanceof BaselineProfile) { return self::empty( 'no_assignment', 'The assigned baseline profile no longer exists.', ); } $profileName = (string) $profile->name; $profileId = (int) $profile->getKey(); $snapshotId = $profile->active_snapshot_id !== null ? (int) $profile->active_snapshot_id : null; if ($snapshotId === null) { return self::empty( 'no_snapshot', 'The baseline profile has no active snapshot yet. A workspace manager needs to capture a snapshot first.', profileName: $profileName, profileId: $profileId, ); } $latestRun = OperationRun::query() ->where('tenant_id', $tenant->getKey()) ->where('type', 'baseline_compare') ->latest('id') ->first(); // Active run (queued/running) if ($latestRun instanceof OperationRun && in_array($latestRun->status, ['queued', 'running'], true)) { return new self( state: 'comparing', message: 'A baseline comparison is currently in progress.', profileName: $profileName, profileId: $profileId, snapshotId: $snapshotId, operationRunId: (int) $latestRun->getKey(), findingsCount: null, severityCounts: [], lastComparedHuman: null, lastComparedIso: null, failureReason: null, ); } // Failed run — explicit error state if ($latestRun instanceof OperationRun && $latestRun->outcome === 'failed') { $failureSummary = is_array($latestRun->failure_summary) ? $latestRun->failure_summary : []; $failureReason = $failureSummary['message'] ?? $failureSummary['reason'] ?? 'The comparison job failed. Check the run details for more information.'; return new self( state: 'failed', message: (string) $failureReason, profileName: $profileName, profileId: $profileId, snapshotId: $snapshotId, operationRunId: (int) $latestRun->getKey(), findingsCount: null, severityCounts: [], lastComparedHuman: $latestRun->finished_at?->diffForHumans(), lastComparedIso: $latestRun->finished_at?->toIso8601String(), failureReason: (string) $failureReason, ); } $lastComparedHuman = null; $lastComparedIso = null; if ($latestRun instanceof OperationRun && $latestRun->finished_at !== null) { $lastComparedHuman = $latestRun->finished_at->diffForHumans(); $lastComparedIso = $latestRun->finished_at->toIso8601String(); } $scopeKey = 'baseline_profile:'.$profile->getKey(); // Single grouped query instead of 4 separate COUNT queries $severityRows = Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_DRIFT) ->where('source', 'baseline.compare') ->where('scope_key', $scopeKey) ->whereIn('status', Finding::openStatusesForQuery()) ->selectRaw('severity, count(*) as cnt') ->groupBy('severity') ->pluck('cnt', 'severity'); $totalFindings = (int) $severityRows->sum(); $severityCounts = [ 'high' => (int) ($severityRows[Finding::SEVERITY_HIGH] ?? 0), 'medium' => (int) ($severityRows[Finding::SEVERITY_MEDIUM] ?? 0), 'low' => (int) ($severityRows[Finding::SEVERITY_LOW] ?? 0), ]; if ($totalFindings > 0) { return new self( state: 'ready', message: null, profileName: $profileName, profileId: $profileId, snapshotId: $snapshotId, operationRunId: $latestRun instanceof OperationRun ? (int) $latestRun->getKey() : null, findingsCount: $totalFindings, severityCounts: $severityCounts, lastComparedHuman: $lastComparedHuman, lastComparedIso: $lastComparedIso, failureReason: null, ); } if ($latestRun instanceof OperationRun && $latestRun->status === 'completed' && $latestRun->outcome === 'succeeded') { return new self( state: 'ready', message: 'No open drift findings for this baseline comparison. The tenant matches the baseline.', profileName: $profileName, profileId: $profileId, snapshotId: $snapshotId, operationRunId: (int) $latestRun->getKey(), findingsCount: 0, severityCounts: $severityCounts, lastComparedHuman: $lastComparedHuman, lastComparedIso: $lastComparedIso, failureReason: null, ); } return new self( state: 'idle', message: 'Baseline profile is assigned and has a snapshot. Run "Compare Now" to check for drift.', profileName: $profileName, profileId: $profileId, snapshotId: $snapshotId, operationRunId: null, findingsCount: null, severityCounts: $severityCounts, lastComparedHuman: $lastComparedHuman, lastComparedIso: $lastComparedIso, failureReason: null, ); } /** * Create a DTO for widget consumption (only open/new findings). */ public static function forWidget(?Tenant $tenant): self { if (! $tenant instanceof Tenant) { return self::empty('no_tenant', null); } $assignment = BaselineTenantAssignment::query() ->where('tenant_id', $tenant->getKey()) ->with('baselineProfile') ->first(); if (! $assignment instanceof BaselineTenantAssignment || $assignment->baselineProfile === null) { return self::empty('no_assignment', null); } $profile = $assignment->baselineProfile; $scopeKey = 'baseline_profile:'.$profile->getKey(); $severityRows = Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('finding_type', Finding::FINDING_TYPE_DRIFT) ->where('source', 'baseline.compare') ->where('scope_key', $scopeKey) ->where('status', Finding::STATUS_NEW) ->selectRaw('severity, count(*) as cnt') ->groupBy('severity') ->pluck('cnt', 'severity'); $totalFindings = (int) $severityRows->sum(); $latestRun = OperationRun::query() ->where('tenant_id', $tenant->getKey()) ->where('type', 'baseline_compare') ->where('context->baseline_profile_id', (string) $profile->getKey()) ->whereNotNull('completed_at') ->latest('completed_at') ->first(); return new self( state: $totalFindings > 0 ? 'ready' : 'idle', message: null, profileName: (string) $profile->name, profileId: (int) $profile->getKey(), snapshotId: $profile->active_snapshot_id !== null ? (int) $profile->active_snapshot_id : null, operationRunId: $latestRun instanceof OperationRun ? (int) $latestRun->getKey() : null, findingsCount: $totalFindings, severityCounts: [ 'high' => (int) ($severityRows[Finding::SEVERITY_HIGH] ?? 0), 'medium' => (int) ($severityRows[Finding::SEVERITY_MEDIUM] ?? 0), 'low' => (int) ($severityRows[Finding::SEVERITY_LOW] ?? 0), ], lastComparedHuman: $latestRun?->finished_at?->diffForHumans(), lastComparedIso: $latestRun?->finished_at?->toIso8601String(), failureReason: null, ); } private static function empty( string $state, ?string $message, ?string $profileName = null, ?int $profileId = null, ): self { return new self( state: $state, message: $message, profileName: $profileName, profileId: $profileId, snapshotId: null, operationRunId: null, findingsCount: null, severityCounts: [], lastComparedHuman: null, lastComparedIso: null, failureReason: null, ); } }