TenantAtlas/apps/platform/app/Filament/Pages/BaselineCompareLanding.php
ahmido 4c661f18f0 feat: align baseline compare product process flow (#406)
## Summary
- align the Baseline Compare landing page with the shared Product Process Flow contract introduced by Spec 332
- add the horizontal flow rendering primitive and update the landing view/state presentation for readiness, proof, evidence, and next action
- add Spec 336 artifacts, screenshots, focused feature coverage, and browser smoke coverage for the aligned states

## Testing
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/BaselineCompareEnvironmentRouteContractTest.php tests/Feature/Filament/Spec330EnvironmentDashboardBaselineCompareProductizationTest.php tests/Feature/Filament/Spec336BaselineCompareProductProcessFlowAlignmentTest.php tests/Browser/Spec330EnvironmentDashboardBaselineCompareSmokeTest.php tests/Browser/Spec336BaselineCompareProductProcessFlowAlignmentSmokeTest.php`

## Notes
- Filament v5 / Livewire v4 stack remains unchanged
- no panel provider registration changes; `bootstrap/providers.php` is unaffected
- no global-search resource behavior changes
- no new destructive actions and no asset registration/deployment changes

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #406
2026-05-29 22:22:53 +00:00

1564 lines
62 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use App\Filament\Resources\BaselineProfileResource;
use App\Filament\Resources\FindingResource;
use App\Models\BaselineProfile;
use App\Models\InventoryItem;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Auth\CapabilityResolver;
use App\Services\Baselines\BaselineCompareService;
use App\Support\Auth\Capabilities;
use App\Support\Baselines\BaselineCaptureMode;
use App\Support\Baselines\BaselineCompareEvidenceGapDetails;
use App\Support\Baselines\BaselineCompareStats;
use App\Support\Baselines\TenantGovernanceAggregate;
use App\Support\Baselines\TenantGovernanceAggregateResolver;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\Navigation\NavigationScope;
use App\Support\OperationCatalog;
use App\Support\OperationRunLinks;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\OpsUx\OpsUxBrowserEvents;
use App\Support\Rbac\UiEnforcement;
use App\Support\ReasonTranslation\ReasonPresenter;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use App\Support\Workspaces\WorkspaceContext;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Database\Eloquent\Model;
use Livewire\Attributes\Locked;
use UnitEnum;
class BaselineCompareLanding extends Page
{
protected const MONITORING_PAGE_STATE_CONTRACT = [
'surfaceKey' => 'baseline_compare_landing',
'surfaceType' => 'launch_context_support',
'stateFields' => [
[
'stateKey' => 'baseline_profile_id',
'stateClass' => 'contextual_prefilter',
'carrier' => 'query_param',
'queryRole' => 'scoped_deeplink',
'shareable' => true,
'restorableOnRefresh' => true,
'tenantSensitive' => false,
'invalidFallback' => 'discard_and_continue',
],
[
'stateKey' => 'subject_key',
'stateClass' => 'contextual_prefilter',
'carrier' => 'query_param',
'queryRole' => 'scoped_deeplink',
'shareable' => true,
'restorableOnRefresh' => true,
'tenantSensitive' => false,
'invalidFallback' => 'discard_and_continue',
],
[
'stateKey' => 'nav',
'stateClass' => 'contextual_prefilter',
'carrier' => 'query_param',
'queryRole' => 'durable_restorable',
'shareable' => true,
'restorableOnRefresh' => true,
'tenantSensitive' => false,
'invalidFallback' => 'discard_and_continue',
],
],
'hydrationRule' => [
'precedenceOrder' => ['query', 'default'],
'appliesOnInitialMountOnly' => true,
'activeStateBecomesAuthoritativeAfterMount' => true,
'clearsOnTenantSwitch' => ['baseline_profile_id', 'subject_key', 'nav'],
'invalidRequestedStateFallback' => 'discard_and_continue',
],
'inspectContract' => [
'primaryModel' => 'none',
'selectedStateKey' => null,
'openedBy' => ['launch_context'],
'presentation' => 'none',
'shareable' => true,
'invalidSelectionFallback' => 'discard_and_continue',
],
'shareableStateKeys' => ['baseline_profile_id', 'subject_key', 'nav'],
'localOnlyStateKeys' => [],
];
private const LEGACY_SCOPE_QUERY_KEYS = [
'environment_id',
'tenant',
'tenant_id',
'managed_environment_id',
'environment',
'tenant_scope',
'tableFilters',
];
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-scale';
protected static string|UnitEnum|null $navigationGroup = 'Governance';
protected static ?string $navigationLabel = 'Baseline Compare';
protected static ?int $navigationSort = 10;
protected static ?string $title = 'Baseline Compare';
protected static ?string $slug = 'workspaces/{workspace}/environments/{environment}/baseline-compare';
protected string $view = 'filament.pages.baseline-compare-landing';
public static function shouldRegisterNavigation(): bool
{
return NavigationScope::shouldRegisterEnvironmentNavigation()
&& parent::shouldRegisterNavigation();
}
public ?string $state = null;
public ?string $message = null;
public ?string $reasonCode = null;
public ?string $reasonMessage = null;
public ?string $profileName = null;
public ?int $profileId = null;
public ?int $snapshotId = null;
public ?int $duplicateNamePoliciesCount = null;
public ?int $duplicateNameSubjectsCount = null;
public ?int $operationRunId = null;
public ?int $findingsCount = null;
/** @var array<string, int>|null */
public ?array $severityCounts = null;
public ?string $lastComparedAt = null;
public ?string $lastComparedIso = null;
public ?string $failureReason = null;
public ?string $coverageStatus = null;
public ?int $uncoveredTypesCount = null;
/** @var list<string>|null */
public ?array $uncoveredTypes = null;
public ?string $fidelity = null;
public ?int $evidenceGapsCount = null;
/** @var array<string, int>|null */
public ?array $evidenceGapsTopReasons = null;
/** @var array<string, mixed>|null */
public ?array $evidenceGapSummary = null;
/** @var list<array<string, mixed>>|null */
public ?array $evidenceGapBuckets = null;
/** @var array<string, mixed>|null */
public ?array $baselineCompareDiagnostics = null;
/** @var array<string, int>|null */
public ?array $rbacRoleDefinitionSummary = null;
/** @var array<string, mixed>|null */
public ?array $operatorExplanation = null;
/** @var array<string, mixed>|null */
public ?array $summaryAssessment = null;
/** @var array<string, mixed>|null */
public ?array $navigationContextPayload = null;
public ?int $matrixBaselineProfileId = null;
public ?string $matrixSubjectKey = null;
#[Locked]
public ?int $scopedEnvironmentId = null;
/**
* @param array<mixed> $parameters
*/
public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string
{
$panelId = $panel ?? Filament::getCurrentOrDefaultPanel()?->getId() ?? 'admin';
if ($panelId !== 'admin') {
return parent::getUrl($parameters, $isAbsolute, $panelId, $tenant);
}
$environment = static::resolveAdminUrlEnvironment($parameters, $tenant);
if (! $environment instanceof ManagedEnvironment) {
return url('/admin');
}
$workspace = static::resolveAdminUrlWorkspace($environment, $parameters);
if (! $workspace instanceof Workspace && ! is_string($workspace) && ! is_int($workspace)) {
return url('/admin');
}
$parameters = static::withoutLegacyScopeQuery($parameters);
$parameters['environment'] = $environment;
$parameters['workspace'] = $workspace instanceof Workspace
? static::workspaceRouteKey($workspace)
: $workspace;
return parent::getUrl($parameters, $isAbsolute, $panelId, null);
}
public static function canAccess(): bool
{
return static::hasEnvironmentAccess(static::resolveRouteOwnedEnvironment());
}
/**
* @return array<string, mixed>
*/
public static function monitoringPageStateContract(): array
{
return self::MONITORING_PAGE_STATE_CONTRACT;
}
public function mount(ManagedEnvironment|string|null $environment = null): void
{
$tenant = static::resolveRouteOwnedEnvironment($environment);
if (! $tenant instanceof ManagedEnvironment || ! static::hasEnvironmentAccess($tenant)) {
abort(404);
}
$this->scopedEnvironmentId = (int) $tenant->getKey();
$this->navigationContextPayload = is_array(request()->query('nav')) ? request()->query('nav') : null;
$baselineProfileId = request()->query('baseline_profile_id');
$subjectKey = request()->query('subject_key');
$this->matrixBaselineProfileId = is_numeric($baselineProfileId) ? (int) $baselineProfileId : null;
$this->matrixSubjectKey = is_string($subjectKey) && trim($subjectKey) !== '' ? trim($subjectKey) : null;
$this->refreshStatsForEnvironment($tenant);
}
public function refreshStats(): void
{
$this->refreshStatsForEnvironment($this->currentEnvironment());
}
private function refreshStatsForEnvironment(?ManagedEnvironment $tenant): void
{
$stats = BaselineCompareStats::forTenant($tenant);
$aggregate = $tenant instanceof ManagedEnvironment
? $this->governanceAggregate($tenant, $stats)
: null;
$this->state = $stats->state;
$this->message = $stats->message;
$this->profileName = $stats->profileName;
$this->profileId = $stats->profileId;
$this->snapshotId = $stats->snapshotId;
$this->duplicateNamePoliciesCount = $stats->duplicateNamePoliciesCount;
$this->duplicateNameSubjectsCount = $stats->duplicateNameSubjectsCount;
$this->operationRunId = $stats->operationRunId;
$this->findingsCount = $stats->findingsCount;
$this->severityCounts = $stats->severityCounts !== [] ? $stats->severityCounts : null;
$this->lastComparedAt = $stats->lastComparedHuman;
$this->lastComparedIso = $stats->lastComparedIso;
$this->failureReason = $stats->failureReason;
$this->reasonCode = $stats->reasonCode;
$this->reasonMessage = $stats->reasonMessage;
$this->coverageStatus = $stats->coverageStatus;
$this->uncoveredTypesCount = $stats->uncoveredTypesCount;
$this->uncoveredTypes = $stats->uncoveredTypes !== [] ? $stats->uncoveredTypes : null;
$this->fidelity = $stats->fidelity;
$this->evidenceGapsCount = $stats->evidenceGapsCount;
$this->evidenceGapsTopReasons = $stats->evidenceGapsTopReasons !== [] ? $stats->evidenceGapsTopReasons : null;
$this->evidenceGapSummary = is_array($stats->evidenceGapDetails['summary'] ?? null)
? $stats->evidenceGapDetails['summary']
: null;
$this->evidenceGapBuckets = is_array($stats->evidenceGapDetails['buckets'] ?? null) && $stats->evidenceGapDetails['buckets'] !== []
? $stats->evidenceGapDetails['buckets']
: null;
$this->baselineCompareDiagnostics = $stats->baselineCompareDiagnostics !== []
? $stats->baselineCompareDiagnostics
: null;
$this->rbacRoleDefinitionSummary = $stats->rbacRoleDefinitionSummary !== [] ? $stats->rbacRoleDefinitionSummary : null;
$this->operatorExplanation = $stats->operatorExplanation()->toArray();
$this->summaryAssessment = $aggregate?->summaryAssessment->toArray();
}
/**
* Computed view data exposed to the Blade template.
*
* Moves presentational logic out of Blade `@php` blocks so the
* template only receives ready-to-render values.
*
* @return array<string, mixed>
*/
protected function getViewData(): array
{
$evidenceGapSummary = is_array($this->evidenceGapSummary) ? $this->evidenceGapSummary : [];
$reasonPresenter = app(ReasonPresenter::class);
$reasonSemantics = $reasonPresenter->semantics(
$reasonPresenter->forArtifactTruth($this->reasonCode, 'baseline_compare_landing'),
);
$hasCoverageWarnings = in_array($this->coverageStatus, ['warning', 'unproven'], true);
$evidenceGapsCountValue = is_numeric($evidenceGapSummary['count'] ?? null)
? (int) $evidenceGapSummary['count']
: (int) ($this->evidenceGapsCount ?? 0);
$hasEvidenceGaps = $evidenceGapsCountValue > 0;
$hasWarnings = $hasCoverageWarnings || $hasEvidenceGaps;
$hasRbacRoleDefinitionSummary = is_array($this->rbacRoleDefinitionSummary)
&& array_sum($this->rbacRoleDefinitionSummary) > 0;
$evidenceGapDetailState = is_string($evidenceGapSummary['detail_state'] ?? null)
? (string) $evidenceGapSummary['detail_state']
: 'no_gaps';
$hasEvidenceGapDetailSection = $evidenceGapDetailState !== 'no_gaps';
$hasEvidenceGapDiagnostics = is_array($this->baselineCompareDiagnostics) && $this->baselineCompareDiagnostics !== [];
$evidenceGapsSummary = null;
$evidenceGapsTooltip = null;
if ($hasEvidenceGaps) {
$parts = array_map(
static fn (array $reason): string => $reason['reason_label'].' ('.$reason['count'].')',
BaselineCompareEvidenceGapDetails::topReasons(
is_array($evidenceGapSummary['by_reason'] ?? null) ? $evidenceGapSummary['by_reason'] : [],
5,
),
);
if ($parts !== []) {
$evidenceGapsSummary = implode(', ', $parts);
$evidenceGapsTooltip = __('baseline-compare.evidence_gaps_tooltip', ['summary' => $evidenceGapsSummary]);
}
}
// Derive the colour class for the findings-count stat card.
// Only show danger-red when high-severity findings exist;
// use warning-orange for low/medium-only, and success-green for zero.
$findingsColorClass = $this->resolveFindingsColorClass($hasWarnings);
// "Why no findings" explanation when count is zero.
$whyNoFindingsMessage = filled($this->reasonMessage) ? (string) $this->reasonMessage : null;
$whyNoFindingsFallback = ! $hasWarnings
? __('baseline-compare.no_findings_all_clear')
: ($hasCoverageWarnings
? __('baseline-compare.no_findings_coverage_warnings')
: ($hasEvidenceGaps
? __('baseline-compare.no_findings_evidence_gaps')
: __('baseline-compare.no_findings_default')));
$whyNoFindingsColor = $hasWarnings
? 'text-warning-600 dark:text-warning-400'
: 'text-success-600 dark:text-success-400';
if ($this->reasonCode === 'no_subjects_in_scope') {
$whyNoFindingsColor = 'text-gray-600 dark:text-gray-400';
}
return [
'navigationContext' => $this->navigationContext()?->toQuery()['nav'] ?? null,
'matrixBaselineProfileId' => $this->matrixBaselineProfileId,
'matrixSubjectKey' => $this->matrixSubjectKey,
'openCompareMatrixUrl' => $this->openCompareMatrixUrl(),
'decisionCard' => $this->decisionCard($hasWarnings, $hasCoverageWarnings, $hasEvidenceGaps),
'decisionSummaryItems' => $this->decisionSummaryItems($hasWarnings, $hasCoverageWarnings, $hasEvidenceGaps),
'proofPanelItems' => $this->proofPanelItems($hasWarnings, $hasCoverageWarnings, $hasEvidenceGaps),
'compareReadinessFlow' => $this->compareReadinessFlow($hasWarnings, $hasCoverageWarnings),
'availableCompareInputs' => $this->availableCompareInputs($hasWarnings, $hasCoverageWarnings, $hasEvidenceGaps),
'assignmentUnlocks' => $this->assignmentUnlocks(),
'diagnosticsDisclosure' => $this->diagnosticsDisclosure($hasEvidenceGapDiagnostics),
'hasCoverageWarnings' => $hasCoverageWarnings,
'evidenceGapsCountValue' => $evidenceGapsCountValue,
'hasEvidenceGaps' => $hasEvidenceGaps,
'hasWarnings' => $hasWarnings,
'hasRbacRoleDefinitionSummary' => $hasRbacRoleDefinitionSummary,
'evidenceGapDetailState' => $evidenceGapDetailState,
'hasEvidenceGapDetailSection' => $hasEvidenceGapDetailSection,
'hasEvidenceGapDiagnostics' => $hasEvidenceGapDiagnostics,
'evidenceGapsSummary' => $evidenceGapsSummary,
'evidenceGapsTooltip' => $evidenceGapsTooltip,
'findingsColorClass' => $findingsColorClass,
'whyNoFindingsMessage' => $whyNoFindingsMessage,
'whyNoFindingsFallback' => $whyNoFindingsFallback,
'whyNoFindingsColor' => $whyNoFindingsColor,
'summaryAssessment' => is_array($this->summaryAssessment) ? $this->summaryAssessment : null,
'reasonSemantics' => $reasonSemantics,
];
}
/**
* @return array<string, mixed>
*/
private function decisionCard(bool $hasWarnings, bool $hasCoverageWarnings, bool $hasEvidenceGaps): array
{
$state = (string) ($this->state ?? 'unknown');
$findingsCount = (int) ($this->findingsCount ?? 0);
$hasHighSeverity = ($this->severityCounts['high'] ?? 0) > 0;
$primaryAction = $this->primaryDecisionAction($state, $findingsCount);
return [
'question' => 'Which baseline drift requires action?',
'statusLabel' => 'Status',
'status' => $this->decisionStatus($state, $findingsCount, $hasWarnings),
'tone' => match (true) {
$state === 'failed' || $hasHighSeverity => 'danger',
in_array($state, ['no_assignment', 'no_snapshot', 'invalid_scope'], true) || $hasWarnings || $findingsCount > 0 => 'warning',
$state === 'comparing' => 'info',
$state === 'ready' => 'success',
default => 'gray',
},
'reasonLabel' => 'Reason',
'reason' => $this->decisionReason($state, $findingsCount),
'impactLabel' => 'Impact',
'impact' => $this->decisionImpact($state, $findingsCount, $hasCoverageWarnings, $hasEvidenceGaps),
'evidenceLabel' => 'Evidence path',
'evidence' => $this->evidencePathSummary($hasCoverageWarnings, $hasEvidenceGaps),
'nextActionLabel' => 'Next action',
...$primaryAction,
];
}
private function decisionStatus(string $state, int $findingsCount, bool $hasWarnings): string
{
return match ($state) {
'no_assignment' => 'Baseline not assigned',
'no_snapshot' => 'Baseline snapshot required',
'invalid_scope' => 'Baseline scope requires review',
'comparing' => 'Compare in progress',
'failed' => 'Compare failed',
'idle' => 'Compare run required',
'ready' => $findingsCount > 0
? 'Drift findings available'
: ($hasWarnings ? 'Decision output needs review' : 'No drift detected'),
default => 'Compare unavailable',
};
}
private function decisionReason(string $state, int $findingsCount): string
{
$reason = trim((string) ($this->reasonMessage ?? ''));
$message = trim((string) ($this->message ?? ''));
return match ($state) {
'no_assignment' => 'This environment does not have an assigned baseline.',
'no_snapshot' => 'A baseline is assigned, but no usable baseline snapshot is available.'
.($reason !== '' ? ' '.$reason : ($message !== '' ? ' '.$message : '')),
'invalid_scope' => 'A baseline is assigned, but its scope cannot be used safely for compare.'
.($reason !== '' ? ' '.$reason : ($message !== '' ? ' '.$message : '')),
'failed' => $this->failureReason ?: ($reason !== '' ? $reason : 'The compare operation ended with errors.'),
'comparing' => 'Baseline comparison is currently running.',
'idle' => 'Required inputs exist, but no compare run has been created for the current state.',
'ready' => $findingsCount > 0
? 'Baseline comparison found governance-relevant differences. Drift requires review before a decision is recorded.'
: 'Current environment state matches the assigned baseline within available compare coverage.',
default => 'Compare state is derived from the latest baseline assignment, snapshot, and operation proof.',
};
}
private function decisionImpact(string $state, int $findingsCount, bool $hasCoverageWarnings, bool $hasEvidenceGaps): string
{
if ($state === 'no_assignment') {
return 'Baseline drift cannot be used for governance decisions until a baseline assignment exists.';
}
if ($state === 'no_snapshot') {
return 'Compare cannot run until baseline snapshot input exists.';
}
if ($state === 'invalid_scope') {
return 'Compare cannot run safely until the assigned baseline scope is reviewed.';
}
if ($state === 'failed') {
return 'Drift findings cannot be trusted until the failure is resolved. Review operation proof before retrying.';
}
if ($state === 'comparing') {
return 'Drift findings are not final yet. Wait for operation proof before acting on drift or evidence state.';
}
if ($state === 'idle') {
return 'Drift findings are not available yet. Run compare after the required inputs are confirmed.';
}
if ($findingsCount > 0) {
return 'Review findings and decide the next governance action before presenting this environment as aligned to baseline.';
}
if ($hasCoverageWarnings || $hasEvidenceGaps) {
return 'Zero findings must not be treated as final while coverage or evidence gaps remain.';
}
return 'No governance action is required from this compare result within available compare coverage.';
}
/**
* @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string,actionName?:string}
*/
private function primaryDecisionAction(string $state, int $findingsCount): array
{
if ($state === 'no_assignment') {
$canOpenBaselines = BaselineProfileResource::canViewAny();
return [
'actionLabel' => $canOpenBaselines ? 'Open baseline profiles' : 'Baseline assignment unavailable',
'actionUrl' => $canOpenBaselines ? BaselineProfileResource::getUrl('index', panel: 'admin') : null,
'actionDisabled' => ! $canOpenBaselines,
'helperText' => $canOpenBaselines
? 'Open baseline profiles to assign a baseline to this environment.'
: 'No authorized baseline assignment path is available from this page.',
];
}
if (in_array($state, ['no_snapshot', 'invalid_scope'], true)) {
$profileUrl = $this->baselineProfileUrl();
return [
'actionLabel' => $profileUrl !== null ? 'Open baseline profile' : 'Baseline profile unavailable',
'actionUrl' => $profileUrl,
'actionDisabled' => $profileUrl === null,
'helperText' => $profileUrl !== null
? 'Open the assigned baseline profile to review capture and snapshot state.'
: 'No authorized baseline profile path is available from this page.',
];
}
if ($state === 'idle') {
$canRunCompare = $this->canRunCompareAction();
return [
'actionLabel' => $canRunCompare ? 'Compare now' : 'Compare unavailable',
'actionUrl' => null,
'actionDisabled' => ! $canRunCompare,
'actionName' => 'compareNow',
'helperText' => $canRunCompare
? 'Use the confirmed Compare now action to generate drift findings.'
: 'You are not authorized to start baseline compare from this environment.',
];
}
if ($state === 'comparing' && $this->getRunUrl() !== null) {
return [
'actionLabel' => 'View operation progress',
'actionUrl' => $this->getRunUrl(),
'actionDisabled' => false,
'helperText' => null,
];
}
if ($state === 'failed' && $this->getRunUrl() !== null) {
return [
'actionLabel' => 'Review compare failure',
'actionUrl' => $this->getRunUrl(),
'actionDisabled' => false,
'helperText' => null,
];
}
if ($state === 'ready' && $findingsCount > 0 && $this->getFindingsUrl() !== null) {
return [
'actionLabel' => 'Review drift findings',
'actionUrl' => $this->getFindingsUrl(),
'actionDisabled' => false,
'helperText' => null,
];
}
if ($state === 'ready' && $this->getRunUrl() !== null) {
return [
'actionLabel' => 'Review evidence',
'actionUrl' => $this->getRunUrl(),
'actionDisabled' => false,
'helperText' => null,
];
}
return [
'actionLabel' => 'Review compare state',
'actionUrl' => $this->getRunUrl(),
'actionDisabled' => $this->getRunUrl() === null,
'helperText' => $this->getRunUrl() === null ? 'No operation proof link is currently available.' : null,
];
}
/**
* @return list<array<string, string>>
*/
private function decisionSummaryItems(bool $hasWarnings, bool $hasCoverageWarnings, bool $hasEvidenceGaps): array
{
if ($this->state === 'no_assignment') {
return [];
}
return [
[
'label' => 'Assigned baseline',
'value' => $this->profileName ?? 'Baseline not assigned',
'description' => $this->snapshotId !== null ? 'Snapshot #'.$this->snapshotId : 'No consumable snapshot proof is linked.',
],
[
'label' => 'Compare trust',
'value' => $this->compareTrustLabel($hasWarnings),
'description' => $this->coverageDescription($hasCoverageWarnings, $hasEvidenceGaps),
],
[
'label' => 'Drift impact',
'value' => $this->driftImpactLabel(),
'description' => 'Derived from latest findings, severity counts, and compare result state.',
],
];
}
/**
* @return list<array<string, mixed>>
*/
private function proofPanelItems(bool $hasWarnings, bool $hasCoverageWarnings, bool $hasEvidenceGaps): array
{
if ($this->state === 'no_assignment') {
return [];
}
$environmentSnapshotState = $this->environmentSnapshotState($hasCoverageWarnings);
$operationProofAvailable = $this->operationRunId !== null && $this->getRunUrl() !== null;
$baselineSnapshotState = $this->snapshotId !== null ? 'Available' : ($this->state === 'no_snapshot' ? 'Missing' : 'Unavailable');
$driftFindingsAvailable = $this->state === 'ready';
return [
[
'key' => 'assigned_baseline',
'label' => 'Assigned baseline',
'value' => $this->profileName ?? 'Baseline not assigned',
'tone' => $this->profileName !== null ? 'success' : 'warning',
'description' => $this->state === 'invalid_scope'
? 'Assignment exists, but the baseline scope requires review.'
: 'Environment-owned baseline assignment state.',
'actionLabel' => $this->openCompareMatrixUrl() !== null ? 'Open compare matrix' : null,
'actionUrl' => $this->openCompareMatrixUrl(),
],
[
'key' => 'baseline_snapshot',
'label' => 'Baseline snapshot',
'value' => $baselineSnapshotState,
'tone' => $this->flowTone($baselineSnapshotState),
'description' => $this->snapshotId !== null ? 'Snapshot #'.$this->snapshotId.' is the baseline compare input.' : 'No usable baseline snapshot input is linked.',
'actionLabel' => $this->baselineProfileUrl() !== null ? 'Open baseline profile' : null,
'actionUrl' => $this->baselineProfileUrl(),
],
[
'key' => 'environment_snapshot',
'label' => 'Environment snapshot',
'value' => $environmentSnapshotState,
'tone' => $this->flowTone($environmentSnapshotState),
'description' => $this->environmentSnapshotDescription($environmentSnapshotState),
'actionLabel' => null,
'actionUrl' => null,
],
[
'key' => 'operation_run_proof',
'label' => 'OperationRun proof',
'value' => $operationProofAvailable ? 'Available' : 'Unavailable',
'tone' => $operationProofAvailable ? 'success' : 'gray',
'description' => $operationProofAvailable ? 'Compare proof is linked to an OperationRun.' : 'No compare OperationRun proof is available yet.',
'actionLabel' => $operationProofAvailable ? 'Open operation proof' : null,
'actionUrl' => $operationProofAvailable ? $this->getRunUrl() : null,
],
[
'key' => 'drift_findings',
'label' => 'Drift findings',
'value' => $driftFindingsAvailable ? $this->driftImpactLabel() : 'Unavailable',
'tone' => $driftFindingsAvailable && ((int) ($this->findingsCount ?? 0)) > 0 ? 'warning' : ($driftFindingsAvailable ? 'success' : 'gray'),
'description' => $this->driftFindingsDescription(),
'actionLabel' => $this->getFindingsUrl() !== null && ((int) ($this->findingsCount ?? 0)) > 0 ? 'Review findings' : null,
'actionUrl' => ((int) ($this->findingsCount ?? 0)) > 0 ? $this->getFindingsUrl() : null,
],
[
'key' => 'evidence_path',
'label' => 'Evidence path',
'value' => $this->evidencePathSummary($hasCoverageWarnings, $hasEvidenceGaps),
'tone' => ($hasCoverageWarnings || $hasEvidenceGaps) ? 'warning' : 'gray',
'description' => $this->evidenceInputDescription($hasCoverageWarnings, $hasEvidenceGaps),
'actionLabel' => null,
'actionUrl' => null,
],
];
}
/**
* @return list<array<string, mixed>>
*/
private function compareReadinessFlow(bool $hasWarnings, bool $hasCoverageWarnings): array
{
$state = (string) ($this->state ?? 'unknown');
$environmentSnapshotState = $this->environmentSnapshotState($hasCoverageWarnings);
$baselineAssignedState = match ($state) {
'no_assignment' => 'Missing',
'invalid_scope' => 'Needs review',
'no_tenant' => 'Unavailable',
default => 'Complete',
};
$baselineSnapshotState = match ($state) {
'no_assignment' => 'Unavailable',
'no_snapshot' => 'Missing',
'invalid_scope' => 'Unavailable',
default => $this->snapshotId !== null ? 'Available' : 'Unavailable',
};
$compareRunState = match ($state) {
'idle' => 'Required',
'comparing' => 'In progress',
'failed' => 'Failed',
'ready' => 'Available',
default => 'Unavailable',
};
$decisionOutputState = match ($state) {
'ready' => $hasWarnings ? 'Needs review' : 'Available',
'idle' => 'Required',
default => 'Unavailable',
};
return [
[
'label' => 'Baseline assigned',
'state' => $baselineAssignedState,
'tone' => $this->flowTone($baselineAssignedState),
'description' => match ($baselineAssignedState) {
'Complete' => 'Baseline assignment exists.',
'Needs review' => 'Assignment scope needs review.',
'Missing' => 'No baseline is assigned.',
default => 'Assignment unavailable.',
},
'currentBlocker' => in_array($state, ['no_assignment', 'invalid_scope'], true),
],
[
'label' => 'Baseline snapshot',
'state' => $baselineSnapshotState,
'tone' => $this->flowTone($baselineSnapshotState),
'description' => match ($baselineSnapshotState) {
'Available' => 'Snapshot #'.$this->snapshotId.' is available.',
'Missing' => 'No usable snapshot.',
'Needs review' => 'Snapshot needs review.',
default => 'No snapshot linked.',
},
'currentBlocker' => $state === 'no_snapshot',
],
[
'label' => 'Environment snapshot',
'state' => $environmentSnapshotState,
'tone' => $this->flowTone($environmentSnapshotState),
'description' => $this->environmentSnapshotDescription($environmentSnapshotState),
'currentBlocker' => false,
],
[
'label' => 'Compare run',
'state' => $compareRunState,
'tone' => $this->flowTone($compareRunState),
'description' => match ($compareRunState) {
'Available' => 'Completed run available.',
'Required' => 'Run compare.',
'In progress' => 'Queued or running.',
'Failed' => 'Latest run failed.',
default => 'Blocked by missing inputs.',
},
'currentBlocker' => in_array($state, ['idle', 'comparing', 'failed'], true),
],
[
'label' => 'Decision output',
'state' => $decisionOutputState,
'tone' => $this->flowTone($decisionOutputState),
'description' => match ($decisionOutputState) {
'Available' => 'Decision output available.',
'Needs review' => 'Evidence or coverage needs review.',
'Required' => 'Run compare first.',
default => 'No decision output.',
},
'currentBlocker' => $state === 'ready' && $hasWarnings,
],
];
}
/**
* @return list<array<string, string>>
*/
private function availableCompareInputs(bool $hasWarnings, bool $hasCoverageWarnings, bool $hasEvidenceGaps): array
{
if ($this->state !== 'no_assignment') {
$environmentSnapshotState = $this->environmentSnapshotState($hasCoverageWarnings);
$baselineSnapshotState = $this->state === 'no_snapshot'
? 'Missing'
: ($this->snapshotId !== null ? 'Available' : 'Unavailable');
$operationProofState = $this->operationRunId !== null && $this->getRunUrl() !== null ? 'Available' : 'Unavailable';
$driftFindingsState = $this->state === 'ready' ? 'Available' : 'Unavailable';
$evidencePathState = $this->evidenceInputState($hasWarnings);
return [
[
'label' => 'Assigned baseline',
'state' => $this->state === 'invalid_scope' ? 'Needs review' : 'Available',
'tone' => $this->state === 'invalid_scope' ? 'warning' : 'success',
'description' => $this->profileName !== null
? 'Assigned baseline: '.$this->profileName.'.'
: 'Baseline assignment exists but requires review.',
],
[
'label' => 'Baseline snapshot',
'state' => $baselineSnapshotState,
'tone' => $this->flowTone($baselineSnapshotState),
'description' => $this->snapshotId !== null
? 'Snapshot #'.$this->snapshotId.' is the compare input.'
: 'No usable baseline snapshot input is linked.',
],
[
'label' => 'Environment snapshot',
'state' => $environmentSnapshotState,
'tone' => $this->flowTone($environmentSnapshotState),
'description' => $this->environmentSnapshotDescription($environmentSnapshotState),
],
[
'label' => 'OperationRun proof',
'state' => $operationProofState,
'tone' => $this->flowTone($operationProofState),
'description' => $operationProofState === 'Available'
? 'A compare OperationRun proof link is available.'
: 'No compare OperationRun proof is available yet.',
],
[
'label' => 'Drift findings',
'state' => $driftFindingsState,
'tone' => $driftFindingsState === 'Available' && ((int) ($this->findingsCount ?? 0)) > 0 ? 'warning' : $this->flowTone($driftFindingsState),
'description' => $this->driftFindingsDescription(),
],
[
'label' => 'Evidence path',
'state' => $evidencePathState,
'tone' => $this->flowTone($evidencePathState),
'description' => $this->evidenceInputDescription($hasCoverageWarnings, $hasEvidenceGaps),
],
];
}
$environmentSnapshotState = $this->hasEnvironmentSnapshot() ? 'Available' : 'Unavailable';
$operationProofState = $this->operationRunId !== null && $this->getRunUrl() !== null ? 'Available' : 'Unavailable';
return [
[
'label' => 'Environment snapshot',
'state' => $environmentSnapshotState,
'tone' => $environmentSnapshotState === 'Available' ? 'success' : 'gray',
'description' => $environmentSnapshotState === 'Available'
? 'Current environment evidence is present.'
: 'No repo-backed environment snapshot is available yet.',
],
[
'label' => 'Operation proof',
'state' => $operationProofState,
'tone' => $operationProofState === 'Available' ? 'success' : 'gray',
'description' => $operationProofState === 'Available'
? 'A compare operation proof link is available.'
: 'No compare operation proof is available yet.',
],
[
'label' => 'Baseline snapshot',
'state' => 'Unavailable',
'tone' => 'gray',
'description' => 'Unavailable because no baseline assigned.',
],
];
}
/**
* @return list<string>
*/
private function assignmentUnlocks(): array
{
if ($this->state !== 'no_assignment') {
return [];
}
return [
'Actionable drift categories',
'Evidence-backed compare',
'Governance decision path',
];
}
private function hasEnvironmentSnapshot(): bool
{
$environment = $this->currentEnvironment();
if (! $environment instanceof ManagedEnvironment) {
return false;
}
$environmentId = (int) $environment->getKey();
if (InventoryItem::query()->where('managed_environment_id', $environmentId)->exists()) {
return true;
}
return OperationRun::query()
->where('managed_environment_id', $environmentId)
->whereIn('type', OperationCatalog::rawValuesForCanonical(OperationRunType::InventorySync->value))
->where('status', OperationRunStatus::Completed->value)
->exists();
}
private function compareTrustLabel(bool $hasWarnings): string
{
if ($this->state === 'ready' && ! $hasWarnings) {
return 'Usable latest compare';
}
if ($this->state === 'ready') {
return 'Usable with warnings';
}
return match ($this->state) {
'idle' => 'Not generated yet',
'comparing' => 'Generating',
'failed' => 'Failed',
'no_assignment' => 'Unavailable',
'no_snapshot' => 'Unavailable',
'invalid_scope' => 'Needs review',
default => 'Unavailable',
};
}
private function coverageDescription(bool $hasCoverageWarnings, bool $hasEvidenceGaps): string
{
if ($this->state === 'no_assignment') {
return 'Compare trust is unavailable until a baseline assignment exists.';
}
if ($hasCoverageWarnings && $hasEvidenceGaps) {
return 'Coverage warnings and evidence gaps limit the latest compare.';
}
if ($hasCoverageWarnings) {
return 'Coverage warnings limit the latest compare.';
}
if ($hasEvidenceGaps) {
return 'Evidence gaps still need review before governance use.';
}
return 'No coverage warning is currently reported for the latest compare.';
}
private function driftImpactLabel(): string
{
$findingsCount = (int) ($this->findingsCount ?? 0);
if ($findingsCount > 0) {
return $findingsCount.' '.\Illuminate\Support\Str::plural('finding', $findingsCount).' need review';
}
if ($this->state === 'ready') {
return 'No confirmed drift';
}
return 'No usable drift result';
}
private function evidencePathSummary(bool $hasCoverageWarnings, bool $hasEvidenceGaps): string
{
if ($hasEvidenceGaps) {
return 'Evidence unavailable - Evidence gaps need review';
}
if ($hasCoverageWarnings) {
return 'Evidence unavailable - Coverage warning recorded';
}
if ($this->operationRunId !== null) {
return 'Evidence unavailable - Operation proof available';
}
return 'Evidence unavailable';
}
private function baselineProfileUrl(): ?string
{
if (! BaselineProfileResource::canViewAny()) {
return null;
}
if ($this->profileId !== null) {
return BaselineProfileResource::getUrl('view', ['record' => $this->profileId], panel: 'admin');
}
return BaselineProfileResource::getUrl('index', panel: 'admin');
}
private function canRunCompareAction(): bool
{
if (! in_array($this->state, ['idle', 'ready', 'failed'], true)) {
return false;
}
$user = auth()->user();
$tenant = $this->currentEnvironment();
if (! $user instanceof User || ! $tenant instanceof ManagedEnvironment) {
return false;
}
return app(CapabilityResolver::class)->can($user, $tenant, Capabilities::TENANT_SYNC);
}
private function environmentSnapshotState(bool $hasCoverageWarnings): string
{
if ($hasCoverageWarnings) {
return 'Needs review';
}
return $this->hasEnvironmentSnapshot() ? 'Available' : 'Unavailable';
}
private function environmentSnapshotDescription(string $environmentSnapshotState): string
{
return match ($environmentSnapshotState) {
'Available' => 'Environment snapshot evidence is present.',
'Needs review' => 'Coverage proof has warnings.',
default => 'No environment snapshot yet.',
};
}
private function flowTone(string $state): string
{
return match ($state) {
'Available', 'Complete' => 'success',
'Missing', 'Required', 'Needs review' => 'warning',
'In progress' => 'info',
'Failed' => 'danger',
default => 'gray',
};
}
private function driftFindingsDescription(): string
{
if ($this->state !== 'ready') {
return 'Run compare after required inputs exist.';
}
$findingsCount = (int) ($this->findingsCount ?? 0);
if ($findingsCount > 0) {
return $findingsCount.' '.\Illuminate\Support\Str::plural('open drift finding', $findingsCount).' available for review.';
}
return 'Zero open drift findings are recorded for the latest compare result.';
}
private function evidenceInputState(bool $hasWarnings): string
{
if ($this->operationRunId === null) {
return 'Unavailable';
}
return $hasWarnings ? 'Needs review' : 'Unavailable';
}
private function evidenceInputDescription(bool $hasCoverageWarnings, bool $hasEvidenceGaps): string
{
if ($hasEvidenceGaps) {
return 'Compare result exists, but evidence output is not available. Evidence gaps need review.';
}
if ($hasCoverageWarnings) {
return 'Compare result exists, but evidence output is not available. Coverage warnings need review.';
}
if ($this->operationRunId !== null) {
return 'Compare result exists, but no evidence output is linked yet.';
}
return 'No evidence output is linked yet.';
}
/**
* @return array<string, mixed>
*/
private function diagnosticsDisclosure(bool $hasEvidenceGapDiagnostics): array
{
return [
'label' => 'Diagnostics - Collapsed',
'summary' => $hasEvidenceGapDiagnostics
? 'Support diagnostics exist for this compare and stay closed behind the disclosure until needed.'
: 'No support diagnostics are available for this compare state.',
];
}
/**
* Resolve the Tailwind colour class for the Total Findings stat.
*
* - Red (danger) only when high-severity findings exist
* - Orange (warning) for medium/low-only findings or when warnings present
* - Green (success) when fully clear
*/
private function resolveFindingsColorClass(bool $hasWarnings): string
{
$count = (int) ($this->findingsCount ?? 0);
if ($count === 0) {
return $hasWarnings
? 'text-warning-600 dark:text-warning-400'
: 'text-success-600 dark:text-success-400';
}
$hasHigh = ($this->severityCounts['high'] ?? 0) > 0;
return $hasHigh
? 'text-danger-600 dark:text-danger-400'
: 'text-warning-600 dark:text-warning-400';
}
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly)
->satisfy(ActionSurfaceSlot::ListHeader, 'Header action: Compare Now (confirmation modal, capability-gated).')
->exempt(ActionSurfaceSlot::InspectAffordance, 'This is a tenant-scoped landing page, not a record inspect surface.')
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'This page does not render table rows with secondary actions.')
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'This page has no bulk actions.')
->satisfy(ActionSurfaceSlot::ListEmptyState, 'Page renders explicit empty states for missing tenant, missing assignment, and missing snapshot, with guidance messaging.')
->exempt(ActionSurfaceSlot::DetailHeader, 'This page does not have a record detail header; it uses a page header action instead.');
}
/**
* @return array<Action>
*/
protected function getHeaderActions(): array
{
$actions = [];
$navigationContext = $this->navigationContext();
if ($navigationContext?->backLinkLabel !== null && $navigationContext->backLinkUrl !== null) {
$actions[] = Action::make('backToOrigin')
->label($navigationContext->backLinkLabel)
->color('gray')
->url($navigationContext->backLinkUrl);
}
$actions[] = $this->compareNowAction();
return $actions;
}
private function compareNowAction(): Action
{
$isFullContent = false;
if (is_int($this->profileId) && $this->profileId > 0) {
$profile = \App\Models\BaselineProfile::query()->find($this->profileId);
$mode = $profile?->capture_mode instanceof BaselineCaptureMode
? $profile->capture_mode
: (is_string($profile?->capture_mode) ? BaselineCaptureMode::tryFrom($profile->capture_mode) : null);
$isFullContent = $mode === BaselineCaptureMode::FullContent;
}
$label = $isFullContent ? 'Compare now (full content)' : 'Compare now';
$modalDescription = $isFullContent
? 'This will refresh content evidence on demand (redacted) before comparing the current tenant inventory against the assigned baseline snapshot.'
: 'This will compare the current tenant inventory against the assigned baseline snapshot and generate drift findings.';
$action = Action::make('compareNow')
->label($label)
->icon('heroicon-o-play')
->requiresConfirmation()
->modalHeading($label)
->modalDescription($modalDescription)
->disabled(fn (): bool => ! in_array($this->state, ['idle', 'ready', 'failed'], true))
->action(function (): void {
$user = auth()->user();
if (! $user instanceof User) {
Notification::make()->title('Not authenticated')->danger()->send();
return;
}
$tenant = $this->currentEnvironment();
if (! $tenant instanceof ManagedEnvironment) {
Notification::make()->title('Open an environment to compare baselines')->danger()->send();
return;
}
$service = app(BaselineCompareService::class);
$result = $service->startCompare($tenant, $user);
if (! ($result['ok'] ?? false)) {
$reasonCode = is_string($result['reason_code'] ?? null) ? (string) $result['reason_code'] : 'unknown';
$translation = is_array($result['reason_translation'] ?? null) ? $result['reason_translation'] : [];
$message = is_string($translation['short_explanation'] ?? null) && trim((string) $translation['short_explanation']) !== ''
? trim((string) $translation['short_explanation'])
: match ($reasonCode) {
\App\Support\Baselines\BaselineReasonCodes::COMPARE_ROLLOUT_DISABLED => 'Full-content baseline compare is currently disabled for controlled rollout.',
\App\Support\Baselines\BaselineReasonCodes::COMPARE_PROFILE_NOT_ACTIVE => 'The assigned baseline profile is not active.',
\App\Support\Baselines\BaselineReasonCodes::COMPARE_NO_ACTIVE_SNAPSHOT,
\App\Support\Baselines\BaselineReasonCodes::COMPARE_NO_CONSUMABLE_SNAPSHOT => 'No complete baseline snapshot is currently available for compare.',
\App\Support\Baselines\BaselineReasonCodes::COMPARE_SNAPSHOT_BUILDING => 'The latest baseline capture is still building. Compare will be available after it completes.',
\App\Support\Baselines\BaselineReasonCodes::COMPARE_SNAPSHOT_INCOMPLETE => 'The latest baseline capture is incomplete. Capture a new baseline before comparing.',
\App\Support\Baselines\BaselineReasonCodes::COMPARE_SNAPSHOT_SUPERSEDED => 'A newer complete baseline snapshot is current. Compare uses the latest complete baseline only.',
\App\Support\Baselines\BaselineReasonCodes::COMPARE_INVALID_SCOPE => 'The assigned baseline scope is invalid and must be reviewed before compare can start.',
\App\Support\Baselines\BaselineReasonCodes::COMPARE_UNSUPPORTED_SCOPE => 'The selected governed subjects are not supported by any compare strategy.',
\App\Support\Baselines\BaselineReasonCodes::COMPARE_MIXED_SCOPE => 'The selected governed subjects span multiple compare strategy families and must be narrowed before compare can start.',
default => 'Reason: '.$reasonCode,
};
Notification::make()
->title('Cannot start comparison')
->body($message)
->danger()
->send();
return;
}
$run = $result['run'] ?? null;
if ($run instanceof OperationRun) {
$this->operationRunId = (int) $run->getKey();
}
$this->state = 'comparing';
OpsUxBrowserEvents::dispatchRunEnqueued($this);
OperationUxPresenter::queuedToast($run instanceof OperationRun ? (string) $run->type : OperationRunType::BaselineCompare->value)
->actions($run instanceof OperationRun ? [
Action::make('view_run')
->label('Open operation')
->url(OperationRunLinks::view($run, $tenant, $this->navigationContext())),
] : [])
->send();
});
return UiEnforcement::forAction($action)
->requireCapability(Capabilities::TENANT_SYNC)
->preserveDisabled()
->apply();
}
public function getFindingsUrl(): ?string
{
$tenant = $this->currentEnvironment();
if (! $tenant instanceof ManagedEnvironment) {
return null;
}
return FindingResource::getUrl('index', tenant: $tenant);
}
public function getRunUrl(): ?string
{
if ($this->operationRunId === null) {
return null;
}
$tenant = $this->currentEnvironment();
if (! $tenant instanceof ManagedEnvironment) {
return null;
}
return OperationRunLinks::view($this->operationRunId, $tenant);
}
public function openCompareMatrixUrl(): ?string
{
$profile = $this->resolveCompareMatrixProfile();
if (! $profile instanceof BaselineProfile) {
return null;
}
$url = BaselineProfileResource::compareMatrixUrl($profile);
$query = array_filter([
'subject_key' => $this->matrixSubjectKey,
], static fn (mixed $value): bool => $value !== null && $value !== '');
if ($query === []) {
return $url;
}
return $url.(str_contains($url, '?') ? '&' : '?').http_build_query($query);
}
private function governanceAggregate(ManagedEnvironment $tenant, BaselineCompareStats $stats): TenantGovernanceAggregate
{
/** @var TenantGovernanceAggregateResolver $resolver */
$resolver = app(TenantGovernanceAggregateResolver::class);
/** @var TenantGovernanceAggregate $aggregate */
$aggregate = $resolver->fromStats($tenant, $stats);
return $aggregate;
}
private function navigationContext(): ?CanonicalNavigationContext
{
if (! is_array($this->navigationContextPayload)) {
return CanonicalNavigationContext::fromRequest(request());
}
return CanonicalNavigationContext::fromPayload($this->navigationContextPayload);
}
private function resolveCompareMatrixProfile(): ?BaselineProfile
{
$tenant = $this->currentEnvironment();
if (! $tenant instanceof ManagedEnvironment) {
return null;
}
$candidateIds = array_values(array_filter([
$this->matrixBaselineProfileId,
$this->profileId,
], static fn (mixed $value): bool => is_int($value) && $value > 0));
foreach ($candidateIds as $profileId) {
$profile = BaselineProfile::query()
->whereKey($profileId)
->where('workspace_id', (int) $tenant->workspace_id)
->first();
if ($profile instanceof BaselineProfile) {
return $profile;
}
}
return null;
}
private function currentEnvironment(): ?ManagedEnvironment
{
if ($this->scopedEnvironmentId !== null) {
$tenant = ManagedEnvironment::query()->whereKey($this->scopedEnvironmentId)->first();
return $tenant instanceof ManagedEnvironment && static::hasEnvironmentAccess($tenant)
? $tenant
: null;
}
$tenant = static::resolveRouteOwnedEnvironment();
return $tenant instanceof ManagedEnvironment && static::hasEnvironmentAccess($tenant)
? $tenant
: null;
}
protected static function resolveRouteOwnedEnvironment(ManagedEnvironment|string|null $environment = null): ?ManagedEnvironment
{
if ($environment instanceof ManagedEnvironment) {
return $environment;
}
if (is_string($environment) && $environment !== '') {
return ManagedEnvironment::query()
->where('slug', $environment)
->first();
}
$routeEnvironment = request()->route('environment');
if ($routeEnvironment instanceof ManagedEnvironment) {
return $routeEnvironment;
}
if (is_string($routeEnvironment) && $routeEnvironment !== '') {
return ManagedEnvironment::query()
->where('slug', $routeEnvironment)
->first();
}
return static::resolveRefererOwnedEnvironment();
}
private static function hasEnvironmentAccess(?ManagedEnvironment $environment): bool
{
$user = auth()->user();
if (! $environment instanceof ManagedEnvironment || ! $user instanceof User) {
return false;
}
$routeWorkspace = request()->route('workspace');
if ($routeWorkspace instanceof Workspace && (int) $routeWorkspace->getKey() !== (int) $environment->workspace_id) {
return false;
}
if (is_string($routeWorkspace) && $routeWorkspace !== '') {
$workspace = $environment->workspace instanceof Workspace
? $environment->workspace
: $environment->workspace()->first();
if (! $workspace instanceof Workspace) {
return false;
}
if ($routeWorkspace !== static::workspaceRouteKey($workspace) && $routeWorkspace !== (string) $workspace->getKey()) {
return false;
}
}
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
if ($workspaceId !== null && (int) $workspaceId !== (int) $environment->workspace_id) {
return false;
}
/** @var CapabilityResolver $resolver */
$resolver = app(CapabilityResolver::class);
return $resolver->isMember($user, $environment)
&& $resolver->can($user, $environment, Capabilities::TENANT_VIEW);
}
/**
* @param array<mixed> $parameters
*/
private static function resolveAdminUrlEnvironment(array $parameters, ?Model $tenant = null): ?ManagedEnvironment
{
$parameterEnvironment = $parameters['tenant'] ?? $parameters['environment'] ?? null;
if ($parameterEnvironment instanceof ManagedEnvironment) {
return $parameterEnvironment;
}
if ($tenant instanceof ManagedEnvironment) {
return $tenant;
}
$routeEnvironment = static::resolveRouteOwnedEnvironment();
if ($routeEnvironment instanceof ManagedEnvironment) {
return $routeEnvironment;
}
$filamentTenant = Filament::getTenant();
return $filamentTenant instanceof ManagedEnvironment ? $filamentTenant : null;
}
/**
* @param array<mixed> $parameters
*/
private static function resolveAdminUrlWorkspace(ManagedEnvironment $environment, array $parameters): Workspace|string|int|null
{
$workspace = $parameters['workspace'] ?? null;
if ($workspace instanceof Workspace || is_string($workspace) || is_int($workspace)) {
return $workspace;
}
$environmentWorkspace = $environment->workspace;
if ($environmentWorkspace instanceof Workspace) {
return $environmentWorkspace;
}
return $environment->workspace()->first();
}
/**
* @param array<mixed> $parameters
* @return array<mixed>
*/
private static function withoutLegacyScopeQuery(array $parameters): array
{
foreach (self::LEGACY_SCOPE_QUERY_KEYS as $key) {
unset($parameters[$key]);
}
return $parameters;
}
private static function workspaceRouteKey(Workspace $workspace): string
{
$slug = $workspace->getAttribute('slug');
return is_string($slug) && $slug !== ''
? $slug
: (string) $workspace->getKey();
}
private static function resolveRefererOwnedEnvironment(): ?ManagedEnvironment
{
$referer = request()->headers->get('referer');
if (! is_string($referer) || $referer === '') {
return null;
}
$path = parse_url($referer, PHP_URL_PATH);
if (! is_string($path)) {
return null;
}
if (preg_match('#^/admin/workspaces/([^/]+)/environments/([^/]+)/baseline-compare$#', $path, $matches) !== 1) {
return null;
}
$workspaceRouteKey = rawurldecode($matches[1]);
$environmentRouteKey = rawurldecode($matches[2]);
$environment = ManagedEnvironment::query()
->where('slug', $environmentRouteKey)
->first();
if (! $environment instanceof ManagedEnvironment) {
return null;
}
$workspace = $environment->workspace;
if (! $workspace instanceof Workspace) {
$workspace = $environment->workspace()->first();
}
if (! $workspace instanceof Workspace) {
return null;
}
if ($workspaceRouteKey !== static::workspaceRouteKey($workspace) && $workspaceRouteKey !== (string) $workspace->getKey()) {
return null;
}
return $environment;
}
}