diff --git a/apps/platform/app/Filament/Pages/BaselineCompareLanding.php b/apps/platform/app/Filament/Pages/BaselineCompareLanding.php index 179bbbc4..fabfe7a3 100644 --- a/apps/platform/app/Filament/Pages/BaselineCompareLanding.php +++ b/apps/platform/app/Filament/Pages/BaselineCompareLanding.php @@ -7,6 +7,7 @@ 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; @@ -21,7 +22,9 @@ 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; @@ -385,6 +388,13 @@ protected function getViewData(): array '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(), + 'availableCompareInputs' => $this->availableCompareInputs(), + 'assignmentUnlocks' => $this->assignmentUnlocks(), + 'diagnosticsDisclosure' => $this->diagnosticsDisclosure($hasEvidenceGapDiagnostics), 'hasCoverageWarnings' => $hasCoverageWarnings, 'evidenceGapsCountValue' => $evidenceGapsCountValue, 'hasEvidenceGaps' => $hasEvidenceGaps, @@ -404,6 +414,444 @@ protected function getViewData(): array ]; } + /** + * @return array + */ + 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), + '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' => 'Compare unavailable', + 'invalid_scope' => 'Compare unavailable', + 'comparing' => 'Compare running', + 'failed' => 'Compare failed', + 'idle' => 'Compare available', + 'ready' => $findingsCount > 0 + ? 'Drift requires review' + : ($hasWarnings ? 'Evidence requires review' : 'No confirmed drift'), + default => 'Compare unavailable', + }; + } + + private function decisionReason(string $state): string + { + $reason = trim((string) ($this->reasonMessage ?? '')); + + if ($reason !== '') { + return $reason; + } + + $message = trim((string) ($this->message ?? '')); + + if ($message !== '') { + return $message; + } + + return match ($state) { + 'no_assignment' => 'No baseline profile is assigned to this environment.', + 'no_snapshot' => 'No complete baseline snapshot is available for comparison.', + 'invalid_scope' => 'The assigned baseline scope cannot be compared safely.', + 'failed' => $this->failureReason ?: 'The latest compare run failed.', + 'comparing' => 'The comparison is still running.', + 'idle' => 'The assigned baseline can be compared against current observed inventory.', + 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 compare cannot be used for governance decisions until an assignment exists. Compare trust is unavailable until a baseline assignment exists. No usable drift result is available yet.'; + } + + if ($state === 'no_snapshot' || $state === 'invalid_scope') { + return 'Drift decisions stay unavailable until the baseline source is usable.'; + } + + if ($state === 'failed') { + return 'Review operation proof before retrying or treating the latest compare as evidence.'; + } + + if ($state === 'comparing') { + return 'Wait for operation proof before acting on drift or evidence state.'; + } + + if ($findingsCount > 0) { + return 'Review drift findings before presenting this environment as aligned to baseline.'; + } + + if ($hasCoverageWarnings || $hasEvidenceGaps) { + return 'Zero findings must not be treated as an all-clear while coverage or evidence gaps remain.'; + } + + return 'The latest compare shows no confirmed drift for the assigned baseline profile.'; + } + + /** + * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?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 ($state === 'ready' && $findingsCount > 0 && $this->getFindingsUrl() !== null) { + return [ + 'actionLabel' => 'Review drift findings', + 'actionUrl' => $this->getFindingsUrl(), + 'actionDisabled' => false, + 'helperText' => null, + ]; + } + + if (in_array($state, ['ready', 'failed'], true) && $this->getRunUrl() !== null) { + return [ + 'actionLabel' => 'Open operation proof', + 'actionUrl' => $this->getRunUrl(), + 'actionDisabled' => false, + 'helperText' => null, + ]; + } + + if ($state === 'idle') { + return [ + 'actionLabel' => 'Run compare from the page header', + 'actionUrl' => null, + 'actionDisabled' => true, + 'helperText' => 'The existing confirmed Compare now action remains in the page header.', + ]; + } + + 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> + */ + 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> + */ + private function proofPanelItems(bool $hasWarnings, bool $hasCoverageWarnings, bool $hasEvidenceGaps): array + { + if ($this->state === 'no_assignment') { + return []; + } + + return [ + [ + 'key' => 'assigned_baseline', + 'label' => 'Assigned baseline', + 'value' => $this->profileName ?? 'Baseline not assigned', + 'tone' => $this->profileName !== null ? 'success' : 'warning', + 'description' => $this->snapshotId !== null ? 'Snapshot #'.$this->snapshotId : 'No complete baseline snapshot is linked.', + 'actionLabel' => $this->openCompareMatrixUrl() !== null ? 'Open compare matrix' : null, + 'actionUrl' => $this->openCompareMatrixUrl(), + ], + [ + 'key' => 'compare_trust', + 'label' => 'Compare trust', + 'value' => $this->compareTrustLabel($hasWarnings), + 'tone' => $hasWarnings ? 'warning' : ($this->state === 'ready' ? 'success' : 'gray'), + 'description' => $this->coverageDescription($hasCoverageWarnings, $hasEvidenceGaps), + 'actionLabel' => $this->getRunUrl() !== null ? 'Open operation proof' : null, + 'actionUrl' => $this->getRunUrl(), + ], + [ + 'key' => 'drift_impact', + 'label' => 'Drift impact', + 'value' => $this->driftImpactLabel(), + 'tone' => ((int) ($this->findingsCount ?? 0)) > 0 ? 'warning' : 'gray', + 'description' => 'Findings and evidence gaps are reviewed before raw details.', + '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' => 'Evidence gaps and coverage limits stay visible before diagnostics.', + 'actionLabel' => null, + 'actionUrl' => null, + ], + ]; + } + + /** + * @return list> + */ + private function compareReadinessFlow(): array + { + if ($this->state !== 'no_assignment') { + return []; + } + + $environmentSnapshotState = $this->hasEnvironmentSnapshot() ? 'Available' : 'Unavailable'; + + return [ + [ + 'label' => 'Baseline assigned', + 'state' => 'Missing', + 'tone' => 'warning', + 'description' => 'No baseline is assigned to this environment.', + ], + [ + 'label' => 'Baseline snapshot', + 'state' => 'Unavailable', + 'tone' => 'gray', + 'description' => 'No baseline snapshot is linked.', + ], + [ + 'label' => 'Environment snapshot', + 'state' => $environmentSnapshotState, + 'tone' => $environmentSnapshotState === 'Available' ? 'success' : 'gray', + 'description' => 'Environment snapshot state is required for compare.', + ], + [ + 'label' => 'Compare run', + 'state' => 'Unavailable', + 'tone' => 'gray', + 'description' => 'Compare cannot run until required inputs exist.', + ], + [ + 'label' => 'Decision output', + 'state' => 'Unavailable', + 'tone' => 'gray', + 'description' => 'No drift decision output is available yet.', + ], + ]; + } + + /** + * @return list> + */ + private function availableCompareInputs(): array + { + if ($this->state !== 'no_assignment') { + return []; + } + + $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 + */ + 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', + 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 gaps need review'; + } + + if ($hasCoverageWarnings) { + return 'Coverage warning recorded'; + } + + if ($this->operationRunId !== null) { + return 'Operation proof available'; + } + + return 'Operation proof unavailable'; + } + + /** + * @return array + */ + 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. * diff --git a/apps/platform/app/Filament/Widgets/Dashboard/EnvironmentDashboardOverview.php b/apps/platform/app/Filament/Widgets/Dashboard/EnvironmentDashboardOverview.php index 77bb9149..abe1db16 100644 --- a/apps/platform/app/Filament/Widgets/Dashboard/EnvironmentDashboardOverview.php +++ b/apps/platform/app/Filament/Widgets/Dashboard/EnvironmentDashboardOverview.php @@ -39,10 +39,35 @@ protected function getViewData(): array 'headline' => __('localization.dashboard.overview.tenant_context_unavailable_headline'), 'summary' => __('localization.dashboard.overview.tenant_context_unavailable_summary'), ], + 'readinessDecision' => [ + 'question' => 'Is this environment ready, blocked, stale, or requiring review?', + 'statusLabel' => 'Status', + 'status' => __('localization.dashboard.overview.status_unavailable'), + 'tone' => 'gray', + 'reasonLabel' => 'Reason', + 'reason' => __('localization.dashboard.overview.tenant_context_unavailable_headline'), + 'impactLabel' => 'Impact', + 'impact' => __('localization.dashboard.overview.tenant_context_unavailable_summary'), + 'proofLabel' => 'Readiness proof', + 'proof' => 'Evidence, operation, review, provider, backup, and baseline signals are summarized before diagnostics.', + 'nextActionLabel' => 'Next action', + 'actionLabel' => 'Review readiness proof', + 'actionUrl' => null, + 'actionDisabled' => true, + 'helperText' => 'No single repo-real follow-up is currently available.', + ], 'kpis' => [], 'recommendedActions' => [], 'governanceStatus' => [], 'readinessCards' => [], + 'readinessDimensions' => [], + 'readinessProofPanel' => [], + 'supportingSignals' => [], + 'diagnosticsDisclosure' => [ + 'label' => 'Diagnostics - Collapsed', + 'summary' => 'Support diagnostics stay closed by default and require the existing diagnostics capability.', + 'tone' => 'gray', + ], 'activeOperationSummary' => null, 'recentOperations' => [], 'pollingInterval' => null, diff --git a/apps/platform/app/Models/ManagedEnvironment.php b/apps/platform/app/Models/ManagedEnvironment.php index dd4d3454..cb6e44dc 100644 --- a/apps/platform/app/Models/ManagedEnvironment.php +++ b/apps/platform/app/Models/ManagedEnvironment.php @@ -353,9 +353,7 @@ public function roleMappings(): HasMany public function getFilamentName(): string { - $environment = strtoupper((string) ($this->kind ?? 'managed_environment')); - - return ($this->display_name ?: $this->name)." ({$environment})"; + return (string) ($this->display_name ?: $this->name ?: $this->external_id ?: 'Environment #'.$this->getKey()); } public function users(): BelongsToMany diff --git a/apps/platform/app/Support/Baselines/BaselineCompareSummaryAssessor.php b/apps/platform/app/Support/Baselines/BaselineCompareSummaryAssessor.php index 83363f65..2fbbb307 100644 --- a/apps/platform/app/Support/Baselines/BaselineCompareSummaryAssessor.php +++ b/apps/platform/app/Support/Baselines/BaselineCompareSummaryAssessor.php @@ -242,7 +242,7 @@ private function headline( }, BaselineCompareSummaryAssessment::STATE_IN_PROGRESS => 'Baseline compare is in progress.', default => match ($stats->state) { - 'no_assignment' => 'This environment does not have an assigned baseline yet.', + 'no_assignment' => 'Baseline assignment is missing for this environment.', 'no_snapshot' => 'The current baseline snapshot is not available for compare.', 'idle' => 'A current baseline compare result is not available yet.', default => 'A usable baseline compare result is not currently available.', diff --git a/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummary.php b/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummary.php index 0d61e5d7..80589a38 100644 --- a/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummary.php +++ b/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummary.php @@ -7,22 +7,32 @@ final readonly class EnvironmentDashboardSummary { /** - * @param array{workspace:string,tenant:string,provider:?string,providerKey:?string,latestActivity:?string} $context + * @param array{workspace:string,tenant:string,provider:?string,providerKey:?string,latestActivity:?string} $context * @param array{status:string,tone:string,headline:string,summary:string} $posture + * @param array $readinessDecision * @param list> $kpis * @param list> $recommendedActions * @param list> $governanceStatus * @param list> $readinessCards + * @param list> $readinessDimensions + * @param list> $readinessProofPanel + * @param list> $supportingSignals + * @param array $diagnosticsDisclosure * @param array|null $activeOperationSummary * @param list> $recentOperations */ public function __construct( public array $context, public array $posture, + public array $readinessDecision, public array $kpis, public array $recommendedActions, public array $governanceStatus, public array $readinessCards, + public array $readinessDimensions, + public array $readinessProofPanel, + public array $supportingSignals, + public array $diagnosticsDisclosure, public ?array $activeOperationSummary, public array $recentOperations, public ?string $pollingInterval, @@ -30,13 +40,18 @@ public function __construct( /** * @return array{ - * context: array{workspace:string,tenant:string,provider:?string,providerKey:?string,latestActivity:?string}, + * context: array{workspace:string,tenant:string,provider:?string,providerKey:?string,latestActivity:?string}, * posture: array{status:string,tone:string,headline:string,summary:string}, + * readinessDecision: array, * kpis: list>, * recommendedActions: list>, * governanceStatus: list>, * readinessCards: list>, - * activeOperationSummary: array|null, + * readinessDimensions: list>, + * readinessProofPanel: list>, + * supportingSignals: list>, + * diagnosticsDisclosure: array, + * activeOperationSummary: array|null, * recentOperations: list>, * pollingInterval: ?string, * } @@ -46,13 +61,18 @@ public function toArray(): array return [ 'context' => $this->context, 'posture' => $this->posture, + 'readinessDecision' => $this->readinessDecision, 'kpis' => $this->kpis, 'recommendedActions' => $this->recommendedActions, 'governanceStatus' => $this->governanceStatus, 'readinessCards' => $this->readinessCards, + 'readinessDimensions' => $this->readinessDimensions, + 'readinessProofPanel' => $this->readinessProofPanel, + 'supportingSignals' => $this->supportingSignals, + 'diagnosticsDisclosure' => $this->diagnosticsDisclosure, 'activeOperationSummary' => $this->activeOperationSummary, 'recentOperations' => $this->recentOperations, 'pollingInterval' => $this->pollingInterval, ]; } -} \ No newline at end of file +} diff --git a/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php b/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php index 092707ec..b6ef1c93 100644 --- a/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php +++ b/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php @@ -101,6 +101,34 @@ public function build(ManagedEnvironment $tenant, ?User $user = null): Environme 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); + $summary = new EnvironmentDashboardSummary( context: [ 'workspace' => (string) ($tenant->workspace?->name ?? $this->overviewText('context_workspace')), @@ -115,16 +143,15 @@ public function build(ManagedEnvironment $tenant, ?User $user = null): Environme recentOperations: $recentOperations, ), ], - posture: $this->posture( - aggregate: $aggregate, - backupHealth: $backupHealth, - recoveryEvidence: $recoveryEvidence, - requiredPermissions: $requiredPermissions, - recommendedActions: $recommendedActions, - ), + posture: $posture, + readinessDecision: $this->readinessDecision($posture, $recommendedActions), kpis: $this->kpis($tenant, $user, $aggregate, $requiredPermissions), recommendedActions: $recommendedActions, - governanceStatus: $this->governanceStatus( + 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, @@ -132,18 +159,10 @@ public function build(ManagedEnvironment $tenant, ?User $user = null): Environme requiredPermissions: $requiredPermissions, latestReview: $latestReview, latestEvidenceSnapshot: $latestEvidenceSnapshot, + activeOperationSummary: $activeOperationSummary, ), - readinessCards: $this->readinessCards( - tenant: $tenant, - user: $user, - primaryProviderConnection: $primaryProviderConnection, - requiredPermissions: $requiredPermissions, - latestReview: $latestReview, - latestReviewPack: $latestReviewPack, - latestEvidenceSnapshot: $latestEvidenceSnapshot, - exceptionStats: $exceptionStats, - ), - activeOperationSummary: $this->activeOperationSummary($tenant, $user), + diagnosticsDisclosure: $this->diagnosticsDisclosure(), + activeOperationSummary: $activeOperationSummary, recentOperations: $this->recentOperationCards($tenant, $recentOperations), pollingInterval: ActiveRuns::pollingIntervalForTenant($tenant), ); @@ -153,6 +172,280 @@ public function build(ManagedEnvironment $tenant, ?User $user = null): Environme return $summary; } + /** + * @param array{status:string,tone:string,headline:string,summary:string} $posture + * @param list> $recommendedActions + * @return array + */ + private function readinessDecision(array $posture, array $recommendedActions): array + { + $primaryAction = $recommendedActions[0] ?? null; + + return [ + 'question' => 'Is this environment ready, blocked, stale, or requiring review?', + 'statusLabel' => 'Status', + 'status' => (string) ($posture['status'] ?? $this->overviewText('status_unavailable')), + 'tone' => (string) ($posture['tone'] ?? 'gray'), + 'reasonLabel' => 'Reason', + 'reason' => is_array($primaryAction) && is_string($primaryAction['reason'] ?? null) + ? (string) $primaryAction['reason'] + : (string) ($posture['headline'] ?? $this->overviewText('tenant_context_unavailable_headline')), + 'impactLabel' => 'Impact', + 'impact' => is_array($primaryAction) && is_string($primaryAction['impact'] ?? null) + ? (string) $primaryAction['impact'] + : (string) ($posture['summary'] ?? $this->overviewText('tenant_context_unavailable_summary')), + 'proofLabel' => 'Readiness proof', + 'proof' => 'Evidence, operation, review, provider, backup, and baseline signals are summarized before diagnostics.', + 'nextActionLabel' => 'Next action', + 'actionLabel' => is_array($primaryAction) && is_string($primaryAction['actionLabel'] ?? null) + ? (string) $primaryAction['actionLabel'] + : 'Review readiness proof', + 'actionUrl' => is_array($primaryAction) && is_string($primaryAction['actionUrl'] ?? null) + ? (string) $primaryAction['actionUrl'] + : null, + 'actionDisabled' => ! is_array($primaryAction) || blank($primaryAction['actionUrl'] ?? null), + 'helperText' => is_array($primaryAction) && is_string($primaryAction['helperText'] ?? null) + ? (string) $primaryAction['helperText'] + : 'No single repo-real follow-up is currently available.', + ]; + } + + /** + * @param list> $governanceStatus + * @param list> $readinessCards + * @param array|null $activeOperationSummary + * @return list> + */ + 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> $governanceStatus + * @param list> $readinessCards + * @param array|null $activeOperationSummary + * @return list> + */ + 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 $requiredPermissions + * @param array|null $activeOperationSummary + * @return list> + */ + 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 $action + * @return array + */ + 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 + */ + 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; @@ -760,7 +1053,7 @@ private function governanceStatus( 'key' => 'baseline_compare', 'label' => $this->overviewText('governance_baseline_compare_label'), 'icon' => $this->governanceStatusIcon('baseline_compare'), - 'value' => $aggregate->headline, + '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')), @@ -804,6 +1097,22 @@ private function governanceStatus( ]; } + 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 $requiredPermissions * @param array{active:int,expiring:int,expired:int,pending:int,total:int} $exceptionStats diff --git a/apps/platform/lang/de/baseline-compare.php b/apps/platform/lang/de/baseline-compare.php index 45a88639..1b81aba8 100644 --- a/apps/platform/lang/de/baseline-compare.php +++ b/apps/platform/lang/de/baseline-compare.php @@ -18,7 +18,7 @@ 'badge_evidence_gaps' => 'Evidence Gaps: :count', 'evidence_gaps_tooltip' => 'Wichtigste Gaps: :summary', 'evidence_gap_details_heading' => 'Evidence-Gap-Details', - 'evidence_gap_details_description' => 'Durchsuchen Sie aufgezeichnete Gap-Subjekte nach Grund, Governed Subject, Subjektklasse, Ergebnis, nächster Aktion oder Subject Key, bevor Sie Rohdiagnosen verwenden.', + 'evidence_gap_details_description' => 'Durchsuchen Sie aufgezeichnete Gap-Subjekte nach Grund, Governed Subject, Subjektklasse, Ergebnis, nächster Aktion oder Subject Key, bevor Sie Supportdetails verwenden.', 'evidence_gap_search_label' => 'Gap-Details suchen', 'evidence_gap_search_placeholder' => 'Nach Grund, Typ, Klasse, Ergebnis, Aktion oder Subject Key suchen', 'evidence_gap_search_help' => 'Filtert über Grund, Governed Subject, Subjektklasse, Ergebnis, nächste Aktion und Subject Key.', @@ -39,12 +39,12 @@ 'evidence_gap_bucket_operational' => ':count operativ', 'evidence_gap_bucket_transient' => ':count temporär', 'evidence_gap_missing_details_title' => 'Für diesen Run wurden keine Detailzeilen aufgezeichnet', - 'evidence_gap_missing_details_body' => 'Evidence Gaps wurden für diesen Compare Run gezählt, aber Details auf Subjektebene wurden nicht gespeichert. Prüfen Sie Rohdiagnosen oder wiederholen Sie den Vergleich.', + 'evidence_gap_missing_details_body' => 'Evidence Gaps wurden für diesen Compare Run gezählt, aber Details auf Subjektebene wurden nicht gespeichert. Prüfen Sie Supportdetails oder wiederholen Sie den Vergleich.', 'evidence_gap_missing_reason_body' => ':count betroffene Subjekte wurden für diesen Grund gezählt, aber Detailzeilen wurden nicht aufgezeichnet.', 'evidence_gap_legacy_title' => 'Legacy-Development-Gap-Payload erkannt', 'evidence_gap_legacy_body' => 'Dieser Run verwendet noch die retired breite Grundform. Erzeugen Sie den Run neu oder bereinigen Sie alte lokale Development-Payloads.', 'evidence_gap_diagnostics_heading' => 'Baseline-Compare-Evidence', - 'evidence_gap_diagnostics_description' => 'Rohdiagnosen bleiben für Support und tiefere Fehlersuche nach Operator-Zusammenfassung und Detailansicht verfügbar.', + 'evidence_gap_diagnostics_description' => 'Supportdiagnosen bleiben nach Operator-Zusammenfassung und Detailansicht verfügbar.', 'evidence_gap_policy_type' => 'Governed Subject', 'evidence_gap_subject_class' => 'Subjektklasse', 'evidence_gap_outcome' => 'Ergebnis', @@ -64,8 +64,8 @@ 'failed_title' => 'Vergleich fehlgeschlagen', 'failed_body_default' => 'Der letzte Baseline-Vergleich ist fehlgeschlagen. Prüfen Sie die Run-Details oder wiederholen Sie ihn.', 'critical_drift_title' => 'Kritischer Drift erkannt', - 'critical_drift_body' => 'Der aktuelle Tenant-Zustand weicht von Baseline :profile ab. :count High-Severity :findings erfordern sofortige Aufmerksamkeit.', - 'empty_no_tenant' => 'Kein Tenant ausgewählt', + 'critical_drift_body' => 'Der aktuelle Environment-Zustand weicht von Baseline :profile ab. :count High-Severity :findings erfordern sofortige Aufmerksamkeit.', + 'empty_no_tenant' => 'Kein Environment ausgewählt', 'empty_no_assignment' => 'Keine Baseline zugewiesen', 'empty_no_snapshot' => 'Kein Snapshot verfügbar', 'findings_description' => 'Die Tenant-Konfiguration weicht vom Baseline-Profil ab.', diff --git a/apps/platform/lang/de/localization.php b/apps/platform/lang/de/localization.php index 7c3f455b..0732676e 100644 --- a/apps/platform/lang/de/localization.php +++ b/apps/platform/lang/de/localization.php @@ -276,7 +276,7 @@ 'readiness_current_review_findings_progress_label' => 'Findings mit Ergebnis', 'readiness_current_review_completion_progress_label' => 'Review-Fortschritt', 'readiness_customer_safe_output_title' => 'Kundensichere Ausgabe', - 'readiness_customer_safe_output_empty_status' => 'Keine kundensichere Ausgabe verfügbar', + 'readiness_customer_safe_output_empty_status' => 'Keine kundensichere Ausgabe', 'readiness_customer_safe_output_empty_description' => 'Erstellen Sie ein Review-Paket, sobald Review und Nachweise für die Übergabe bereit sind.', 'readiness_customer_safe_output_evidence_label' => 'Nachweis-Snapshot', 'readiness_customer_safe_output_review_pack_label' => 'Review-Paket', diff --git a/apps/platform/lang/en/baseline-compare.php b/apps/platform/lang/en/baseline-compare.php index ff54b1e5..4759bf0b 100644 --- a/apps/platform/lang/en/baseline-compare.php +++ b/apps/platform/lang/en/baseline-compare.php @@ -30,7 +30,7 @@ 'badge_evidence_gaps' => 'Evidence gaps: :count', 'evidence_gaps_tooltip' => 'Top gaps: :summary', 'evidence_gap_details_heading' => 'Evidence gap details', - 'evidence_gap_details_description' => 'Search recorded gap subjects by reason, governed subject, subject class, outcome, next action, or subject key before falling back to raw diagnostics.', + 'evidence_gap_details_description' => 'Search recorded gap subjects by reason, governed subject, subject class, outcome, next action, or subject key before falling back to support details.', 'evidence_gap_search_label' => 'Search gap details', 'evidence_gap_search_placeholder' => 'Search by reason, type, class, outcome, action, or subject key', 'evidence_gap_search_help' => 'Filter matches across reason, governed subject, subject class, outcome, next action, and subject key.', @@ -51,12 +51,12 @@ 'evidence_gap_bucket_operational' => ':count operational', 'evidence_gap_bucket_transient' => ':count transient', 'evidence_gap_missing_details_title' => 'Detailed rows were not recorded for this run', - 'evidence_gap_missing_details_body' => 'Evidence gaps were counted for this compare run, but subject-level detail was not stored. Review the raw diagnostics below or rerun the comparison for fresh detail.', + 'evidence_gap_missing_details_body' => 'Evidence gaps were counted for this compare run, but subject-level detail was not stored. Review support details or rerun the comparison for fresh detail.', 'evidence_gap_missing_reason_body' => ':count affected subjects were counted for this reason, but detailed rows were not recorded for this run.', 'evidence_gap_legacy_title' => 'Legacy development gap payload detected', 'evidence_gap_legacy_body' => 'This run still uses the retired broad reason shape. Regenerate the run or purge stale local development payloads before treating the gap details as operator-safe.', 'evidence_gap_diagnostics_heading' => 'Baseline compare evidence', - 'evidence_gap_diagnostics_description' => 'Raw diagnostics remain available for support and deep troubleshooting after the operator summary and searchable detail.', + 'evidence_gap_diagnostics_description' => 'Support diagnostics remain available after the operator summary and searchable detail.', 'evidence_gap_policy_type' => 'Governed subject', 'evidence_gap_subject_class' => 'Subject class', 'evidence_gap_outcome' => 'Outcome', @@ -86,10 +86,10 @@ // Critical drift banner 'critical_drift_title' => 'Critical Drift Detected', - 'critical_drift_body' => 'The current tenant state deviates from baseline :profile. :count high-severity :findings require immediate attention.', + 'critical_drift_body' => 'The current environment state deviates from baseline :profile. :count high-severity :findings require immediate attention.', // Empty states - 'empty_no_tenant' => 'No Tenant Selected', + 'empty_no_tenant' => 'No Environment Selected', 'empty_no_assignment' => 'No Baseline Assigned', 'empty_no_snapshot' => 'No Snapshot Available', diff --git a/apps/platform/lang/en/localization.php b/apps/platform/lang/en/localization.php index 135838c9..ecbda3b5 100644 --- a/apps/platform/lang/en/localization.php +++ b/apps/platform/lang/en/localization.php @@ -276,7 +276,7 @@ 'readiness_current_review_findings_progress_label' => 'Findings with outcome', 'readiness_current_review_completion_progress_label' => 'Review completion', 'readiness_customer_safe_output_title' => 'Customer-safe output', - 'readiness_customer_safe_output_empty_status' => 'No customer-safe output available', + 'readiness_customer_safe_output_empty_status' => 'No customer-safe output', 'readiness_customer_safe_output_empty_description' => 'Generate a review pack once review and evidence are ready for handoff.', 'readiness_customer_safe_output_evidence_label' => 'Evidence snapshot', 'readiness_customer_safe_output_review_pack_label' => 'Review pack', diff --git a/apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php b/apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php index f6a4922f..a385d614 100644 --- a/apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php +++ b/apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php @@ -27,8 +27,240 @@ 'in_progress' => 'In progress', default => 'Unavailable', }; + $showCompareExplanation = $explanation !== null && $state !== 'no_assignment'; + $baselineCompareStatusBadgeClasses = static fn (?string $tone): string => 'inline-flex items-center rounded-md border px-2 py-0.5 text-left text-xs font-medium leading-5 whitespace-normal break-words '.match ($tone) { + 'danger' => 'border-danger-200 bg-danger-50 text-danger-700 dark:border-danger-800 dark:bg-danger-500/10 dark:text-danger-300', + 'success' => 'border-success-200 bg-success-50 text-success-700 dark:border-success-800 dark:bg-success-500/10 dark:text-success-300', + 'warning' => 'border-warning-200 bg-warning-50 text-warning-700 dark:border-warning-800 dark:bg-warning-500/10 dark:text-warning-300', + 'info' => 'border-info-200 bg-info-50 text-info-700 dark:border-info-800 dark:bg-info-500/10 dark:text-info-300', + 'primary' => 'border-primary-200 bg-primary-50 text-primary-700 dark:border-primary-800 dark:bg-primary-500/10 dark:text-primary-300', + default => 'border-gray-200 bg-gray-50 text-gray-700 dark:border-white/10 dark:bg-white/5 dark:text-gray-200', + }; @endphp + +
+
+
+

+ {{ $decisionCard['question'] ?? 'Which baseline drift requires action?' }} +

+ + {{ $decisionCard['status'] ?? 'Compare unavailable' }} + +
+ +
+
+
{{ $decisionCard['statusLabel'] ?? 'Status' }}
+
{{ $decisionCard['status'] ?? 'Compare unavailable' }}
+
+ +
+
{{ $decisionCard['reasonLabel'] ?? 'Reason' }}
+
{{ $decisionCard['reason'] ?? 'Compare state is unavailable.' }}
+
+ +
+
{{ $decisionCard['impactLabel'] ?? 'Impact' }}
+
{{ $decisionCard['impact'] ?? 'No governance decision should rely on this compare state yet.' }}
+
+
+ + @if (! empty($decisionSummaryItems)) +
+ @foreach ($decisionSummaryItems as $item) +
+
{{ $item['label'] }}
+
{{ $item['value'] }}
+

{{ $item['description'] }}

+
+ @endforeach +
+ @endif +
+ + +
+
+ + @if (! empty($compareReadinessFlow)) + +
+
+

+ Compare readiness flow +

+

+ Baseline comparison needs an assigned baseline, linked snapshots, a compare run, and a decision output. +

+
+ +
    + @foreach ($compareReadinessFlow as $step) + @php + $isCurrentBlocker = $loop->first && ($step['state'] ?? null) === 'Missing'; + $stepCardClasses = $isCurrentBlocker + ? 'border-warning-300 bg-warning-50 shadow-sm dark:border-warning-700 dark:bg-warning-950/40' + : 'border-gray-200 bg-gray-50 dark:border-gray-800 dark:bg-gray-950/50'; + $stepNumberClasses = $isCurrentBlocker + ? 'border-warning-300 bg-warning-50 text-warning-700 dark:border-warning-700 dark:bg-warning-950/40 dark:text-warning-300' + : 'border-gray-200 bg-white text-gray-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300'; + @endphp + +
  1. +
    +
    + + {{ $loop->iteration }} + + +
    +
    + {{ $step['label'] }} +
    + +
    + + {{ $step['state'] }} + +
    +
    +
    + +

    + {{ $step['description'] }} +

    +
    + + @if (! $loop->last) + + @endif +
  2. + @endforeach +
+ +
+
+
+
+ Available inputs +
+

+ Repo-backed inputs only. +

+
+ +
+ @foreach ($availableCompareInputs as $input) +
+
+
+ {{ $input['label'] }} +
+

+ {{ $input['description'] }} +

+
+ + + {{ $input['state'] }} + +
+ @endforeach +
+
+ + @if (! empty($assignmentUnlocks)) +
+
+ What this unlocks after assignment +
+
    + @foreach ($assignmentUnlocks as $unlock) +
  • + + {{ $unlock }} +
  • + @endforeach +
+
+ @endif +
+
+
+ @endif + @if (filled($openCompareMatrixUrl ?? null))
@@ -85,7 +317,7 @@
@endif - @if ($explanation !== null) + @if ($showCompareExplanation)
@@ -423,15 +655,12 @@ class="w-fit" @endif {{-- State: No tenant / no assignment / no snapshot --}} - @if (in_array($state, ['no_tenant', 'no_assignment', 'no_snapshot'])) + @if (in_array($state, ['no_tenant', 'no_snapshot']))
@if ($state === 'no_tenant')
{{ __('baseline-compare.empty_no_tenant') }}
- @elseif ($state === 'no_assignment') - -
{{ __('baseline-compare.empty_no_assignment') }}
@elseif ($state === 'no_snapshot')
{{ __('baseline-compare.empty_no_snapshot') }}
@@ -585,7 +814,9 @@ class="w-fit" {{ __('baseline-compare.evidence_gap_diagnostics_description') }} - @include('filament.partials.json-viewer', ['value' => $baselineCompareDiagnostics]) +
+ Diagnostics are available for support review and stay outside the default operator view. +
@endif diff --git a/apps/platform/resources/views/filament/partials/context-bar.blade.php b/apps/platform/resources/views/filament/partials/context-bar.blade.php index 764c6e2a..c82a4118 100644 --- a/apps/platform/resources/views/filament/partials/context-bar.blade.php +++ b/apps/platform/resources/views/filament/partials/context-bar.blade.php @@ -22,9 +22,15 @@ ->values(); } + $environmentDisplayName = static function (ManagedEnvironment $environment): string { + $displayName = trim((string) ($environment->display_name ?: $environment->name ?: $environment->external_id ?: '')); + + return $displayName !== '' ? $displayName : 'Environment #'.$environment->getKey(); + }; + $currentEnvironment = $resolvedContext->tenant; $currentEnvironmentId = $currentEnvironment instanceof ManagedEnvironment ? (int) $currentEnvironment->getKey() : null; - $currentEnvironmentName = $currentEnvironment instanceof ManagedEnvironment ? $currentEnvironment->getFilamentName() : null; + $currentEnvironmentName = $currentEnvironment instanceof ManagedEnvironment ? $environmentDisplayName($currentEnvironment) : null; $lastEnvironmentId = $workspaceContext->lastEnvironmentId(request()); $canClearEnvironmentContext = $currentEnvironment instanceof ManagedEnvironment || $lastEnvironmentId !== null; @@ -188,7 +194,7 @@ class="fi-input fi-text-input w-full" @endforeach diff --git a/apps/platform/resources/views/filament/widgets/dashboard/baseline-compare-now.blade.php b/apps/platform/resources/views/filament/widgets/dashboard/baseline-compare-now.blade.php index a3f6e3b6..685a1221 100644 --- a/apps/platform/resources/views/filament/widgets/dashboard/baseline-compare-now.blade.php +++ b/apps/platform/resources/views/filament/widgets/dashboard/baseline-compare-now.blade.php @@ -66,7 +66,7 @@
-
No Baseline Assigned
+
Baseline not assigned
Assign a baseline profile to start monitoring drift.
diff --git a/apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php b/apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php index 3e6dab28..6272ff41 100644 --- a/apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php +++ b/apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php @@ -1,8 +1,12 @@ @php - $overviewSecondaryListStackClasses = 'flex flex-col gap-2'; - $overviewSecondaryListRowBaseClasses = 'min-w-0 rounded-xl border p-4 shadow-sm'; - $overviewSecondaryListRowSurfaceClasses = 'border-gray-200 bg-white/80 dark:border-white/10 dark:bg-white/5'; - $overviewSecondaryListInteractiveClasses = 'transition duration-150 hover:shadow-md hover:ring-1 hover:ring-gray-950/5 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50 dark:hover:ring-white/10'; + $tenantDashboardStatusBadgeClasses = static fn (?string $tone): string => 'inline-flex items-center rounded-md border px-2 py-0.5 text-left text-xs font-medium leading-5 whitespace-normal break-words '.match ($tone) { + 'danger' => 'border-danger-200 bg-danger-50 text-danger-700 dark:border-danger-800 dark:bg-danger-500/10 dark:text-danger-300', + 'success' => 'border-success-200 bg-success-50 text-success-700 dark:border-success-800 dark:bg-success-500/10 dark:text-success-300', + 'warning' => 'border-warning-200 bg-warning-50 text-warning-700 dark:border-warning-800 dark:bg-warning-500/10 dark:text-warning-300', + 'info' => 'border-info-200 bg-info-50 text-info-700 dark:border-info-800 dark:bg-info-500/10 dark:text-info-300', + 'primary' => 'border-primary-200 bg-primary-50 text-primary-700 dark:border-primary-800 dark:bg-primary-500/10 dark:text-primary-300', + default => 'border-gray-200 bg-gray-50 text-gray-700 dark:border-white/10 dark:bg-white/5 dark:text-gray-200', + }; @endphp
+ +
+
+
+

+ {{ $readinessDecision['question'] ?? 'Is this environment ready, blocked, stale, or requiring review?' }} +

+ + {{ $readinessDecision['status'] ?? ($posture['status'] ?? __('localization.dashboard.overview.status_unavailable')) }} + +
+ +
+
+
{{ $readinessDecision['statusLabel'] ?? 'Status' }}
+
{{ $readinessDecision['status'] ?? ($posture['status'] ?? __('localization.dashboard.overview.status_unavailable')) }}
+
+ +
+
{{ $readinessDecision['reasonLabel'] ?? 'Reason' }}
+
{{ $readinessDecision['reason'] ?? ($posture['headline'] ?? __('localization.dashboard.overview.tenant_context_unavailable_headline')) }}
+
+ +
+
{{ $readinessDecision['impactLabel'] ?? 'Impact' }}
+
{{ $readinessDecision['impact'] ?? ($posture['summary'] ?? __('localization.dashboard.overview.tenant_context_unavailable_summary')) }}
+
+
+
+ +
+
{{ $readinessDecision['nextActionLabel'] ?? 'Next action' }}
+
{{ $readinessDecision['actionLabel'] ?? 'Review readiness proof' }}
+ + @if (filled($readinessDecision['actionUrl'] ?? null)) + + {{ $readinessDecision['actionLabel'] ?? 'Review readiness proof' }} + + @else + + {{ $readinessDecision['actionLabel'] ?? 'Review readiness proof' }} + + @endif + + @if (filled($readinessDecision['helperText'] ?? null)) +

{{ $readinessDecision['helperText'] }}

+ @endif +
+
+
+ + +
+ @forelse ($readinessDimensions as $dimension) +
+
+
+
{{ $dimension['title'] ?? 'Readiness signal' }}
+
+
+ + {{ $dimension['status'] ?? __('localization.dashboard.overview.status_unavailable') }} + +
+

{{ $dimension['description'] ?? '' }}

+
+
+ @empty +
+ Readiness dimensions are unavailable until an environment context is selected. +
+ @endforelse +
+
+ + + Recommended next actions are derived from repo-backed blockers and proof gaps. + + @if ($recommendedActions === [])
{{ __('localization.dashboard.overview.empty_recommended_actions_headline') }}
@@ -65,240 +148,117 @@ class="h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500" @endif - - -
- @foreach ($governanceStatus as $status) - @php - $isGovernanceStatusInteractive = filled($status['actionUrl'] ?? null); - $governanceStatusClasses = $isGovernanceStatusInteractive - ? "{$overviewSecondaryListRowBaseClasses} {$overviewSecondaryListRowSurfaceClasses} {$overviewSecondaryListInteractiveClasses} flex items-start justify-between gap-4" - : "{$overviewSecondaryListRowBaseClasses} {$overviewSecondaryListRowSurfaceClasses} flex items-start justify-between gap-4"; - @endphp + + + Additional readiness signals used to explain the current recommendation. + - @if ($isGovernanceStatusInteractive) - -
-
- @if (filled($status['icon'] ?? null)) - - @endif - -
{{ $status['label'] }}
-
-
{{ $status['description'] }}
-
-
- {{ $status['value'] }} -
-
- @else -
-
-
- @if (filled($status['icon'] ?? null)) - - @endif - -
{{ $status['label'] }}
-
-
{{ $status['description'] }}
-
-
- {{ $status['value'] }} -
-
- @endif - @endforeach +
+ @if ($supportingSignals === []) +
+ Supporting signals are unavailable until an environment context is selected. +
+ @else +
+ + + + + + + + + + @foreach ($supportingSignals as $signal) + + + + + + @endforeach + +
SignalStateAction
+ {{ $signal['label'] ?? 'Readiness signal' }} + + + {{ $signal['value'] ?? __('localization.dashboard.overview.status_unavailable') }} + + + @if (filled($signal['actionLabel'] ?? null)) + @if (filled($signal['actionUrl'] ?? null) && ! ($signal['actionDisabled'] ?? false)) + + {{ $signal['actionLabel'] }} + + @else + + {{ $signal['actionLabel'] }} + + @endif + @endif +
+
+ @endif
- @if ($activeOperationSummary) -
-
-
-
-
-
{{ $activeOperationSummary['title'] }}
- {{ $activeOperationSummary['count'] }} -
-
- -
- - {{ $activeOperationSummary['secondaryActionLabel'] }} - -
-
- -
- @foreach ($activeOperationSummary['items'] ?? [] as $operation) -
-
-
-
- @if (filled($operation['icon'] ?? null)) - - @endif - -
{{ $operation['title'] }}
- - @if (filled($operation['attentionLabel'] ?? null)) - {{ $operation['attentionLabel'] }} - @endif -
- -

{{ $operation['outcomeSentence'] }}

- - @if (filled($operation['timingLabel'] ?? null)) -
{{ $operation['timingLabel'] }}
- @endif - -

{{ __('localization.dashboard.overview.label_reason') }}: {{ $operation['reason'] }}

-

{{ __('localization.dashboard.overview.label_impact') }}: {{ $operation['impact'] }}

-
- -
- - {{ $operation['primaryActionLabel'] }} - -
-
-
- @endforeach -
-
-
- @endif + +
+ + {{ $diagnosticsDisclosure['label'] ?? 'Diagnostics - Collapsed' }} + +

+ {{ $diagnosticsDisclosure['summary'] ?? 'Support diagnostics stay closed by default and require the existing diagnostics capability.' }} +

+
+
- @foreach ($readinessCards as $card) - @php - $cardMeta = array_values(array_filter($card['meta'] ?? [])); - $headline = $card['headline'] ?? null; - $cardProgress = array_values(array_filter($card['progress'] ?? [])); - @endphp -
-
-
-
{{ $card['title'] }}
- @if (filled($headline)) -
{{ $headline }}
- @else -
{{ $card['status'] }}
+ +
+ @foreach ($readinessProofPanel as $proof) +
+
+
+
{{ $proof['label'] ?? 'Proof path' }}
+
+
+ + {{ $proof['value'] ?? __('localization.dashboard.overview.status_unavailable') }} + +
+

{{ $proof['description'] ?? '' }}

+
+ + @if (filled($proof['actionLabel'] ?? null)) +
+ @if (filled($proof['actionUrl'] ?? null)) + + {{ $proof['actionLabel'] }} + + @else + + {{ $proof['actionLabel'] }} + + @endif +
@endif
- {{ $card['status'] }} -
+ @endforeach -

{{ $card['body'] }}

- - @if ($cardProgress !== []) -
- @foreach ($cardProgress as $progress) - @php - $progressBarColor = match ($progress['tone'] ?? 'primary') { - 'success' => 'var(--success-500)', - 'warning' => 'var(--warning-500)', - 'danger' => 'var(--danger-500)', - default => 'var(--primary-500)', - }; - @endphp - -
-
- {{ $progress['label'] }} - {{ $progress['valueLabel'] }} -
- -
-
-
-
- @endforeach -
- @endif - - @if ($cardMeta !== []) -
- @foreach ($cardMeta as $item) -
- {{ $item['label'] }} - {{ $item['value'] }} -
- @endforeach -
- @endif - - @if (filled($card['actionLabel'] ?? null)) -
- @if (filled($card['actionUrl'] ?? null)) - - {{ $card['actionLabel'] }} - - @else - - {{ $card['actionLabel'] }} - - @endif -
- @endif
- @endforeach +
-
\ No newline at end of file +
diff --git a/apps/platform/tests/Browser/Spec330EnvironmentDashboardBaselineCompareSmokeTest.php b/apps/platform/tests/Browser/Spec330EnvironmentDashboardBaselineCompareSmokeTest.php new file mode 100644 index 00000000..84ecd7b7 --- /dev/null +++ b/apps/platform/tests/Browser/Spec330EnvironmentDashboardBaselineCompareSmokeTest.php @@ -0,0 +1,234 @@ +browser()->timeout(60_000); + +it('Spec330 smokes environment dashboard and baseline compare decision surfaces', function (): void { + [$user, $environment] = spec330DecisionSurfaceFixture(); + spec330AuthenticateBrowser($this, $user, $environment); + + visit(ManagedEnvironmentLinks::viewUrl($environment)) + ->waitForText('Is this environment ready, blocked, stale, or requiring review?') + ->assertSee('Readiness proof') + ->assertSee('Readiness dimensions') + ->assertSee('Recommended next actions') + ->assertSee('Supporting signals') + ->assertSee('Additional readiness signals used to explain the current recommendation.') + ->assertSee('Signal') + ->assertSee('State') + ->assertSee('Action') + ->assertSee('Unavailable') + ->assertSee('Not ready') + ->assertSee('Absent') + ->assertSee('No active review') + ->assertSee('No customer-safe output') + ->assertSee('Operations follow-up') + ->assertSee('3 require review') + ->assertSee('Open operations hub') + ->assertSee('Diagnostics - Collapsed') + ->assertDontSee('Secondary context') + ->assertDontSee('Show operation details') + ->assertDontSee('View operation details') + ->assertDontSee('Ab...') + ->assertDontSee('Unava...') + ->assertDontSee('Not re...') + ->assertDontSee('No customer-saf...') + ->assertDontSee('Baseline assig...') + ->assertScript('document.querySelector("[data-testid=\"tenant-dashboard-secondary-context\"]") === null', true) + ->assertScript('document.querySelector("[data-testid=\"tenant-dashboard-operation-details\"]") === null', true) + ->assertScript('document.querySelector("[data-testid=\"tenant-dashboard-diagnostics\"]")?.open === false', true) + ->assertScript('document.querySelectorAll("[data-testid=\"tenant-dashboard-readiness-decision\"]").length === 1', true) + ->assertScript('document.querySelectorAll("[data-testid=\"tenant-dashboard-readiness-card\"]").length === 0', true) + ->assertScript('document.querySelectorAll("[data-testid=\"tenant-dashboard-supporting-signal\"]").length === 6', true) + ->assertScript('document.querySelectorAll("[data-testid=\"tenant-dashboard-operations-attention-item\"]").length === 0', true) + ->assertScript('Array.from(document.querySelectorAll("[data-testid=\"tenant-dashboard-status-badge\"]")).every((badge) => !badge.innerText.includes("...") && getComputedStyle(badge).overflow !== "hidden" && getComputedStyle(badge).textOverflow !== "ellipsis")', true) + ->assertScript('document.querySelector("[data-testid=\"tenant-dashboard-supporting-signals\"]").compareDocumentPosition(document.querySelector("[data-testid=\"tenant-dashboard-diagnostics\"]")) & Node.DOCUMENT_POSITION_FOLLOWING ? true : false', true) + ->assertDontSee('Fully ready') + ->assertDontSee('MANAGED_ENVIRONMENT') + ->assertDontSee('raw diff') + ->assertDontSee('raw payload should stay hidden') + ->assertDontSee('provider response should stay hidden') + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs() + ->screenshot(true, spec330Screenshot('environment-dashboard-readiness-workbench')); + + visit(ManagedEnvironmentLinks::baselineCompareUrl($environment)) + ->waitForText('Which baseline drift requires action?') + ->assertSee('Assigned baseline') + ->assertSee('Compare trust') + ->assertSee('Drift impact') + ->assertSee('Evidence path') + ->assertSee('Diagnostics - Collapsed') + ->assertScript('document.querySelector("[data-testid=\"baseline-compare-diagnostics\"]")?.open === false', true) + ->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-status-badge\"]")).every((badge) => !badge.innerText.includes("...") && getComputedStyle(badge).overflow !== "hidden" && getComputedStyle(badge).textOverflow !== "ellipsis")', true) + ->assertDontSee('MANAGED_ENVIRONMENT') + ->assertDontSee('raw diff') + ->assertDontSee('raw payload should stay hidden') + ->assertDontSee('provider response should stay hidden') + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs() + ->screenshot(true, spec330Screenshot('baseline-compare-decision-workbench')); +}); + +it('Spec330 smokes no-baseline, invalid environment denial, and static tenant-copy guard', function (): void { + [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); + $environment->forceFill(['name' => 'Spec330 Tenant Display Name'])->save(); + createInventorySyncOperationRunWithCoverage($environment, ['deviceConfiguration' => 'succeeded']); + spec330AuthenticateBrowser($this, $user, $environment); + + visit(ManagedEnvironmentLinks::baselineCompareUrl($environment)) + ->waitForText('Which baseline drift requires action?') + ->assertSee('Baseline not assigned') + ->assertSee('Baseline compare cannot be used for governance decisions until an assignment exists.') + ->assertSee('Compare trust is unavailable until a baseline assignment exists.') + ->assertSee('No usable drift result is available yet.') + ->assertSee('Open baseline profiles to assign a baseline to this environment.') + ->assertSee('Compare readiness flow') + ->assertSee('Baseline comparison needs an assigned baseline, linked snapshots, a compare run, and a decision output.') + ->assertSee('Baseline assigned') + ->assertSee('Missing') + ->assertSee('Baseline snapshot') + ->assertSee('Environment snapshot') + ->assertSee('Current environment evidence is present.') + ->assertSee('Compare run') + ->assertSee('Decision output') + ->assertSee('Available inputs') + ->assertSee('Operation proof') + ->assertSee('Unavailable because no baseline assigned.') + ->assertSee('What this unlocks after assignment') + ->assertSee('Actionable drift categories') + ->assertSee('Evidence-backed compare') + ->assertSee('Governance decision path') + ->assertSee('Diagnostics - Collapsed') + ->assertSee('Spec330 Tenant Display Name') + ->assertDontSee('This environment does not have an assigned baseline yet.') + ->assertDontSee('No coverage warning is currently reported for the latest compare.') + ->assertDontSee('Readiness overview') + ->assertDontSee('Recent baseline activity') + ->assertDontSee('0% Ready') + ->assertDontSee('Drift impact') + ->assertDontSee('MANAGED_ENVIRONMENT') + ->assertDontSee('raw diff') + ->assertDontSee('Evidence gap details') + ->assertDontSee('tenant filter') + ->assertDontSee('all tenants') + ->assertDontSee('choose tenant') + ->assertDontSee('tenant scope') + ->assertScript('document.querySelectorAll("[data-testid=\"baseline-compare-decision-workbench\"]").length === 1', true) + ->assertScript('document.querySelectorAll("[data-testid=\"baseline-compare-decision-summary\"]").length === 0', true) + ->assertScript('document.querySelectorAll("[data-testid=\"baseline-compare-proof-item\"]").length === 0', true) + ->assertScript('document.querySelectorAll("[data-testid=\"baseline-compare-readiness-step\"]").length === 5', true) + ->assertScript('document.querySelectorAll("[data-testid=\"baseline-compare-readiness-connector\"]").length === 4', true) + ->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-readiness-connector\"]")).every((connector) => connector.innerText.trim() === String.fromCharCode(8594))', true) + ->assertScript('document.querySelector("[data-step-label=\"Baseline assigned\"]")?.dataset.stepCurrentBlocker === "true"', true) + ->assertScript('document.querySelector("[data-step-label=\"Baseline assigned\"]")?.innerText.includes("Missing") === true', true) + ->assertScript('document.querySelector("[data-step-label=\"Environment snapshot\"]")?.innerText.includes("Available") === true', true) + ->assertScript('document.querySelector("[data-step-label=\"Compare run\"]")?.innerText.includes("Unavailable") === true', true) + ->assertScript('(function () { const steps = Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-readiness-step\"]")); if (steps.length !== 5) { return false; } const tops = steps.map((step) => step.getBoundingClientRect().top); return Math.max(...tops) - Math.min(...tops) < 12; })()', true) + ->assertScript('document.querySelectorAll("[data-testid=\"baseline-compare-available-input\"]").length === 3', true) + ->assertScript('document.querySelector("[data-input-label=\"Environment snapshot\"]")?.innerText.includes("Available") === true', true) + ->assertScript('document.querySelector("[data-input-label=\"Operation proof\"]")?.innerText.includes("Unavailable") === true', true) + ->assertScript('document.querySelector("[data-testid=\"baseline-compare-assignment-unlocks\"]") !== null', true) + ->assertScript('document.body.innerText.includes("No Baseline Assigned") === false', true) + ->assertScript('document.body.innerText.includes("Assigned baseline") === false', true) + ->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-decision-summary\"] > div:first-child")).filter((element) => element.innerText.trim() === "Assigned baseline").length === 0', true) + ->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-decision-summary\"] > div:first-child")).filter((element) => element.innerText.trim() === "Compare trust").length === 0', true) + ->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-decision-summary\"] > div:first-child")).filter((element) => element.innerText.trim() === "Drift impact").length === 0', true) + ->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-proof-panel\"] .text-xs.font-semibold")).filter((element) => element.innerText.trim() === "Evidence path").length === 1', true) + ->assertScript('Array.from(document.querySelectorAll("[data-testid=\"baseline-compare-status-badge\"]")).every((badge) => !badge.innerText.includes("...") && getComputedStyle(badge).overflow !== "hidden" && getComputedStyle(badge).textOverflow !== "ellipsis")', true) + ->assertScript('document.querySelector("[data-testid=\"baseline-compare-diagnostics\"]")?.open === false', true) + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs() + ->screenshot(true, spec330Screenshot('baseline-compare-no-baseline')); + + visit('/admin/baseline-compare-landing?environment_id='.(int) $environment->getKey()) + ->assertSee('404') + ->assertNoJavaScriptErrors(); +}); + +/** + * @return array{0: User, 1: ManagedEnvironment} + */ +function spec330DecisionSurfaceFixture(): array +{ + [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); + [$profile, $snapshot] = seedActiveBaselineForTenant($environment); + + $run = seedBaselineCompareRun($environment, $profile, $snapshot, [ + 'reason_code' => \App\Support\Baselines\BaselineCompareReasonCode::CoverageUnproven->value, + 'coverage' => [ + 'effective_types' => ['deviceConfiguration', 'deviceCompliancePolicy'], + 'covered_types' => ['deviceConfiguration'], + 'uncovered_types' => ['deviceCompliancePolicy'], + 'proof' => true, + ], + 'evidence_gaps' => [ + 'count' => 1, + 'by_reason' => [ + 'inventory_record_missing' => 1, + ], + ], + 'diagnostics' => [ + 'support_only' => 'raw payload should stay hidden', + 'provider_response' => 'provider response should stay hidden', + ], + ], OperationRunStatus::Completed->value, OperationRunOutcome::PartiallySucceeded->value); + + Finding::factory()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'finding_type' => Finding::FINDING_TYPE_DRIFT, + 'scope_key' => 'baseline_profile:'.$profile->getKey(), + 'severity' => Finding::SEVERITY_HIGH, + 'status' => Finding::STATUS_NEW, + 'source' => OperationRunType::BaselineCompare->value, + 'baseline_operation_run_id' => (int) $run->getKey(), + ]); + + foreach (range(1, 2) as $index) { + OperationRun::factory()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'type' => 'inventory_sync', + 'status' => OperationRunStatus::Completed->value, + 'outcome' => OperationRunOutcome::Failed->value, + 'completed_at' => now()->subHours($index), + ]); + } + + return [$user, $environment]; +} + +function spec330AuthenticateBrowser(mixed $test, User $user, ManagedEnvironment $environment): void +{ + $workspaceId = (int) $environment->workspace_id; + + $test->actingAs($user)->withSession([ + WorkspaceContext::SESSION_KEY => $workspaceId, + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ + (string) $workspaceId => (int) $environment->getKey(), + ], + ]); + + session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ + (string) $workspaceId => (int) $environment->getKey(), + ]); +} + +function spec330Screenshot(string $name): string +{ + return 'spec330-'.$name; +} diff --git a/apps/platform/tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php b/apps/platform/tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php index 1c0f34e9..df47cccc 100644 --- a/apps/platform/tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php +++ b/apps/platform/tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php @@ -246,9 +246,16 @@ ], ]); + $stats = BaselineCompareStats::forTenant($tenant); + + expect($stats->baselineCompareDiagnostics) + ->toHaveKey('strategy') + ->and(data_get($stats->baselineCompareDiagnostics, 'strategy.key'))->toBe('intune_policy') + ->and(data_get($stats->baselineCompareDiagnostics, 'strategy.execution_diagnostics.exception_class'))->toBe(RuntimeException::class); + baselineCompareLandingLivewire($tenant) ->assertSee('Baseline compare evidence') - ->assertSee('intune_policy') - ->assertSee('strategy_failed') - ->assertSee('RuntimeException'); + ->assertSee('Support diagnostics remain available after the operator summary and searchable detail.') + ->assertDontSee('intune_policy') + ->assertDontSee('RuntimeException'); }); diff --git a/apps/platform/tests/Feature/Filament/Spec330EnvironmentDashboardBaselineCompareProductizationTest.php b/apps/platform/tests/Feature/Filament/Spec330EnvironmentDashboardBaselineCompareProductizationTest.php new file mode 100644 index 00000000..5bed7df6 --- /dev/null +++ b/apps/platform/tests/Feature/Filament/Spec330EnvironmentDashboardBaselineCompareProductizationTest.php @@ -0,0 +1,325 @@ +first(fn (string $candidate): bool => is_file($candidate)); + + expect($path)->not->toBeNull(); + + $contents = file_get_contents($path); + + expect($contents) + ->toContain('Environment Dashboard') + ->toContain('Baseline Compare') + ->toContain('Provider connection/readiness') + ->toContain('Required permissions') + ->toContain('Backup sets') + ->toContain('Restore runs') + ->toContain('Baseline profile assignment') + ->toContain('Baseline compare result') + ->toContain('OperationRuns'); +}); + +it('renders the Environment Dashboard as a decision-first readiness workbench', function (): void { + [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); + + foreach (range(1, 3) as $index) { + OperationRun::factory()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'type' => 'inventory_sync', + 'status' => OperationRunStatus::Completed->value, + 'outcome' => OperationRunOutcome::Failed->value, + 'completed_at' => now()->subHours($index), + ]); + } + + $this->actingAs($user); + setAdminPanelContext($environment); + + $component = Livewire::test(EnvironmentDashboardOverview::class) + ->assertSee('Is this environment ready, blocked, stale, or requiring review?') + ->assertSee('Status') + ->assertSee('Reason') + ->assertSee('Impact') + ->assertSee('Readiness proof') + ->assertSee('Next action') + ->assertSee('Readiness dimensions') + ->assertSee('Recommended next actions') + ->assertSee('Supporting signals') + ->assertSee('Additional readiness signals used to explain the current recommendation.') + ->assertSee('Signal') + ->assertSee('State') + ->assertSee('Action') + ->assertSee('Unavailable') + ->assertSee('Not ready') + ->assertSee('Absent') + ->assertSee('No active review') + ->assertSee('No customer-safe output') + ->assertSee('Baseline missing') + ->assertSee('Operations follow-up') + ->assertSee('3 require review') + ->assertSee('Open operations hub') + ->assertSee('Diagnostics - Collapsed') + ->assertDontSee('Secondary context') + ->assertDontSee('Show operation details') + ->assertDontSee('View operation details') + ->assertDontSee('Ab...') + ->assertDontSee('Unava...') + ->assertDontSee('Not re...') + ->assertDontSee('No customer-saf...') + ->assertDontSee('Baseline assig...') + ->assertDontSee('Fully ready') + ->assertDontSee('Protected') + ->assertDontSee('Compliant') + ->assertDontSee('raw payload') + ->assertDontSee('raw diff') + ->assertDontSee('provider secret') + ->assertDontSee('stack trace') + ->assertDontSee('debug metadata') + ->assertDontSee('internal exception') + ->assertDontSee('provider response'); + + $content = $component->html(); + $readinessDecisionPosition = strpos($content, 'data-testid="tenant-dashboard-readiness-decision"'); + $supportingSignalsPosition = strpos($content, 'data-testid="tenant-dashboard-supporting-signals"'); + $operationFollowUpPosition = strpos($content, 'data-signal-key="operations_follow_up"'); + $diagnosticsPosition = strpos($content, 'data-testid="tenant-dashboard-diagnostics"'); + + expect(substr_count($content, 'data-testid="tenant-dashboard-readiness-decision"'))->toBe(1) + ->and(substr_count($content, 'data-testid="tenant-dashboard-readiness-proof-panel"'))->toBe(1) + ->and(substr_count($content, 'data-testid="tenant-dashboard-readiness-card"'))->toBe(0) + ->and(substr_count($content, 'data-testid="tenant-dashboard-supporting-signals"'))->toBe(1) + ->and(substr_count($content, 'data-testid="tenant-dashboard-supporting-signal"'))->toBe(6) + ->and(substr_count($content, 'data-testid="tenant-dashboard-operations-attention-summary"'))->toBe(0) + ->and(substr_count($content, 'data-testid="tenant-dashboard-operations-attention-item"'))->toBe(0) + ->and(substr_count($content, 'data-testid="tenant-dashboard-operation-details"'))->toBe(0) + ->and(substr_count($content, 'data-testid="tenant-dashboard-diagnostics"'))->toBe(1) + ->and(substr_count($content, 'data-testid="tenant-dashboard-status-badge"'))->toBeGreaterThan(0) + ->and($content)->not->toContain('tenant-dashboard-secondary-context') + ->and($content)->not->toContain('data-testid="tenant-dashboard-diagnostics" open') + ->and($content)->not->toContain('truncate') + ->and($content)->not->toContain('text-overflow') + ->and($readinessDecisionPosition)->not->toBeFalse() + ->and($supportingSignalsPosition)->not->toBeFalse() + ->and($operationFollowUpPosition)->not->toBeFalse() + ->and($diagnosticsPosition)->not->toBeFalse() + ->and($supportingSignalsPosition)->toBeGreaterThan($readinessDecisionPosition) + ->and($operationFollowUpPosition)->toBeGreaterThan($supportingSignalsPosition) + ->and($diagnosticsPosition)->toBeGreaterThan($supportingSignalsPosition); +}); + +it('keeps dynamic Tenant display names while avoiding static tenant copy on the dashboard', function (): void { + [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); + $environment->forceFill(['name' => 'Spec330 Tenant Named Environment'])->save(); + + $this->actingAs($user); + + $this->get(ManagedEnvironmentLinks::viewUrl($environment)) + ->assertOk() + ->assertSeeText('Spec330 Tenant Named Environment') + ->assertDontSeeText('MANAGED_ENVIRONMENT') + ->assertDontSeeText('current tenant') + ->assertDontSeeText('tenant filter') + ->assertDontSeeText('all tenants') + ->assertDontSeeText('choose tenant') + ->assertDontSeeText('tenant scope'); +}); + +it('renders Baseline Compare no-assignment as an actionable unavailable decision state', function (): void { + [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); + createInventorySyncOperationRunWithCoverage($environment, ['deviceConfiguration' => 'succeeded']); + + $this->actingAs($user); + setAdminPanelContext($environment); + + $component = baselineCompareLandingLivewire($environment) + ->assertSee('Which baseline drift requires action?') + ->assertSee('Baseline not assigned') + ->assertSee('Baseline compare cannot be used for governance decisions until an assignment exists.') + ->assertSee('Compare trust is unavailable until a baseline assignment exists.') + ->assertSee('No usable drift result is available yet.') + ->assertSee('Open baseline profiles to assign a baseline to this environment.') + ->assertSee('Evidence path') + ->assertSee('Next action') + ->assertSee('Compare readiness flow') + ->assertSee('Baseline comparison needs an assigned baseline, linked snapshots, a compare run, and a decision output.') + ->assertSee('Baseline assigned') + ->assertSee('Missing') + ->assertSee('No baseline is assigned to this environment.') + ->assertSee('Baseline snapshot') + ->assertSee('No baseline snapshot is linked.') + ->assertSee('Environment snapshot') + ->assertSee('Current environment evidence is present.') + ->assertSee('Environment snapshot state is required for compare.') + ->assertSee('Compare run') + ->assertSee('Compare cannot run until required inputs exist.') + ->assertSee('Decision output') + ->assertSee('No drift decision output is available yet.') + ->assertSee('Available inputs') + ->assertSee('Operation proof') + ->assertSee('Unavailable because no baseline assigned.') + ->assertSee('What this unlocks after assignment') + ->assertSee('Actionable drift categories') + ->assertSee('Evidence-backed compare') + ->assertSee('Governance decision path') + ->assertSee('Diagnostics - Collapsed') + ->assertDontSee('No Baseline Assigned') + ->assertDontSee('This environment does not have an assigned baseline yet.') + ->assertDontSee('No coverage warning is currently reported for the latest compare.') + ->assertDontSee('Readiness overview') + ->assertDontSee('Recent baseline activity') + ->assertDontSee('0% Ready') + ->assertDontSee('Assigned baseline') + ->assertDontSee('Drift impact') + ->assertDontSee('Evidence gap details') + ->assertDontSee('Search recorded gap subjects') + ->assertDontSee('raw payload') + ->assertDontSee('raw diff') + ->assertDontSee('provider secret') + ->assertDontSee('stack trace') + ->assertDontSee('debug metadata') + ->assertDontSee('internal exception') + ->assertDontSee('provider response'); + + $content = $component->html(); + $visibleLabelCount = static fn (string $label): int => preg_match_all('/>\s*'.preg_quote($label, '/').'\s*toBe(1) + ->and(substr_count($content, 'data-testid="baseline-compare-decision-summary"'))->toBe(0) + ->and(substr_count($content, 'data-testid="baseline-compare-proof-item"'))->toBe(0) + ->and(substr_count($content, 'data-testid="baseline-compare-readiness-flow"'))->toBe(1) + ->and(substr_count($content, 'data-testid="baseline-compare-readiness-step"'))->toBe(5) + ->and(substr_count($content, 'data-testid="baseline-compare-readiness-connector"'))->toBe(4) + ->and(substr_count($content, 'data-testid="baseline-compare-available-input"'))->toBe(3) + ->and(substr_count($content, 'data-testid="baseline-compare-assignment-unlocks"'))->toBe(1) + ->and($content)->toContain('aria-label="Compare readiness pipeline"') + ->and($content)->toContain('data-connector-label="Baseline assigned to Baseline snapshot"') + ->and($content)->toContain('data-connector-label="Baseline snapshot to Environment snapshot"') + ->and($content)->toContain('data-connector-label="Environment snapshot to Compare run"') + ->and($content)->toContain('data-connector-label="Compare run to Decision output"') + ->and($content)->toMatch('/data-step-label="Baseline assigned"[\s\S]*?data-step-state="Missing"[\s\S]*?data-step-current-blocker="true"[\s\S]*?>\s*Missing\s*and($content)->toMatch('/data-step-label="Environment snapshot"[\s\S]*?>\s*Available\s*and($content)->toMatch('/data-step-label="Compare run"[\s\S]*?>\s*Unavailable\s*and($content)->toMatch('/data-input-label="Environment snapshot"[\s\S]*?>\s*Available\s*and($content)->toMatch('/data-input-label="Operation proof"[\s\S]*?>\s*Unavailable\s*and($visibleLabelCount('Assigned baseline'))->toBe(0) + ->and($visibleLabelCount('Compare trust'))->toBe(0) + ->and($visibleLabelCount('Drift impact'))->toBe(0) + ->and($visibleLabelCount('Evidence path'))->toBe(1) + ->and($content)->not->toContain('data-testid="baseline-compare-diagnostics" open') + ->and($content)->not->toContain('evidence_gap_details') + ->and($content)->not->toContain('raw diff'); +}); + +it('renders Baseline Compare drift and evidence summary before support diagnostics', function (): void { + [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); + [$profile, $snapshot] = seedActiveBaselineForTenant($environment); + + $run = seedBaselineCompareRun($environment, $profile, $snapshot, [ + 'reason_code' => \App\Support\Baselines\BaselineCompareReasonCode::CoverageUnproven->value, + 'coverage' => [ + 'effective_types' => ['deviceConfiguration', 'deviceCompliancePolicy'], + 'covered_types' => ['deviceConfiguration'], + 'uncovered_types' => ['deviceCompliancePolicy'], + 'proof' => true, + ], + 'evidence_gaps' => [ + 'count' => 1, + 'by_reason' => [ + 'inventory_record_missing' => 1, + ], + ], + 'diagnostics' => [ + 'support_only' => 'raw payload should stay hidden', + 'provider_response' => 'provider response should stay hidden', + ], + ], OperationRunStatus::Completed->value, OperationRunOutcome::PartiallySucceeded->value); + + Finding::factory()->create([ + 'managed_environment_id' => (int) $environment->getKey(), + 'workspace_id' => (int) $environment->workspace_id, + 'finding_type' => Finding::FINDING_TYPE_DRIFT, + 'scope_key' => 'baseline_profile:'.$profile->getKey(), + 'severity' => Finding::SEVERITY_HIGH, + 'status' => Finding::STATUS_NEW, + 'source' => OperationRunType::BaselineCompare->value, + 'baseline_operation_run_id' => (int) $run->getKey(), + ]); + + $this->actingAs($user); + setAdminPanelContext($environment); + + baselineCompareLandingLivewire($environment) + ->assertSee('Which baseline drift requires action?') + ->assertSee('Drift requires review') + ->assertSee('Evidence path') + ->assertSee('Evidence gaps need review') + ->assertSee('Open operation proof') + ->assertSee('Diagnostics - Collapsed') + ->assertDontSee('raw payload should stay hidden') + ->assertDontSee('provider response should stay hidden') + ->assertDontSee('stack trace') + ->assertDontSee('debug metadata'); +}); + +it('keeps both surfaces environment-owned and rejects legacy compare entry points', function (): void { + [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager'); + + $this->actingAs($user) + ->withSession([ + WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id, + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ + (string) $environment->workspace_id => (int) $environment->getKey(), + ], + ]) + ->get(ManagedEnvironmentLinks::viewUrl($environment)) + ->assertOk() + ->assertSeeText('Is this environment ready, blocked, stale, or requiring review?') + ->assertDontSeeText('MANAGED_ENVIRONMENT'); + + $this->actingAs($user) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id]) + ->get(ManagedEnvironmentLinks::baselineCompareUrl($environment)) + ->assertOk() + ->assertSeeText('Which baseline drift requires action?') + ->assertDontSeeText('MANAGED_ENVIRONMENT'); + + $this->actingAs($user) + ->withSession([ + WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id, + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ + (string) $environment->workspace_id => (int) $environment->getKey(), + ], + ]) + ->get('/admin/baseline-compare-landing?environment_id='.(int) $environment->getKey().'&tenant_scope=selected') + ->assertNotFound(); + + expect(BaselineCompareLanding::getUrl([ + 'tenant' => (string) $environment->external_id, + 'tenant_id' => (int) $environment->getKey(), + 'managed_environment_id' => (int) $environment->getKey(), + 'environment' => 'legacy-alias', + 'tenant_scope' => 'selected', + 'tableFilters' => ['managed_environment_id' => ['value' => (int) $environment->getKey()]], + ], panel: 'admin', tenant: $environment)) + ->not->toContain('tenant_id=') + ->not->toContain('managed_environment_id=') + ->not->toContain('tenant_scope=') + ->not->toContain('tableFilters'); +}); diff --git a/apps/platform/tests/Feature/Monitoring/HeaderContextBarTest.php b/apps/platform/tests/Feature/Monitoring/HeaderContextBarTest.php index 4a8424fa..e3642c46 100644 --- a/apps/platform/tests/Feature/Monitoring/HeaderContextBarTest.php +++ b/apps/platform/tests/Feature/Monitoring/HeaderContextBarTest.php @@ -31,7 +31,8 @@ ->assertSee('Switch workspace') ->assertSee('admin/select-environment') ->assertSee(__('localization.shell.clear_environment_scope')) - ->assertSee($tenant->getFilamentName()); + ->assertSee($tenant->name) + ->assertDontSee('MANAGED_ENVIRONMENT'); $content = $response->getContent(); @@ -80,8 +81,9 @@ ]) ->get(ManagedEnvironmentLinks::viewUrl($tenant)) ->assertOk() - ->assertSee($tenant->getFilamentName()) - ->assertSee(__('localization.shell.clear_environment_scope')); + ->assertSee($tenant->name) + ->assertSee(__('localization.shell.clear_environment_scope')) + ->assertDontSee('MANAGED_ENVIRONMENT'); }); it('renders routed tenant resource view pages with a clear tenant scope action but no inline selector list', function (): void { @@ -133,8 +135,9 @@ ]) ->get(route('admin.operations.index', ['workspace' => $tenantA->workspace])) ->assertOk() - ->assertSee($tenantA->getFilamentName()) - ->assertDontSee($tenantB->getFilamentName()); + ->assertSee($tenantA->name) + ->assertDontSee($tenantB->name) + ->assertDontSee('MANAGED_ENVIRONMENT'); }); it('keeps the header tenant picker limited to tenant-entitled active tenants for workspace owners', function (): void { @@ -155,8 +158,9 @@ ]) ->get(route('admin.operations.index', ['workspace' => $tenantA->workspace])) ->assertOk() - ->assertSee($tenantA->getFilamentName()) - ->assertDontSee($tenantB->getFilamentName()); + ->assertSee($tenantA->name) + ->assertDontSee($tenantB->name) + ->assertDontSee('MANAGED_ENVIRONMENT'); }); it('hides onboarding tenants from the header tenant picker even for workspace owners', function (): void { @@ -170,7 +174,7 @@ $onboardingTenant = ManagedEnvironment::factory()->create([ 'status' => ManagedEnvironment::STATUS_ONBOARDING, 'workspace_id' => (int) $tenantA->workspace_id, - 'name' => 'YPTW2', + 'name' => 'ZZZ-ONBOARDING-TENANT-NAME-12345', 'environment' => 'dev', ]); @@ -182,8 +186,9 @@ ]) ->get(route('admin.operations.index', ['workspace' => $tenantA->workspace])) ->assertOk() - ->assertSee($tenantA->getFilamentName()) - ->assertDontSee($onboardingTenant->getFilamentName()); + ->assertSee($tenantA->name) + ->assertDontSee($onboardingTenant->name) + ->assertDontSee('MANAGED_ENVIRONMENT'); }); it('does not implicitly switch tenant when opening canonical operation deep links', function (): void { diff --git a/apps/platform/tests/Unit/ManagedEnvironment/ManagedEnvironmentModelTest.php b/apps/platform/tests/Unit/ManagedEnvironment/ManagedEnvironmentModelTest.php index 15eae0c7..b0748202 100644 --- a/apps/platform/tests/Unit/ManagedEnvironment/ManagedEnvironmentModelTest.php +++ b/apps/platform/tests/Unit/ManagedEnvironment/ManagedEnvironmentModelTest.php @@ -26,7 +26,7 @@ ->and($environment->getRouteKeyName())->toBe('slug') ->and((new ManagedEnvironment)->resolveRouteBinding('contoso-prod')->is($environment))->toBeTrue() ->and($environment->isSelectableAsContext())->toBeTrue() - ->and($environment->getFilamentName())->toBe('Contoso Prod (PRODUCTION)'); + ->and($environment->getFilamentName())->toBe('Contoso Prod'); }); it('keeps archived environments resolvable by route key but unavailable as active context', function (): void { diff --git a/specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/baseline-compare-decision-workbench.png b/specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/baseline-compare-decision-workbench.png new file mode 100644 index 00000000..d9c4e66a Binary files /dev/null and b/specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/baseline-compare-decision-workbench.png differ diff --git a/specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/baseline-compare-no-baseline.png b/specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/baseline-compare-no-baseline.png new file mode 100644 index 00000000..2b98c7d5 Binary files /dev/null and b/specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/baseline-compare-no-baseline.png differ diff --git a/specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/environment-dashboard-action-needed.png b/specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/environment-dashboard-action-needed.png new file mode 100644 index 00000000..01cd54a6 Binary files /dev/null and b/specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/environment-dashboard-action-needed.png differ diff --git a/specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/environment-dashboard-readiness-workbench.png b/specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/environment-dashboard-readiness-workbench.png new file mode 100644 index 00000000..01cd54a6 Binary files /dev/null and b/specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/environment-dashboard-readiness-workbench.png differ diff --git a/specs/330-environment-dashboard-baseline-compare-productization/checklists/requirements.md b/specs/330-environment-dashboard-baseline-compare-productization/checklists/requirements.md new file mode 100644 index 00000000..3a11e659 --- /dev/null +++ b/specs/330-environment-dashboard-baseline-compare-productization/checklists/requirements.md @@ -0,0 +1,45 @@ +# Spec 330 Requirements Checklist + +**Purpose**: Preparation-quality validation for Spec 330 before runtime implementation. +**Created**: 2026-05-19 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] Spec is focused on user value and decision-first environment governance. +- [x] No implementation code changes are included in preparation artifacts. +- [x] Requirements are testable and measurable. +- [x] Scope is constrained to Environment Dashboard and Baseline Compare productization. +- [x] Out-of-scope items explicitly exclude new engines, migrations, packages, env vars, queues, scheduler, storage, and broad backend foundation. + +## Product Requirements + +- [x] Environment Dashboard primary question is defined. +- [x] Baseline Compare primary question is defined. +- [x] Decision-first ordering is defined for both surfaces. +- [x] No false-green language is prohibited unless repo-backed. +- [x] Diagnostics/raw payload/raw diff are required to be collapsed or secondary by default. +- [x] Static platform-copy use of `tenant` is prohibited while dynamic names containing `Tenant` remain allowed. + +## Repo Truth + +- [x] Current repo paths were verified and corrected from the input draft where needed. +- [x] Repo truth map exists and covers required data areas. +- [x] UI elements are mapped to source, scope, authorization, fallback, and classification. +- [x] OperationRun, evidence, baseline, and drift links are documented as proof/context rather than broad health certification. + +## Scope And Routing + +- [x] Both surfaces are specified as environment-owned. +- [x] Explicit workspace/environment route ownership is required. +- [x] Remembered environment fallback is rejected as ownership authority. +- [x] Legacy tenant/context aliases are prohibited. +- [x] Cross-workspace environment access must be rejected. + +## Readiness + +- [x] Candidate Selection Gate is documented. +- [x] Proportionality review is included and identifies no new persisted entity, abstraction, enum/status family, or taxonomy/framework. +- [x] Implementation phases and validation commands are present. +- [x] Tests and browser smoke expectations are listed. +- [x] Follow-up specs are listed but excluded from Spec 330 implementation. diff --git a/specs/330-environment-dashboard-baseline-compare-productization/plan.md b/specs/330-environment-dashboard-baseline-compare-productization/plan.md new file mode 100644 index 00000000..1f7cedcf --- /dev/null +++ b/specs/330-environment-dashboard-baseline-compare-productization/plan.md @@ -0,0 +1,356 @@ +# Implementation Plan: Spec 330 - Environment Dashboard / Baseline Compare Productization + +**Branch**: `330-environment-dashboard-baseline-compare-productization` | **Date**: 2026-05-19 | **Spec**: `specs/330-environment-dashboard-baseline-compare-productization/spec.md` +**Input**: User-provided Spec 330 and repo inspection. + +## Summary + +Productize two existing environment-owned strategic surfaces into decision-first governance pages: + +```text +Is this environment ready, blocked, stale, or requiring review? +Which baseline drift requires action? +``` + +The implementation must keep current routes, source truth, RBAC, environment-owned scope, and OperationRun semantics. It must not add new backend readiness/drift/proof engines. Environment Dashboard should elevate status, reason, impact, proof, and one primary next action before technical detail. Baseline Compare should elevate assignment, compare trust, drift impact, evidence/proof, and one primary next action before findings detail, raw diff, or diagnostics. + +## Technical Context + +**Language/Version**: PHP 8.4.15, Laravel 12.52.0. +**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1, Tailwind CSS 4.2.2. +**Storage**: PostgreSQL; no schema change expected. +**Testing**: Pest 4 Feature/Livewire/Browser tests. +**Validation Lanes**: confidence and browser; targeted navigation/context guard filters. +**Target Platform**: Laravel Sail locally; Dokploy/container deployment posture unchanged. +**Project Type**: Laravel monolith under `apps/platform`. +**Performance Goals**: DB-only page render; no Graph/provider API calls during render; no broad new query families. +**Constraints**: No new persisted truth, migration, package, queue, scheduler, storage, env var, deployment asset, compatibility route, or legacy alias support. +**Scale/Scope**: Two existing environment-owned Filament pages, existing widget/view structures, feature-local payload helpers if needed, focused tests, and browser smoke. + +## UI / Surface Guardrail Plan + +- **Guardrail scope**: changed existing operator-facing strategic environment-owned surfaces. +- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**: + - `/admin/workspaces/{workspace}/environments/{environment}` + - `/admin/workspaces/{workspace}/environments/{environment}/baseline-compare` + - `apps/platform/app/Filament/Pages/EnvironmentDashboard.php` + - `apps/platform/app/Filament/Widgets/Dashboard/EnvironmentDashboardOverview.php` + - `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php` + - `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-context-chips.blade.php` + - `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php` + - `apps/platform/app/Filament/Pages/BaselineCompareLanding.php` + - `apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php` +- **No-impact class, if applicable**: N/A. +- **Native vs custom classification summary**: Native Filament Dashboard/Page/Widget plus existing Blade composition; no new UI framework. +- **Shared-family relevance**: dashboard signals, status messaging, action links, proof links, OperationRun links, baseline compare state, diagnostics disclosure. +- **State layers in scope**: route-owned environment, page payload, widget payload, Baseline Compare launch query state (`baseline_profile_id`, `subject_key`, `nav`), and existing session/context guard behavior. +- **Audience modes in scope**: operator-MSP, manager, governance operator, support reviewer where authorized. +- **Decision/diagnostic/raw hierarchy plan**: decision first, proof path visible, diagnostics collapsed, raw/support hidden or gated. +- **Raw/support gating plan**: collapsed by default and capability-gated through existing support diagnostics/raw capabilities where exposed. +- **One-primary-action / duplicate-truth control**: one dominant next action per page; secondary links/cards support the decision rather than repeat the same verdict. +- **Handling modes by drift class or surface**: review-mandatory for UI-002 and UI-061 strategic surfaces; document-in-feature for any UI coverage registry no-change decision. +- **Repository-signal treatment**: Spec 325 target images are direction only; runtime claims must be repo-verified, derived, unavailable, or deferred. +- **Special surface test profiles**: `global-context-shell`, `monitoring-state-page`, `shared-detail-family`. +- **Required tests or manual smoke**: Feature/Livewire tests for decision layout, false-green guard, RBAC/scope/disclosure plus Pest Browser smoke for explicit environment routes, no-baseline/action-needed/drift states, diagnostics collapsed, raw detail hidden, cross-workspace denial, and static tenant copy guard. +- **Exception path and spread control**: none expected. Any new dangerous action, schema, capability, backend proof source, or route family requires spec/plan/tasks update first. +- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage. +- **UI/Productization coverage decision**: active spec package carries productization proof. Update UI coverage registry only if route/archetype/coverage classification changes; otherwise document why UI-002/UI-061 plus Spec 325 artifacts are sufficient. +- **Coverage artifacts to update**: none expected unless implementation changes route/archetype state. +- **Navigation / Filament provider-panel handling**: no panel provider registration changes expected. Laravel 12 panel providers remain in `apps/platform/bootstrap/providers.php`. +- **Screenshot or page-report need**: screenshots required; full page report optional unless implementation materially changes coverage classification. + +## Shared Pattern & System Fit + +- **Cross-cutting feature marker**: yes. +- **Systems touched**: Environment Dashboard page, dashboard widgets, dashboard summary builder, Baseline Compare Landing page/view, baseline stats/aggregate helpers, backup/recovery helpers, provider permission view models, OperationRun links, resource policies, `UiEnforcement`, and browser fixtures/tests. +- **Shared abstractions reused**: `EnvironmentDashboardSummaryBuilder`, `EnvironmentDashboardSummary`, `BaselineCompareStats`, `TenantGovernanceAggregateResolver`, `OperationRunLinks`, `ManagedEnvironmentLinks`, `OperationUxPresenter`, `OpsUxBrowserEvents`, `BadgeRenderer`, `UiEnforcement`, policies/capability resolvers. +- **New abstraction introduced? why?**: none. Page-local private helpers may be added only to reduce Blade complexity or duplicate payload calculation. +- **Why the existing abstraction was sufficient or insufficient**: Existing paths provide source truth, authorization, links, compare state, backup/recovery/readiness signals, and OperationRun proof. They do not fully enforce the first-read decision hierarchy and collapsed diagnostics disclosure. +- **Bounded deviation / spread control**: no public readiness/proof/disclosure framework; keep composition local to the two surfaces. + +## OperationRun UX Impact + +- **Touches OperationRun start/completion/link UX?**: yes, through existing Baseline Compare start action and proof links. No new OperationRun type or lifecycle behavior. +- **Central contract reused**: `OperationRunLinks`, `OperationUxPresenter`, `OpsUxBrowserEvents`, existing `OperationRunPolicy`, existing operation detail routes, and `BaselineCompareService::startCompare()`. +- **Delegated UX behaviors**: compare queued toast, run-enqueued browser event, open operation link, blocked/cannot-start messaging, and tenant/workspace-safe URLs stay on the existing Ops UX path. +- **Surface-owned behavior kept local**: display-only proof availability labels and next-action hierarchy. +- **Queued DB-notification policy**: unchanged / N/A. +- **Terminal notification path**: unchanged. +- **Exception path**: none. + +## Provider Boundary & Portability Fit + +- **Shared provider/platform boundary touched?**: display-only provider readiness/permissions and static copy guard. +- **Provider-owned seams**: existing provider connection, Microsoft/Entra/Intune required permissions, provider tenant ID where source records require it. +- **Platform-core seams**: workspace, environment, readiness, proof, baseline, drift, evidence, operation, review, diagnostics. +- **Neutral platform terms / contracts preserved**: workspace, environment, provider, required permissions, operation proof, evidence, review, baseline, drift, diagnostics. +- **Retained provider-specific semantics and why**: Microsoft/Entra/Intune can remain at source/provider-boundary labels only. +- **Bounded extraction or follow-up path**: Provider Readiness Productization remains a separate follow-up spec. + +## Constitution Check + +- **Inventory-first, snapshots-second**: Environment and compare surfaces derive from last-observed inventory, explicit snapshots, backups, evidence, reviews, and operations; no new source truth is created. +- **Read/write separation by default**: Pages remain read/decision-first. Existing compare start is the only high-impact operation start and already requires confirmation/authorization/OperationRun. +- **Single Contract Path to Graph**: No Graph/provider API calls may be added to page render. +- **Deterministic Capabilities**: Reuse existing `Capabilities`, `CapabilityResolver`, resource policies, `UiEnforcement`, and route authorization. +- **RBAC-UX**: UI visibility is not security. Server-side policies/gates remain authoritative for operations, evidence, review packs, backup/restore, provider permissions, and diagnostics. +- **Workspace/environment isolation**: Both pages require explicit route-bound environment. Non-member or mismatched workspace/environment access is 404. +- **OperationRun UX**: Existing compare start and operation links reuse shared OperationRun UX helpers; no local lifecycle/notification behavior. +- **UI-COV-001**: Existing strategic surfaces UI-002 and UI-061 materially change. Active spec package must carry repo-truth map, tests, screenshots, and coverage close-out decision. +- **TEST-GOV-001**: Targeted Feature and Browser lanes are explicit. Browser cost is named and scoped. +- **PROP-001 / BLOAT-001**: No new source of truth, persisted entity, enum/status family, public abstraction, proof engine, or cross-domain UI framework. +- **UI-FIL-001**: Use native Filament components/shared primitives first; custom Blade must preserve Filament semantics, accessibility, dark mode, and one-primary-action hierarchy. +- **Filament v5 / Livewire v4**: Livewire v4.0+ compliance required. No Livewire v3 or Filament v3/v4 APIs. +- **Panel provider location**: panel providers remain in `apps/platform/bootstrap/providers.php`; no provider registration change expected. +- **Global search**: no resource global search change expected. If a resource is touched, it must retain safe View/Edit pages or disable global search. +- **Destructive/high-impact actions**: no destructive action introduced. Existing compare start remains confirmed/capability-gated/OperationRun-backed. +- **Assets**: no Filament asset registration expected; `filament:assets` deployment posture unchanged. + +## Current Repo Truth Summary + +Existing verified Environment Dashboard surfaces: + +- `EnvironmentDashboard` extends Filament `Dashboard` and renders `EnvironmentDashboardContextChips`, `ManagedEnvironmentTriageArrivalContinuity`, `DashboardKpis`, and `EnvironmentDashboardOverview`. +- `EnvironmentDashboard::getUrl()` resolves route-owned `ManagedEnvironment` and strips workspace/environment parameters into `ManagedEnvironmentLinks::viewUrl()`. +- `EnvironmentDashboardSummaryBuilder` already derives context, posture, KPIs, recommended actions, governance status, readiness cards, active operation summary, and recent operations from existing models/services. +- Current dashboard view renders recommended actions, governance status, active operations requiring attention, and right-side readiness cards. +- Existing browser tests already assert dashboard context chips, KPI cards, readiness cards, ranked actions, and no table-first layout. + +Existing verified Baseline Compare surfaces: + +- `BaselineCompareLanding` is environment-owned at slug `workspaces/{workspace}/environments/{environment}/baseline-compare`. +- It rejects old workspace-style `/admin/baseline-compare-landing?environment_id=` URLs and remembered environment fallback. +- It strips legacy scope keys from generated URLs while preserving `baseline_profile_id`, `subject_key`, and `nav`. +- `BaselineCompareStats::forTenant()` derives states including `no_tenant`, `no_assignment`, `no_snapshot`, `invalid_scope`, `comparing`, `failed`, `idle`, and `ready`. +- Existing compare start action uses `Action::make('compareNow')->requiresConfirmation()->action(...)`, `BaselineCompareService::startCompare()`, `UiEnforcement`, `OperationUxPresenter`, and `OpsUxBrowserEvents`. +- Current Blade already has explanation/stats, no-assignment/no-snapshot empty states, coverage warnings, evidence gap details, findings summaries, and diagnostics section; diagnostics are currently rendered when present and must become collapsed/default-secondary. + +Known productization gaps: + +- Environment Dashboard needs a more explicit decision card/question and proof/action panel instead of equal-weight dashboard sections. +- Environment Dashboard readiness dimensions should map to source truth and unavailable states in the spec map before implementation. +- Baseline Compare needs a first-read drift/action decision card, no-baseline actionable unavailable state, proof/disclosure panel, and raw diff/diagnostics collapsed by default. +- Baseline Compare must avoid false all-clear copy when zero findings coexist with evidence/coverage/trust gaps. + +## Existing Repository Surfaces Likely Affected + +Runtime files, only during later implementation: + +- `apps/platform/app/Filament/Pages/EnvironmentDashboard.php` +- `apps/platform/app/Filament/Widgets/Dashboard/EnvironmentDashboardOverview.php` +- `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php` +- `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-context-chips.blade.php` +- `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php` +- `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummary.php` +- `apps/platform/app/Filament/Pages/BaselineCompareLanding.php` +- `apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php` +- `apps/platform/resources/lang/en/*` and `apps/platform/resources/lang/de/*` only where touched copy follows existing localization conventions. + +Tests, only during later implementation: + +- `apps/platform/tests/Feature/Filament/TenantDashboardTenantScopeTest.php` +- `apps/platform/tests/Feature/Filament/TenantDashboardDbOnlyTest.php` +- `apps/platform/tests/Feature/Rbac/DashboardRecoveryPostureVisibilityTest.php` +- `apps/platform/tests/Feature/Rbac/TenantDashboardArrivalContextVisibilityTest.php` +- `apps/platform/tests/Feature/Filament/BaselineCompareEnvironmentRouteContractTest.php` +- `apps/platform/tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php` +- `apps/platform/tests/Feature/Filament/BaselineCompareExplanationSurfaceTest.php` +- `apps/platform/tests/Feature/Rbac/DriftLandingUiEnforcementTest.php` +- `apps/platform/tests/Feature/Navigation/Spec322AdminSurfaceScopeContractTest.php` +- `apps/platform/tests/Feature/Navigation/Spec322EnvironmentCtaUrlContractTest.php` +- `apps/platform/tests/Feature/ManagedEnvironment/ManagedEnvironmentCopyNeutralizationGuardTest.php` +- `apps/platform/tests/Browser/Spec330EnvironmentDashboardBaselineCompareSmokeTest.php` + +Spec/UI artifacts: + +- `specs/330-environment-dashboard-baseline-compare-productization/repo-truth-map.md` +- screenshot artifacts under `specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/` +- optional UI coverage registry updates only if implementation materially changes route/archetype/coverage state. + +## Domain / Model Implications + +- No new model, table, migration, enum, status family, persisted display state, or public service contract. +- Environment readiness must derive from: + - `ManagedEnvironment` and workspace relation. + - `ProviderConnection` status/health/consent/readiness fields. + - `ManagedEnvironmentPermission` and required permissions view model. + - `TenantBackupHealthResolver`, `TenantBackupHealthAssessment`, backup sets/schedules. + - `RestoreSafetyResolver` and restore/recovery evidence presentation. + - `TenantGovernanceAggregateResolver`, `Finding`, `FindingException`, and baseline compare stats. + - `EvidenceSnapshot`, `EnvironmentReview`, `ReviewPack`, and related links. + - `OperationRun` attention/freshness/outcome and `OperationRunLinks`. +- Baseline drift/action state must derive from: + - `BaselineTenantAssignment`, `BaselineProfile`, `BaselineSnapshot`, `BaselineSnapshotTruthResolver`. + - `BaselineCompareStats`, `TenantGovernanceAggregate`, `BaselineCompareSummaryAssessment`, `operatorExplanation`. + - `Finding` drift findings/severity counts. + - Evidence gap summaries/details and compare diagnostics only as secondary/collapsed context. + - Existing compare OperationRun relation and links. + +## UI / Filament Implications + +- Filament v5 and Livewire v4.0+ compliance must be preserved. +- Keep panel provider registration unchanged. +- Use Filament sections/buttons/badges/actions and existing shared primitives where suitable. +- Avoid fake charts, fake health, fake compliance, fake customer-safe, fake restore confidence, or generic KPI dashboard expansion. +- Environment Dashboard target order: + - header/scope + - main readiness decision card + - readiness dimensions + - right-side proof/action/disclosure panel + - ranked next actions + - operations/reviews/evidence/backup/provider details as secondary context + - diagnostics collapsed +- Baseline Compare target order: + - header/scope + - main drift/action decision card + - no-baseline visual Compare readiness stepper/pipeline and compact available inputs when compare is blocked by missing assignment + - assignment / compare trust / drift summary only when a baseline-backed compare state exists + - right-side evidence/proof/disclosure panel + - findings/evidence-gap summary + - raw diff/details collapsed or secondary + +## Livewire / Page State Implications + +- Environment Dashboard and Baseline Compare must rely on route-owned environment context. +- Baseline Compare launch context (`baseline_profile_id`, `subject_key`, `nav`) remains initialization/query state only; it must not override environment ownership. +- Existing `BaselineCompareLanding::monitoringPageStateContract()` should remain consistent with Spec 198/319 contracts. +- Clean old workspace-style URLs and legacy query aliases must not establish environment context. +- Cross-workspace route mismatches stay 404. + +## RBAC / Policy Implications + +Reuse existing authorization: + +- Environment page visibility: environment membership and `TENANT_VIEW`. +- Provider permissions/readiness: required permissions routes and existing provider/permission capabilities. +- Backup posture: backup-set/schedule capabilities and policies. +- Restore/recovery proof: restore-run/backup capabilities and policies. +- Baseline compare view: environment access and `TENANT_VIEW`. +- Compare start: `TENANT_SYNC` through existing `UiEnforcement`. +- Baseline profile links: `BaselineProfileResource` authorization/policies. +- Drift findings: `FindingResource` / finding capabilities. +- Evidence snapshots: `EvidenceSnapshotPolicy` / evidence capability. +- Review and review packs: `EnvironmentReviewResource`, `ReviewPackResource`, policies. +- Operation proof: `OperationRunPolicy` and link helpers. +- Diagnostics/raw detail: `SUPPORT_DIAGNOSTICS_VIEW` or stricter existing raw/support capability. + +No new permission semantics should be added unless implementation proves existing capabilities cannot express the action and spec/plan/tasks are updated first. + +## Audit / Observability Implications + +- No new audit events are expected for read-only productization. +- Existing support diagnostics modal on Environment Dashboard already writes audit/telemetry and must remain authorized/redacted. +- Existing Baseline Compare start audit/OperationRun behavior remains unchanged. +- Do not imply OperationRun completion proves environment health or compare acceptability. + +## Test Strategy + +Feature/Livewire tests: + +- Repo truth map existence and required sections. +- Environment Dashboard decision layout. +- Environment Dashboard no false green when proof is missing. +- Environment Dashboard ranked next actions. +- Baseline Compare decision layout. +- Baseline Compare no-baseline actionable unavailable state. +- Baseline Compare drift/evidence state with raw details hidden by default. +- Diagnostics hidden/collapsed by default across both pages. +- Environment-owned route contract and legacy alias rejection. +- Static `tenant` platform-copy guard. +- RBAC for visible/hidden actions and raw diagnostics. + +Browser tests: + +- Explicit Environment Dashboard route non-empty/action-needed state. +- Explicit Baseline Compare route no-baseline state. +- Baseline Compare compare/drift state if fixtures support it. +- Diagnostics collapsed and raw details absent by default. +- Cross-workspace/invalid environment safe denial. +- Dynamic display name containing `Tenant` allowed, static tenant copy absent. +- Required screenshots under the Spec 330 artifacts path. + +Validation: + +- Targeted Feature/Livewire tests. +- Targeted Browser smoke. +- Context/route guard filter for EnvironmentDashboard, BaselineCompare, AdminSurfaceScope, EnvironmentOwned, LegacyTenant, and Spec322. +- `pint --dirty`. +- `git diff --check`. + +## Implementation Phases + +### Phase 1 - Repo Truth And Current UI Audit + +- Inspect Environment Dashboard page/widget/summary builder/tests. +- Inspect Baseline Compare Landing page/view/stats/tests. +- Inspect Spec 325 target images/briefs and UI audit reports. +- Create/update `repo-truth-map.md`. +- Confirm exact source/capability/fallback for every visible element. +- Stop if a desired visible element requires new backend truth. + +### Phase 2 - Environment Dashboard Productization + +- Add a first-read readiness question and decision card. +- Refine existing summary builder/widget payloads to expose status/reason/impact/proof/next-action without new state persistence. +- Add/right-size proof/action panel and ranked actions. +- Keep useful existing cards/details as secondary context. +- Collapse diagnostics/raw support by default. + +### Phase 3 - Baseline Compare Productization + +- Add a first-read drift/action question and decision card. +- Render no-baseline/no-snapshot/no-compare states as actionable unavailable states. +- Show assignment, compare trust, drift impact, evidence/proof, and operation proof before findings/raw details. +- Collapse raw diff/diagnostics by default. +- Preserve existing compare start action semantics and matrix/detail links. + +### Phase 4 - Route, RBAC, Copy, And Disclosure Guards + +- Verify route-owned environment context. +- Reject remembered fallback and legacy aliases. +- Guard cross-workspace mismatches. +- Gate/hide unavailable actions. +- Preserve dynamic names containing `Tenant`; remove static platform `tenant` copy. +- Ensure no false green claims. + +### Phase 5 - Browser Smoke And Screenshots + +- Add browser smoke file. +- Create required screenshots where generated. +- Confirm diagnostics collapsed and raw details absent by default. +- Confirm no context drift. + +### Phase 6 - Validation And Close-Out + +- Run focused Feature/Livewire/browser tests. +- Run targeted Spec 314-322 context guards. +- Run Pint and diff check. +- Report full suite status honestly. +- Record no migrations/packages/env/queues/storage/deployment assets/backcompat/legacy aliases. + +## Risk Controls + +- Treat Spec 325 images as direction only; map every visible element to repo truth. +- Use unavailable/deferred states instead of inventing proof. +- Do not broaden Baseline Compare Matrix scope. +- Do not turn Environment Dashboard into a generic M365/admin-center status page. +- Keep OperationRun as execution proof, not health proof. +- Keep raw diagnostics collapsed/capability-gated. + +## Rollout / Deployment Considerations + +- No migration expected. +- No seeders expected. +- No env var expected. +- No queue/scheduler/storage change expected. +- No package change expected. +- No deployment asset change expected unless implementation unexpectedly registers Filament assets; current expectation is no `filament:assets` change beyond existing deployment posture. +- Staging validation should focus on route-owned environment behavior, no-baseline/action-needed states, diagnostics collapsed, and no static `tenant` copy. + +## Candidate Selection Gate + +Passed. Spec 330 was directly user-provided, is aligned with Spec 325 strategic target surfaces and post-Spec 329 follow-up sequencing, is not already present as an active/completed package, preserves completed Specs 314-329, and is scoped to two existing environment-owned pages. + +## Spec Readiness Gate + +Expected pass after `spec.md`, `plan.md`, `tasks.md`, `repo-truth-map.md`, and `checklists/requirements.md` are generated and preparation analysis finds no blocking consistency issues. diff --git a/specs/330-environment-dashboard-baseline-compare-productization/repo-truth-map.md b/specs/330-environment-dashboard-baseline-compare-productization/repo-truth-map.md new file mode 100644 index 00000000..4ad61ca5 --- /dev/null +++ b/specs/330-environment-dashboard-baseline-compare-productization/repo-truth-map.md @@ -0,0 +1,118 @@ +# Spec 330 Repo Truth Map + +Status: implemented +Created: 2026-05-19 +Purpose: classify each Environment Dashboard and Baseline Compare productization element before runtime implementation. + +## Classification Legend + +- `repo-verified`: exact runtime source exists and was inspected. +- `foundation-real`: backend model/service/policy exists, but exact page binding still needs implementation verification. +- `derived from existing model`: display value can be derived from existing persisted/domain truth. +- `empty/unavailable state`: no safe source/action exists for v1; show explicit unavailable state or omit. +- `deferred future capability`: outside Spec 330 and must not be shown as live runtime truth. + +## Required Data Areas + +| Data area | Repo source | Preparation finding | Classification | +|---|---|---|---| +| ManagedEnvironment | `App\Models\ManagedEnvironment`, `ManagedEnvironmentLinks`, environment routes | Environment-owned route, workspace relation, display name, slug, provider links are repo-real | repo-verified | +| Provider connection/readiness | `ProviderConnection`, `EnvironmentDashboardSummaryBuilder::primaryProviderConnection()`, provider health fields | Existing primary provider chip/health/readiness sources exist; page binding already used by dashboard cards | repo-verified | +| Required permissions | `ManagedEnvironmentPermission`, `ManagedEnvironmentRequiredPermissionsViewModelBuilder`, required permissions route | Existing dashboard summary derives missing application/delegated permissions and action links | repo-verified | +| Backup sets | `BackupSet`, `TenantBackupHealthResolver`, backup-set resources | Backup health/posture is existing repo truth; exact dashboard wording must stay derived | repo-verified | +| Restore runs | `RestoreRun`, `RestoreSafetyResolver`, restore-run resources | Recovery proof/restore safety exists as derived dashboard evidence; no new recovery engine | foundation-real | +| Recovery proof | `RestoreSafetyResolver::dashboardRecoveryEvidence()`, `TenantRecoveryTriagePresentation` | Current dashboard already considers recovery evidence state in posture | repo-verified | +| Baseline profile assignment | `BaselineTenantAssignment`, `BaselineProfile` | Baseline Compare stats resolve assignment/no-assignment state | repo-verified | +| Baseline snapshots | `BaselineSnapshot`, `BaselineSnapshotTruthResolver` | Compare availability depends on consumable active/effective snapshot | repo-verified | +| Baseline compare result | `BaselineCompareStats`, `OperationRun`, compare summary/outcome fields | Compare state, latest run, coverage, evidence gaps, findings, failure state are existing truth | repo-verified | +| Drift findings | `Finding`, `FindingResource`, `BaselineCompareStats::findingAttentionCounts()` | Findings/severity and attention counts exist; use only scoped/authorized links | repo-verified | +| Risk exceptions / accepted risks | `FindingException`, `FindingExceptionResource`, exception stats in summary builder | Dashboard already derives exception stats; accepted-risk copy must remain honest | repo-verified | +| Evidence snapshots | `EvidenceSnapshot`, `EvidenceSnapshotResource`, latest evidence snapshot in summary builder | Dashboard can link/show latest evidence; Baseline Compare has evidence gap details from runs | repo-verified | +| Review packs | `ReviewPack`, `ReviewPackResource`, dashboard latest review pack | Dashboard summary uses latest review pack/customer output card | repo-verified | +| Environment reviews | `EnvironmentReview`, `EnvironmentReviewResource`, `EnvironmentReviewComposer` | Latest/current review cards and links exist | repo-verified | +| OperationRuns | `OperationRun`, `OperationRunLinks`, `OperationUxPresenter` | Existing execution proof and attention summaries exist; OperationRun is proof/context, not health certification | repo-verified | +| Audit links if present | `AuditLog`, related resource/action records | Audit trail exists but Environment Dashboard/Baseline Compare direct audit links must be implementation-verified before display | foundation-real | + +## UI Element Map + +| UI element | Surface | Source model/service/page | Status source | Authorization/capability | Workspace/Environment scope | OperationRun/evidence/baseline/drift link | Fallback/empty state | Classification | +|---|---|---|---|---|---|---|---|---| +| Environment Dashboard route | Environment Dashboard | `EnvironmentDashboard`, `ManagedEnvironmentLinks::viewUrl()` | route binding | environment membership + `TENANT_VIEW` | explicit `/admin/workspaces/{workspace}/environments/{environment}` | child resource links | 404 when inaccessible | repo-verified | +| Environment title/context | Environment Dashboard | `EnvironmentDashboard::getTitle()`, context chips widget | environment/workspace/provider records | page access | route-bound environment | latest activity from provider/review/evidence/operations | title without provider/latest activity when missing | repo-verified | +| Main readiness question | Environment Dashboard | new view copy over existing widget | static copy + summary payload | page access | route-bound environment | none | still show question with unavailable state | derived from existing model | +| Readiness status | Environment Dashboard | `EnvironmentDashboardSummaryBuilder::posture()` | missing permissions, aggregate, backup health, recovery evidence, actions | page access | route-bound environment | links via recommended actions | `Action needed` or `Unavailable` | repo-verified | +| Readiness reason | Environment Dashboard | recommended action reason or aggregate headline | existing action/aggregate strings | page access | route-bound environment | action target | `Reason unavailable` | derived from existing model | +| Readiness impact | Environment Dashboard | recommended action impact or aggregate supporting message | existing action/aggregate strings | page access | route-bound environment | action target | `Impact unavailable` | derived from existing model | +| Primary next action | Environment Dashboard | `recommendedActions[0]` and header action | route/action URL + capability | existing resource policies/capabilities | route-bound environment | provider/backup/evidence/review/operation/baseline links | disabled/unavailable when no safe action | repo-verified | +| Ranked next actions | Environment Dashboard | `EnvironmentDashboardSummaryBuilder::recommendedActions()` | priorities and candidate actions | existing resource policies/capabilities | route-bound environment | multiple child routes | empty calm/unavailable state | repo-verified | +| Provider readiness dimension | Environment Dashboard | `ProviderConnection`, required permissions VM | provider health/status/check timestamps | provider/permission capabilities | route-bound environment | required permissions route | `Provider readiness unavailable` | repo-verified | +| Required permissions dimension | Environment Dashboard | permissions VM overview counts | missing application/delegated counts | required permissions visibility | route-bound environment | required permissions route | `Permissions unavailable` | repo-verified | +| Backup posture dimension | Environment Dashboard | `TenantBackupHealthResolver` | backup health posture/headline | backup capabilities | route-bound environment | backup-set/schedule routes | `Backup proof missing` | repo-verified | +| Recovery proof dimension | Environment Dashboard | `RestoreSafetyResolver`, recovery presentation | recovery evidence state | restore/backup capabilities | route-bound environment | restore/backup routes | `Recovery proof unavailable` / stale | foundation-real | +| Baseline assignment dimension | Environment Dashboard | `BaselineTenantAssignment`, aggregate | assignment/compare summary | baseline/profile capabilities | route-bound environment | Baseline Compare route | `Baseline not assigned` | derived from existing model | +| Baseline compare / drift dimension | Environment Dashboard | `TenantGovernanceAggregateResolver`, `BaselineCompareStats` | aggregate family/headline/counts | finding/baseline visibility | route-bound environment | Baseline Compare / findings | `Compare unavailable` | repo-verified | +| Evidence freshness dimension | Environment Dashboard | latest `EvidenceSnapshot` | generated/expires/status fields | evidence capability/policy | route-bound environment | evidence snapshot route | `Evidence unavailable` | repo-verified | +| Review freshness dimension | Environment Dashboard | latest `EnvironmentReview` | published/generated/updated timestamps | review capability/policy | route-bound environment | review route/customer workspace | `No recent review` | repo-verified | +| Accepted risks dimension | Environment Dashboard | `FindingException` stats | active/expiring/expired/pending counts | finding exception capability | route-bound environment | finding exceptions route | `No accepted risk record` / unavailable | repo-verified | +| Operations requiring attention | Environment Dashboard | `OperationRun` attention query | problem class/freshness/outcome | operation visibility | route-bound environment | operation detail/hub | no attention summary | repo-verified | +| Right proof/action panel | Environment Dashboard | page/widget payload over sources above | source-specific availability | per-link capability | route-bound environment | operation/evidence/review/backup/provider links | explicit unavailable rows | derived from existing model | +| Support diagnostics disclosure | Environment Dashboard | existing support diagnostics action/modal | support diagnostics bundle | `SUPPORT_DIAGNOSTICS_VIEW` | route-bound environment | audit log via existing action | hidden/disabled when unauthorized | repo-verified | +| Raw provider payloads | Environment Dashboard | provider raw payloads/logs | sensitive raw data | support-only future | N/A | N/A | never default-visible | deferred future capability | +| Baseline Compare route | Baseline Compare | `BaselineCompareLanding`, slug route | route binding | environment membership + `TENANT_VIEW` | explicit `/admin/workspaces/{workspace}/environments/{environment}/baseline-compare` | baseline/run/finding links | 404 when inaccessible | repo-verified | +| Baseline Compare generated URL | Baseline Compare | `BaselineCompareLanding::getUrl()`, `ManagedEnvironmentLinks::baselineCompareUrl()` | URL helper | environment access | route-bound environment | preserves baseline profile/subject/nav launch context | `/admin` fallback when no environment | repo-verified | +| Legacy alias stripping | Baseline Compare | `LEGACY_SCOPE_QUERY_KEYS`, `withoutLegacyScopeQuery()` | query key removal | page access | aliases do not set scope | none | query ignored/route rejected | repo-verified | +| Remembered fallback rejection | Baseline Compare | `BaselineCompareEnvironmentRouteContractTest`, `canAccess()` | route-owned environment | environment access | explicit route only | none | 404/false access | repo-verified | +| Main drift question | Baseline Compare | new view copy over existing page | static copy + stats payload | page access | route-bound environment | none | still show question with unavailable state | derived from existing model | +| Assigned baseline state | Baseline Compare | `BaselineCompareStats`, `BaselineTenantAssignment`, `BaselineProfile` | assignment/profile fields | baseline/profile visibility | route-bound environment | baseline profile/matrix routes | `Baseline not assigned` | repo-verified | +| Snapshot/compare availability | Baseline Compare | `BaselineSnapshotTruthResolver`, `BaselineCompareStats` | effective snapshot/reason code | page access | route-bound environment | capture/run proof if exists | `No complete snapshot` / unavailable | repo-verified | +| Compare trust | Baseline Compare | operator explanation, summary assessment, coverage/evidence gap fields | trust/evaluation/coverage state | page access | route-bound environment | run/evidence gap links | `Compare unavailable` | repo-verified | +| Drift impact | Baseline Compare | findings count/severity counts, aggregate | `Finding` and stats counts | finding visibility | route-bound environment | findings route/matrix | `No drift result` / `No usable result` | repo-verified | +| Reason | Baseline Compare | `reasonCode`, `reasonMessage`, operator explanation | reason translation / stats | page access | route-bound environment | run details | `Reason unavailable` | repo-verified | +| Evidence path | Baseline Compare | evidence gaps, snapshot, OperationRun, findings | stats fields/run context | page/finding/run/evidence capabilities | route-bound environment | operation/finding/matrix/evidence gap details | `Evidence unavailable` | foundation-real | +| Primary next action | Baseline Compare | page-local derived from state + existing actions | state and authorized route/action | `TENANT_SYNC` for compare start; resource policies for links | route-bound environment | compare/run/baseline/findings/matrix links | unavailable when no safe action | derived from existing model | +| No-baseline compare readiness flow | Baseline Compare | `BaselineCompareLanding` page-local payload | missing assignment, snapshot id, latest inventory/evidence proof, compare run availability | page access; links stay in decision card | route-bound environment | no new operation | show ordered visual stepper/pipeline with missing/unavailable states only | derived from existing model | +| No-baseline available inputs | Baseline Compare | `InventoryItem`, latest completed inventory sync `OperationRun`, compare `operationRunId`, baseline assignment/snapshot state | repo-backed availability labels | page access | route-bound environment | environment snapshot / operation proof / baseline snapshot availability | unavailable when no repo-backed input exists | derived from existing model | +| Assignment unlocks copy | Baseline Compare | static explanatory copy bounded to assignment outcome | no runtime health/drift claim | page access | route-bound environment | none | omit outside no-baseline state | derived from existing model | +| Compare now action | Baseline Compare | `compareNowAction()` | state in idle/ready/failed | `TENANT_SYNC`, `UiEnforcement` | route-bound environment | creates `OperationRun` via service | disabled when not startable | repo-verified | +| Operation proof link | Baseline Compare | `operationRunId`, `getRunUrl()`, `OperationRunLinks` | latest compare run | operation visibility | route-bound environment | operation detail | `Operation proof unavailable` | repo-verified | +| Findings/diff summary | Baseline Compare | findings/severity counts, evidence gaps | stats fields | finding visibility | route-bound environment | findings route/matrix | no findings / unavailable with trust caveat | repo-verified | +| Evidence gap details | Baseline Compare | `BaselineCompareEvidenceGapDetails`, include partial | run diagnostics/gap details | page access; raw remains secondary | route-bound environment | detail section only | hidden when no gaps | repo-verified | +| Raw compare diagnostics | Baseline Compare | `baselineCompareDiagnostics`, JSON viewer include | diagnostics array from run | support/raw capability or collapsed default | route-bound environment | run details | collapsed/hidden by default | foundation-real | +| Raw diff payload | Baseline Compare | raw compare output/diagnostics | not safe default | support-only future | route-bound environment | run detail if authorized | never default-visible | deferred future capability | +| Static tenant copy guard | Both | runtime copy/tests | string assertions | N/A | route-bound environment copy uses Environment | N/A | dynamic names allowed | repo-verified | + +## Required Runtime Element Decisions + +| Element | v1 decision | +|---|---| +| New environment readiness engine | deferred future capability; do not build | +| New persisted readiness status | deferred future capability; do not build | +| New baseline/drift engine | deferred future capability; do not build | +| New evidence generator | deferred future capability; do not build | +| New backup/recovery proof engine | deferred future capability; do not build | +| Generic health/compliance/protected/customer-safe badge | deferred future capability; do not show unless exact repo proof supports the exact claim | +| Environment readiness status | derive from existing dashboard summary, backup, recovery, permissions, baseline, evidence, review, risk, and operation truth | +| Baseline assignment state | derive from existing `BaselineTenantAssignment` and `BaselineProfile` | +| Compare availability/trust | derive from existing `BaselineCompareStats`, snapshot truth, run state, coverage, evidence gaps, and operator explanation | +| Drift impact | derive from existing findings/severity/evidence gap state | +| Operation proof | link only through existing run relations/helpers and authorization | +| No-baseline readiness flow | visualize required pipeline inputs, dependency order, current blocker, and current availability only; do not show fake drift data or duplicate lower summary cards | +| Diagnostics | collapsed/hidden by default and capability-aware if exposed | +| Raw provider payloads / raw diff | never default-visible | +| Dangerous/mutating actions | do not add; existing compare start remains confirmed/capability-gated/OperationRun-backed | +| Legacy query aliases | rejected/neutralized; do not support | + +## Implementation Update Rule + +If implementation discovers that a planned UI element has no safe source, no authorization path, or would require new persisted truth, the element must become `empty/unavailable state` or `deferred future capability`. Do not create backend foundation inside Spec 330 without updating `spec.md`, `plan.md`, `tasks.md`, and this map first. + +## Close-out Coverage Note + +- Runtime changes stayed within the existing UI-002 Environment Dashboard and UI-061 Baseline Compare strategic surfaces. The no-baseline Compare readiness flow is a page-local visual stepper over existing assignment, snapshot, inventory/evidence, and OperationRun truth. No route archetype, panel provider, global search, navigation family, persisted entity, enum/status family, migration, package, queue, scheduler, env var, storage, deployment asset, or compatibility route was added. +- The existing UI coverage registry remains sufficient because Spec 330 carries the feature-local truth map, focused Feature/Livewire coverage, explicit browser smoke, and screenshots under `specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/`. +- Final screenshot artifacts: + - `environment-dashboard-readiness-workbench.png` + - `environment-dashboard-action-needed.png` + - `baseline-compare-no-baseline.png` + - `baseline-compare-decision-workbench.png` +- Final classification: implemented elements are derived from existing repository truth. Raw provider payloads, raw compare diagnostics, raw diff, and raw OperationRun context remain hidden/collapsed by default and are not treated as default operator evidence. diff --git a/specs/330-environment-dashboard-baseline-compare-productization/spec.md b/specs/330-environment-dashboard-baseline-compare-productization/spec.md new file mode 100644 index 00000000..e8d61e8e --- /dev/null +++ b/specs/330-environment-dashboard-baseline-compare-productization/spec.md @@ -0,0 +1,464 @@ +# Feature Specification: Spec 330 - Environment Dashboard / Baseline Compare Productization + +**Feature Branch**: `330-environment-dashboard-baseline-compare-productization` +**Created**: 2026-05-19 +**Status**: Draft +**Type**: Runtime UI productization / environment readiness / baseline drift decision surface +**Runtime posture**: Narrow runtime UI implementation. Repo-based. No invented backend foundation. +**Input**: User-provided full Spec 330 draft. + +## Dependencies And Historical Context + +Depends on: + +- Spec 314 - Workspace Hub Navigation Context Contract. +- Spec 315 - Environment CTA Explicit Filter Contract. +- Spec 316 - Workspace Hub Clear Filter Contract. +- Spec 317 - Legacy Tenant / Environment Context Cleanup. +- Spec 318 - Admin Surface Scope & Shell Context Audit. +- Spec 319 - Environment-Owned Surface Routing & Shell Context Contract. +- Spec 320 - Workspace-Owned Analysis Surface Registration & Shell Cutover. +- Spec 321 - Alerts / Audit Log Environment Filter Contract Decision. +- Spec 322 - Browser No-Drift Regression Guard. +- Spec 325 - Screenshot-Anchored Strategic Target Images. +- Spec 326 - Customer Review Workspace v1 Productization. +- Spec 327 - Governance Inbox Decision-First Workbench Productization. +- Spec 328 - Operations Hub Decision-First Workbench Productization. +- Spec 329 - Evidence / Audit Log Disclosure Productization. + +Repo truth adjustment: the user draft named likely files under `resources/views/filament/pages/environment-dashboard.blade.php` and `app/Filament/Pages/Governance/BaselineCompare.php`. Current repository truth is: + +- Environment Dashboard page class: `apps/platform/app/Filament/Pages/EnvironmentDashboard.php`. +- Environment Dashboard layout widget: `apps/platform/app/Filament/Widgets/Dashboard/EnvironmentDashboardOverview.php`. +- Environment Dashboard main view: `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php`. +- Environment Dashboard context chips view: `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-context-chips.blade.php`. +- Existing summary builder: `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php`. +- Baseline Compare page class: `apps/platform/app/Filament/Pages/BaselineCompareLanding.php`. +- Baseline Compare view: `apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php`. +- Baseline Compare Matrix exists separately at `apps/platform/app/Filament/Pages/BaselineCompareMatrix.php` and is out of scope except as an existing downstream analysis/detail route. + +Spec 325 target images and briefs are visual calibration only. They are not runtime truth for readiness, health, drift severity, evidence freshness, proof availability, certification, RBAC, or diagnostic access. + +## Spec Candidate Check *(mandatory - SPEC-GATE-001)* + +- **Problem**: Environment Dashboard and Baseline Compare are repo-real, environment-owned surfaces, but the first read can still force operators to reconcile backup, recovery, provider, evidence, review, operation, assignment, compare, and drift signals instead of answering one decision question. +- **Today's failure**: Operators can misread technical status, raw compare/diagnostic state, OperationRun completion, or a zero-finding compare as broad environment readiness. Missing baseline, missing proof, stale recovery, or evidence gaps can be visible but not framed as the decision blocker and next action. +- **User-visible improvement**: Operators can immediately see status, reason, impact, proof path, and one primary next action for environment readiness and baseline drift before opening diagnostics or raw details. +- **Smallest enterprise-capable version**: Productize only the existing Environment Dashboard and Baseline Compare Landing using existing models, services, links, policies, capabilities, widgets, and Blade views. Existing tables/details remain secondary. +- **Explicit non-goals**: No new baseline engine, drift engine, backup engine, restore engine, evidence generator, provider readiness engine, AI summary, dashboard scoring system, migration, seed, package, env var, queue, scheduler, storage change, compatibility route, or legacy query alias support. +- **Permanent complexity imported**: Feature-local layout/payload helpers if needed, targeted Pest Feature/Livewire tests, one Pest Browser smoke file, screenshots, and `repo-truth-map.md`. No new persisted truth, public abstraction, enum/status family, taxonomy, or cross-domain UI framework. +- **Why now**: Specs 314-322 stabilized workspace/environment context and no-drift guards. Specs 326-329 established the decision-first productization pattern. Spec 325 identifies Environment Dashboard and Baseline Compare / Drift as P0 strategic surfaces that still need runtime productization. +- **Why not local**: Small copy or card tweaks would not make the first decision explicit across readiness, proof, action, and diagnostics. A new backend readiness engine would overbuild. The narrow correct slice is page-local productization over existing repo truth. +- **Approval class**: Core Enterprise. +- **Red flags triggered**: Strategic UI productization and presentation semantics across two surfaces. Defense: scope is limited to two existing pages, forbids new backend/state frameworks, and prevents false health/compliance/customer-safe claims. +- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 12/12** +- **Decision**: approve. + +## Candidate Source And Completed-Spec Guardrail + +- **Candidate source**: Direct user-provided manual promotion for Spec 330, aligned with `docs/ui-ux-enterprise-audit/strategic-surfaces.md`, `grouped-follow-up-candidates.md`, and `follow-up-specs/325-strategic-target-image-implementation-candidates.md`. +- **Current package check**: No `specs/330-*` package, local branch, or remote branch existed before this preparation run. +- **Related completed-spec check**: Specs 314-329 include historical/completed foundation and productization signals. They are dependency context only and must not be rewritten by Spec 330. +- **Close alternatives deferred**: Restore Safety Workflow Productization, Provider Readiness Productization, and Sidebar / Shell Native Filament Polish remain follow-up candidates 331-333. +- **Smallest viable implementation slice**: Existing Environment Dashboard and Baseline Compare Landing only: header/scope, decision card, summary cards, right-side proof/action/disclosure panel, ranked actions, secondary context, collapsed diagnostics, environment-owned route tests, RBAC-aware links/actions, and browser smoke. + +## Spec Scope Fields *(mandatory)* + +- **Scope**: environment-owned runtime UI surfaces. +- **Primary Routes**: + - Environment Dashboard route: `/admin/workspaces/{workspace}/environments/{environment}`. + - Environment Dashboard route name: `admin.workspace.environments.show`. + - Environment Dashboard page class: `apps/platform/app/Filament/Pages/EnvironmentDashboard.php`. + - Environment Dashboard widget/view: `apps/platform/app/Filament/Widgets/Dashboard/EnvironmentDashboardOverview.php`, `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php`. + - Baseline Compare route: `/admin/workspaces/{workspace}/environments/{environment}/baseline-compare`. + - Baseline Compare route name: `filament.admin.pages.workspaces.{workspace}.environments.{environment}.baseline-compare`. + - Baseline Compare page class: `apps/platform/app/Filament/Pages/BaselineCompareLanding.php`. + - Baseline Compare view: `apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php`. +- **Data Ownership**: + - Environment-owned records and observed state: `ManagedEnvironment`, `ProviderConnection`, `ManagedEnvironmentPermission`, `BackupSet`, `BackupSchedule`, `RestoreRun`, `BaselineTenantAssignment`, `BaselineProfile`, `BaselineSnapshot`, `Finding`, `FindingException`, `EvidenceSnapshot`, `EnvironmentReview`, `ReviewPack`, and `OperationRun`. + - Workspace-owned standards/configuration: `Workspace`, `BaselineProfile`, report/review definitions where existing. + - No new table or persisted state is introduced. +- **RBAC**: + - Workspace membership and environment entitlement are required. + - Environment Dashboard view uses existing `TENANT_VIEW` / member checks and existing capabilities for linked actions. + - Baseline Compare view requires existing environment access and `TENANT_VIEW`; compare start uses existing `TENANT_SYNC` capability and `UiEnforcement`. + - Provider readiness/permissions, backup posture, restore/recovery proof, baseline profiles/compare, findings, evidence, operations, reviews, review packs, support diagnostics, and raw diagnostics must use existing policies/capabilities. + +For canonical-view specs: N/A. Both primary surfaces are environment-owned, not clean workspace-wide canonical views. + +## UI Surface Impact *(mandatory - UI-COV-001)* + +Does this spec add, remove, rename, or materially change any reachable UI surface? + +- [ ] No UI surface impact +- [x] Existing page changed +- [ ] New page/route added +- [ ] Navigation changed +- [ ] Filament panel/provider surface changed +- [ ] New modal/drawer/wizard/action added +- [x] New table/form/state added +- [ ] Customer-facing surface changed +- [ ] Dangerous action changed +- [x] Status/evidence/review presentation changed +- [x] Workspace/environment context presentation changed + +## UI/Productization Coverage + +- **Route/page/surface**: Environment Dashboard, EnvironmentDashboardOverview widget/view, Baseline Compare Landing, and its Blade view. +- **Current or new page archetype**: Environment Dashboard = Overview / Dashboard, UI-002; Baseline Compare = Drift / Diff, UI-015 / strategic UI-061. +- **Design depth**: Strategic Surface. +- **Repo-truth level**: repo-verified routes/pages/model foundations; individual runtime elements are classified in `repo-truth-map.md`. +- **Existing pattern reused**: Filament Page/Dashboard/Widget, Filament Sections, Filament actions, badges, existing `EnvironmentDashboardSummaryBuilder`, `TenantGovernanceAggregateResolver`, `BaselineCompareStats`, `OperationRunLinks`, `ManagedEnvironmentLinks`, `BadgeRenderer`, `UiEnforcement`, existing policies/capabilities, and existing target images/briefs from Spec 325. +- **New pattern required**: no new runtime framework; page-local decision/proof panel composition only. +- **Screenshot required**: yes, Browser smoke screenshots under `specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/`. +- **Page audit required**: no new full audit unless implementation changes route/archetype/coverage classification. Existing UI-002 and UI-015 reports plus Spec 325 target artifacts remain the reference. +- **Customer-safe review required**: medium. These are operator pages, but readiness and drift copy may feed customer handoff. Do not claim customer-safe, healthy, compliant, protected, or complete unless exact repo proof supports it. +- **Dangerous-action review required**: no new dangerous action expected. Existing compare start remains high-impact and must keep `Action::make(...)->action(...)`, `->requiresConfirmation()`, capability enforcement, OperationRun, notifications, and tests. +- **Coverage files updated or explicitly not needed**: + - [ ] `docs/ui-ux-enterprise-audit/route-inventory.md` + - [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md` + - [ ] `docs/ui-ux-enterprise-audit/page-reports/...` + - [ ] `docs/ui-ux-enterprise-audit/strategic-surfaces.md` + - [ ] `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md` + - [ ] `docs/ui-ux-enterprise-audit/unresolved-pages.md` + - [ ] `N/A - no reachable UI surface impact` + - [x] Active spec package must carry repo-truth map, tests, browser screenshots, and close-out coverage decision. Registry updates are required only if runtime changes alter route/archetype/coverage classification. + +## Cross-Cutting / Shared Pattern Reuse + +- **Cross-cutting feature?**: yes. +- **Interaction class(es)**: dashboard signals/cards, status messaging, action links, header actions, evidence/proof links, OperationRun links, diagnostics disclosure, baseline compare state, empty states. +- **Systems touched**: Environment Dashboard page/widget/summary builder, Baseline Compare Landing page/view, baseline stats/aggregate helpers, backup/recovery support helpers, provider permission view models, OperationRun links, resource policies/capabilities, and browser fixtures/tests. +- **Existing pattern(s) to extend**: existing Environment Dashboard summary builder and widget, existing Baseline Compare stats/explanation pattern, existing OperationRun UX/link helpers, existing badge domains, existing `UiEnforcement` action gating, existing environment-owned routing helpers. +- **Shared contract / presenter / builder / renderer to reuse**: `EnvironmentDashboardSummaryBuilder`, `BaselineCompareStats`, `TenantGovernanceAggregateResolver`, `OperationRunLinks`, `ManagedEnvironmentLinks`, `BadgeRenderer`, `UiEnforcement`, and existing policies/capability resolvers. +- **Why the existing shared path is sufficient or insufficient**: Existing paths are sufficient for truth, links, RBAC, and baseline/backup/evidence calculations. They are insufficient only in first-read hierarchy and diagnostics disclosure ordering. +- **Allowed deviation and why**: bounded page-local payload/view helpers are allowed if needed to keep Blade and widgets reviewable. New readiness engines, proof engines, status families, or UI frameworks are not allowed. +- **Consistency impact**: Status, reason, impact, next action, proof, and diagnostics labels must align with related Operations, Evidence/Audit, Customer Review Workspace, and Governance Inbox language. +- **Review focus**: Verify no fake readiness/drift proof, no false green state, no raw diagnostics by default, no unauthorized links/actions, no shell-scope regression, no static platform `tenant` copy, and no duplicate local truth layer. + +## OperationRun UX Impact + +- **Touches OperationRun start/completion/link UX?**: link/proof presentation plus existing Baseline Compare start action only. No new OperationRun type, lifecycle transition, summary count writer, queue notification policy, or terminal notification path. +- **Shared OperationRun UX contract/layer reused**: `OperationRunLinks`, `OperationUxPresenter`, `OpsUxBrowserEvents`, existing operation policies, existing operation detail pages, and existing `BaselineCompareService::startCompare()`. +- **Delegated start/completion UX behaviors**: Baseline Compare start continues to delegate queued toast/browser event/run link to the existing Ops UX path. Environment Dashboard and proof panels only open existing operations. +- **Local surface-owned behavior that remains**: show `Operation proof available`, `Operation proof missing`, `Latest operation proof`, or equivalent based on existing linked runs. +- **Queued DB-notification policy**: unchanged / N/A. +- **Terminal notification path**: unchanged. +- **Exception required?**: none. + +## Provider Boundary / Platform Core Check + +- **Shared provider/platform boundary touched?**: display-only provider readiness/permissions and copy boundaries. +- **Boundary classification**: mixed display seam. Environment/workspace/readiness/drift terms are platform-core; Microsoft/Entra/Intune terms may remain only where the source provider record or external identity uses them. +- **Seams affected**: provider connection descriptors, required permissions, verification/readiness copy, baseline compare copy, static `tenant` language guards. +- **Neutral platform terms preserved or introduced**: workspace, environment, provider, required permissions, readiness proof, operation proof, evidence, review, baseline, drift, diagnostics. +- **Provider-specific semantics retained and why**: Microsoft/Entra/Intune/provider tenant ID can appear only where existing provider-boundary truth requires it. +- **Why this does not deepen provider coupling accidentally**: no Graph calls, provider contracts, provider persistence, provider taxonomy, or provider-specific readiness model is added. +- **Follow-up path**: Provider Readiness Productization remains a separate follow-up spec. + +## UI / Surface Guardrail Impact + +| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note | +|---|---:|---|---|---|---:|---| +| Environment Dashboard | yes | Filament Dashboard/Page plus existing widgets and Blade | dashboard signals, action links, proof/disclosure | shell, page, widget payload, route environment | no | Existing route only | +| EnvironmentDashboardOverview widget | yes | Existing Blade widget using Filament primitives | readiness cards, ranked actions, proof panel | widget payload | no | Refactor only | +| Baseline Compare Landing | yes | Filament Page plus existing Blade | compare state, OperationRun start/link, diagnostics | page, route environment, query launch context | no | Existing route only | +| Baseline Compare diagnostics/raw diff disclosure | yes | collapsed/progressive disclosure | support/raw detail | page payload/details | no | Collapsed by default | +| Existing compare/findings/details sections | yes | existing Filament/Blade sections | secondary context | page payload | no | Remain secondary | + +## Decision-First Surface Role + +| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction | +|---|---|---|---|---|---|---|---| +| Environment Dashboard | Primary Decision Surface | Operator decides whether an environment is ready, blocked, stale, or requiring review | status, reason, impact, proof path, one next action, readiness dimensions | operations, evidence, review packs, provider checks, backup/restore details, diagnostics | Primary because it is the environment command/readiness surface | Follows environment governance readiness, not raw admin status | Prevents scanning many equal cards | +| Baseline Compare Landing | Primary Decision Surface | Operator decides whether baseline drift requires action or compare is unavailable | assigned baseline, compare trust, drift impact, reason, proof, one next action | findings, evidence gaps, operation run detail, raw diff/diagnostics | Primary because it answers baseline drift/action for one environment | Follows baseline governance decision before matrix/detail | Prevents raw diff or zero findings from reading as all-clear | +| Existing detail sections/tables | Secondary Context | Operator investigates after first decision | concise findings, latest run, evidence gaps, assignment data | full findings, matrix, run detail, raw diagnostics | Secondary because they support the decision | Keeps current drilldowns | Reduces first-read noise | +| Raw diagnostics/raw diff | Tertiary Evidence / Diagnostics | Support/operator inspects technical data after proof path | collapsed availability only | raw JSON/diff/payloads, provider diagnostics, debug metadata | Not primary | Preserves support depth | Prevents raw-console default | + +## Audience-Aware Disclosure + +| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention | +|---|---|---|---|---|---|---|---| +| Environment Dashboard | operator-MSP, manager, support reviewer | readiness question, status, reason, impact, proof path, ranked next actions, readiness dimensions | operations requiring attention, provider check details, evidence/review/backup details | raw provider payloads, support diagnostics bundle, raw run context | open the primary readiness blocker/action | raw payloads, provider secrets, stack traces, debug metadata, unsupported health claims | top card states readiness once; cards add proof not duplicate verdict | +| Baseline Compare Landing | governance operator, manager, support reviewer | drift question, assigned baseline, compare trust, drift impact, reason, proof, primary next action | findings breakdown, evidence gap buckets, operation proof | raw diff, raw diagnostics, raw OperationRun context, provider payloads | assign/open baseline, run compare, or review drift depending on state | raw diff/payloads/debug/provider detail | top card states drift decision once; sections add evidence/details | + +## UI/UX Surface Classification + +| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| Environment Dashboard | Workbench / Dashboard | Environment readiness command surface | Open the readiness blocker or proof path | explicit primary action plus existing linked cards | N/A | right-side panel / secondary cards | none introduced | `/admin/workspaces/{workspace}/environments/{environment}` | same route plus existing child routes | active workspace and route-bound environment | Environment Dashboard | readiness status, reason, impact, proof, next action | none | +| Baseline Compare Landing | Workbench / Drift | Environment baseline compare decision surface | Run compare, assign/open baseline, or review drift | explicit primary action plus existing findings/run/matrix links | N/A | proof panel and secondary detail sections | compare start remains confirmed action | `/admin/workspaces/{workspace}/environments/{environment}/baseline-compare` | same route plus existing matrix/findings/run routes | active workspace and route-bound environment | Baseline Compare | baseline assignment, compare trust, drift impact, evidence/proof | existing special compare action | +| Diagnostics disclosure | Diagnostics / Support Raw | Collapsed technical context | Expand/open diagnostics if authorized | disclosure/detail action | N/A | below proof panel | none | same pages | authorized detail/support surfaces | route-bound environment | Diagnostics | collapsed status only | none | + +## Operator Surface Contract + +| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions | +|---|---|---|---|---|---|---|---|---|---|---| +| Environment Dashboard | MSP operator / manager | Decide whether environment is ready, blocked, stale, or needs review | Environment readiness workbench | Is this environment ready, blocked, stale, or requiring review? | workspace/environment/provider, status, reason, impact, proof path, readiness dimensions, ranked next actions | raw provider payloads, support diagnostics, raw OperationRun context, debug metadata | provider readiness, permissions, backup posture, recovery proof, baseline/drift, evidence freshness, review freshness, accepted risk, operation attention | mostly navigation; support request remains TenantPilot/external handoff per existing action | open primary blocker/proof path | none introduced | +| Baseline Compare Landing | Governance operator | Decide whether drift requires action or compare cannot be trusted | Baseline drift workbench | Which baseline drift requires action? | assigned baseline, compare trust, drift impact, reason, evidence/proof, primary next action | raw diff, evidence gap diagnostics, raw run context, provider payloads | assignment, compare availability, execution outcome, result trust, drift impact, evidence gap, operation proof | compare start queues TenantPilot operation / simulation compare against current observed state | run compare, open baseline profiles/matrix/findings/run depending on state | compare start remains high-impact confirmed action | + +## Proportionality Review + +- **New source of truth?**: no. +- **New persisted entity/table/artifact?**: no. `repo-truth-map.md` is a Spec Kit preparation artifact, not runtime truth. +- **New abstraction?**: no public abstraction. Page-local private helpers are allowed only when they reduce Blade complexity and stay feature-local. +- **New enum/state/reason family?**: no domain state. Display states must derive from existing model/service truth. +- **New cross-domain UI framework/taxonomy?**: no. +- **Current operator problem**: Existing environment and compare pages must answer readiness/drift decisions without forcing raw technical reconstruction or implying false green states. +- **Existing structure is insufficient because**: Current pages already expose much of the truth but do not consistently enforce the status/reason/impact/proof/next-action hierarchy or diagnostics collapsed by default. +- **Narrowest correct implementation**: Refactor existing page/widget/view payloads and Blade layout, bind to existing sources, keep diagnostics collapsed, and add targeted tests/browser smoke. +- **Ownership cost**: Feature-local layout/payload tests, one Browser smoke, screenshots, and spec truth map. No durable backend model or new framework cost. +- **Alternative intentionally rejected**: new readiness scoring engine, baseline/drift backend rebuild, provider readiness foundation, recovery-proof engine, UI framework, AI summary, broad design system work, or route replacement. +- **Release truth**: current-release runtime UI productization over existing environment and baseline foundations. + +### Compatibility posture + +This feature assumes pre-production runtime posture. Backward compatibility, historical aliases, migration shims, dual-read logic, legacy route redirects, and legacy query aliases are out of scope. Existing legacy query aliases (`tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, `tableFilters`) must not establish environment ownership or valid compare/dashboard context. + +## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)* + +- **Test purpose / classification**: Feature, Livewire, Browser. +- **Validation lane(s)**: confidence and browser; targeted context guard filters for Specs 314-322. +- **Why this classification and these lanes are sufficient**: The change is page/view hierarchy, route ownership, RBAC-visible actions, and diagnostics disclosure. Feature/Livewire tests prove rendered contracts and authorization; Browser smoke proves end-to-end page framing and no context drift. +- **New or expanded test families**: one explicit Browser smoke file `Spec330EnvironmentDashboardBaselineCompareSmokeTest.php`; focused Feature/Livewire tests under existing Filament/Dashboard/Baseline families. +- **Fixture / helper cost impact**: use existing factories and baseline compare fixture helpers. Do not broaden default helpers or seeders. +- **Heavy-family visibility / justification**: Browser coverage is explicit because two strategic surfaces require visual/context smoke and screenshots. +- **Special surface test profile**: `global-context-shell`, `monitoring-state-page`, `shared-detail-family`. +- **Standard-native relief or required special coverage**: special coverage required because these are strategic environment-owned decision surfaces with diagnostics disclosure. +- **Reviewer handoff**: Review lane fit, hidden fixture cost, route ownership, RBAC gating, static `tenant` copy guard, false green guard, and raw diagnostics hidden by default. +- **Budget / baseline / trend impact**: none expected; browser file is explicit and scoped. +- **Escalation needed**: document-in-feature if implementation discovers an existing shared path is insufficient; follow-up-spec only for structural readiness/provider/restore gaps. +- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage. +- **Planned validation commands**: + - `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Filament tests/Feature/Rbac tests/Feature/Navigation --filter='EnvironmentDashboard|TenantDashboard|BaselineCompare|EnvironmentOwned|LegacyTenant|Spec322' --compact` + - `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec330EnvironmentDashboardBaselineCompareSmokeTest.php --compact` + - `cd apps/platform && ./vendor/bin/sail artisan test --filter='EnvironmentDashboard|BaselineCompare|AdminSurfaceScope|EnvironmentOwned|LegacyTenant|Spec322' --compact` + - `cd apps/platform && ./vendor/bin/sail pint --dirty` + - `git diff --check` + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Environment readiness is decidable (Priority: P1) + +As an MSP operator, I want the Environment Dashboard to answer whether the environment is ready, blocked, stale, or needs review before I inspect technical details. + +**Why this priority**: Environment Dashboard is the command surface for environment governance and customer handoff readiness. + +**Independent Test**: Render an environment with missing proof/actions and assert the first-read shows the readiness question, status, reason, impact, proof path, next action, ranked actions, and collapsed diagnostics. + +**Acceptance Scenarios**: + +1. **Given** an authorized operator opens an explicit environment route, **When** the dashboard renders, **Then** it shows `Is this environment ready, blocked, stale, or requiring review?` before secondary details. +2. **Given** backup/evidence/baseline proof is missing or stale, **When** the dashboard renders, **Then** it shows an honest action-needed state and does not show false `Healthy`, `Fully ready`, `Protected`, `Compliant`, or `Customer-safe` claims. +3. **Given** a linked proof/action is not authorized, **When** the dashboard renders, **Then** the action is hidden or represented as unavailable without leaking protected details. + +### User Story 2 - Baseline drift is decidable (Priority: P1) + +As a governance operator, I want Baseline Compare to answer which drift requires action or what prevents comparison before I inspect raw diff details. + +**Why this priority**: Baseline Compare directly supports governance decisions and can otherwise be misread as raw technical output or implicit all-clear. + +**Independent Test**: Render no-baseline, stale/missing compare, and drift states and assert the first-read shows assignment, compare trust, drift impact, evidence/proof, next action, and collapsed raw diff/diagnostics. For no-baseline, assert a compact visual Compare readiness stepper/pipeline and available-inputs section explain the missing compare chain without reintroducing duplicated summary cards. + +**Acceptance Scenarios**: + +1. **Given** an environment has no assigned baseline, **When** Baseline Compare renders, **Then** it shows `Baseline not assigned`, the reason, impact, a repo-real next action or unavailable state, a five-step `Compare readiness flow` rendered as an ordered visual pipeline, compact `Available inputs`, and no duplicated `Assigned baseline` / `Compare trust` / `Drift impact` lower summary blocks. +2. **Given** a compare result exists with findings or evidence gaps, **When** Baseline Compare renders, **Then** it shows drift impact and evidence state before raw details. +3. **Given** compare diagnostics exist, **When** the page initially renders, **Then** raw diff/diagnostics are collapsed or hidden by default. + +### User Story 3 - Environment ownership and legacy aliases stay sealed (Priority: P2) + +As a platform owner, I want both pages to require explicit environment route ownership so workspace hub filters, remembered environment context, and legacy tenant aliases cannot create false scope. + +**Why this priority**: Specs 314-322 are the safety foundation for these pages. Productization must not reopen context drift. + +**Independent Test**: Exercise explicit environment route, clean workspace-style URL, remembered environment fallback, cross-workspace environment route, and legacy aliases for both pages. + +**Acceptance Scenarios**: + +1. **Given** an authorized user opens `/admin/workspaces/{workspace}/environments/{environment}`, **When** the route matches the environment workspace, **Then** the Environment Dashboard renders with active environment context. +2. **Given** an authorized user opens `/admin/workspaces/{workspace}/environments/{environment}/baseline-compare`, **When** the route matches the environment workspace, **Then** Baseline Compare renders with active environment context. +3. **Given** only a remembered environment or legacy query alias exists, **When** either page is requested without route-owned environment context, **Then** the request does not establish environment ownership. +4. **Given** workspace route and environment route disagree, **When** either page is requested, **Then** access is denied as not found. + +## Functional Requirements + +- **FR-330-001**: Environment Dashboard MUST render a decision-first readiness question before technical detail. +- **FR-330-002**: Environment Dashboard MUST show default-visible `Status`, `Reason`, `Impact`, `Proof`, and `Next action` labels or final equivalent copy. +- **FR-330-003**: Environment Dashboard MUST derive readiness status from existing repo truth and MUST NOT create a persisted readiness status family. +- **FR-330-004**: Environment Dashboard MUST show compact readiness dimensions only where repo-supported or honestly unavailable. +- **FR-330-005**: Environment Dashboard MUST include a proof/action panel showing evidence, operation proof, review/review pack, provider check, backup/recovery proof, and diagnostics disclosure where repo-supported. +- **FR-330-006**: Environment Dashboard MUST show one primary next action and a ranked short list of additional repo-real actions. +- **FR-330-007**: Baseline Compare MUST render a decision-first drift/action question before raw compare details. +- **FR-330-008**: Baseline Compare MUST show assigned baseline, compare trust/availability, drift impact, evidence/proof, and primary next action when repo-supported. +- **FR-330-009**: Baseline Compare MUST render no-baseline and no-snapshot/no-compare states as actionable unavailable states, not empty technical states. +- **FR-330-009a**: Baseline Compare no-baseline state MUST render a compact visual `Compare readiness flow` stepper/pipeline with five ordered steps: `Baseline assigned`, `Baseline snapshot`, `Environment snapshot`, `Compare run`, and `Decision output`. It MUST show dependency/order indicators between steps, mark `Baseline assigned` as the current `Missing` blocker with a warning accent, mark `Compare run` as `Unavailable`, derive the environment snapshot state from repo truth where available, and avoid fake drift data. +- **FR-330-009b**: Baseline Compare no-baseline state MUST render compact `Available inputs` for environment snapshot, operation proof, and baseline snapshot, plus optional assignment-unlocks copy. It MUST NOT re-add duplicated lower summary blocks named `Assigned baseline`, `Compare trust`, `Drift impact`, or a duplicate `Evidence path`. +- **FR-330-010**: Baseline Compare MUST show drift findings/evidence gaps before raw diff or diagnostics when compare data exists. +- **FR-330-011**: Raw provider payloads, raw baseline diff JSON, raw OperationRun context, stack traces, debug metadata, provider secrets, and internal exception traces MUST NOT be default-visible. +- **FR-330-012**: Diagnostics/raw details MUST be collapsed, hidden, or capability-gated by default on both pages. +- **FR-330-013**: Existing useful detail sections, tables, links, widgets, and actions MUST remain available as secondary context where authorized. +- **FR-330-014**: Both pages MUST remain environment-owned and require explicit route-bound Environment context. +- **FR-330-015**: Clean workspace-wide URLs MUST NOT pretend to be environment-scoped pages for either surface. +- **FR-330-016**: Remembered Environment fallback MUST NOT establish ownership authority for either surface. +- **FR-330-017**: Legacy query aliases (`tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, `tableFilters`) MUST NOT establish valid context on either surface. +- **FR-330-018**: Cross-workspace environment route mismatches MUST be rejected as not found. +- **FR-330-019**: All actions and links MUST be real, route-backed, and authorization-aware. +- **FR-330-020**: Existing compare start MUST preserve confirmation, server-side authorization, OperationRun creation, notification, and tests. +- **FR-330-021**: Copy MUST avoid static platform-context `tenant` language while preserving dynamic display names and provider-bound terms such as Microsoft tenant/provider tenant ID. +- **FR-330-022**: The implementation MUST not show `Healthy`, `Ready`, `Complete`, `Protected`, `Compliant`, or `Customer-safe` unless the exact claim is repo-backed. Preferred labels include `Ready for review`, `Action needed`, `Blocked`, `Evidence missing`, `Backup proof missing`, `Recovery proof stale`, `Baseline not assigned`, `Compare unavailable`, `Drift requires review`, `Provider readiness incomplete`, `Permissions complete`, and `Operation proof available`. +- **FR-330-023**: `repo-truth-map.md` MUST exist before runtime implementation and MUST classify each visible element. + +## Non-Functional Requirements + +- **NFR-330-001**: No Graph/provider API calls during page render. +- **NFR-330-002**: No migrations, seeders, packages, env vars, queues, scheduler, storage, or deployment asset changes are expected. +- **NFR-330-003**: Filament v5 and Livewire v4.0+ compliance MUST be preserved. +- **NFR-330-004**: Panel provider registration remains in `apps/platform/bootstrap/providers.php`; no provider registration changes are expected. +- **NFR-330-005**: No globally searchable resource is added or changed. If implementation touches resources, global search must remain disabled or backed by safe View/Edit pages and `$recordTitleAttribute`. +- **NFR-330-006**: Page render must stay DB-only and bounded to existing queries; no broad new query family or unbounded relationship loading. +- **NFR-330-007**: Browser smoke must capture required screenshots under the Spec 330 artifacts path when generated. + +## Repo Truth / Data Requirements + +- Every visible runtime element MUST be classified in `repo-truth-map.md` as one of `repo-verified`, `foundation-real`, `derived from existing model`, `empty/unavailable state`, or `deferred future capability`. +- Source areas that MUST be considered: + - `ManagedEnvironment` + - provider connection/readiness + - required permissions + - backup sets and schedules + - restore runs and recovery proof + - baseline profile assignment + - baseline snapshots + - baseline compare result + - drift findings + - risk exceptions / accepted risks + - evidence snapshots + - review packs and environment reviews + - OperationRuns + - audit links if present +- If an element lacks a safe source, route, authorization path, or current-release truth, it MUST become unavailable/omitted/deferred instead of being invented. + +## Out Of Scope + +- Rebuilding the Environment model. +- Rebuilding Baseline Compare backend logic. +- New drift detection logic. +- New baseline assignment workflow unless an existing action/link already exists. +- New evidence generation. +- New backup/restore checks. +- Operations Hub, Governance Inbox, Customer Review Workspace, Evidence/Audit, Restore Safety, and Provider Readiness redesign. +- Commercial entitlements. +- External portal. +- AI. +- New migrations, packages, env vars, queues, scheduler, storage, deployment assets, or compatibility layers. +- Legacy `/admin/t` routes or legacy query alias support. + +## Edge Cases + +- Environment has no provider connection. +- Provider permissions are missing or stale. +- Backup proof exists but recovery proof is stale or unavailable. +- No baseline assignment exists. +- Baseline profile exists but no complete snapshot is consumable. +- Compare run is queued/running. +- Compare run failed or produced no usable result. +- Compare result has zero findings but coverage/evidence gaps exist. +- Compare result has drift findings but raw diff is unavailable or not authorized. +- Latest evidence snapshot or review pack is missing. +- OperationRun proof exists but the actor cannot view it. +- Dynamic environment/workspace display name contains `Tenant`; this is allowed. +- Static UI copy uses platform `tenant` wording; this is forbidden. + +## Acceptance Criteria + +### Environment Dashboard + +- [ ] Environment Dashboard has decision-first readiness layout. +- [ ] Main readiness question is visible. +- [ ] Status/reason/impact/proof/next action are visible. +- [ ] Readiness dimensions are visible where repo-supported. +- [ ] Proof/action panel exists. +- [ ] Recommended next actions are ranked. +- [ ] Existing secondary context remains accessible. +- [ ] Diagnostics are collapsed by default. +- [ ] No false green readiness claim is introduced. + +### Baseline Compare + +- [ ] Baseline Compare has decision-first layout. +- [ ] Main drift/action question is visible. +- [ ] Baseline assignment state is visible. +- [ ] Compare trust/state is visible. +- [ ] Drift/evidence state is visible where repo-supported. +- [ ] No-baseline state is actionable and honest. +- [ ] Raw diff/details are not first-read. +- [ ] Diagnostics/raw diff are collapsed by default. + +### Scope / Routing + +- [ ] Both surfaces are environment-owned. +- [ ] Explicit Environment route is required. +- [ ] Remembered Environment is not used as ownership authority. +- [ ] Cross-workspace Environment is rejected. +- [ ] Legacy tenant aliases do not create valid context. +- [ ] No `/admin/t` route assumptions are reintroduced. + +### Safety / Copy + +- [ ] Raw payloads hidden by default. +- [ ] Provider secrets not visible. +- [ ] Internal exception/debug text not visible. +- [ ] Static platform-copy does not use `tenant`. +- [ ] Dynamic names containing `Tenant` remain unchanged. +- [ ] No false health/compliance/customer-safe claims. + +### RBAC + +- [ ] Unauthorized user cannot access protected environment/baseline data. +- [ ] Unauthorized actions are hidden/disabled or unavailable. +- [ ] Evidence access respects capability. +- [ ] Operation proof access respects capability. +- [ ] Diagnostics/raw details access respects capability. + +### Validation + +- [ ] Repo truth map exists. +- [ ] Required Feature tests pass. +- [ ] Required Browser smoke passes. +- [ ] Relevant Spec 314-322 guards still pass. +- [ ] `pint --dirty` passes. +- [ ] `git diff --check` passes. +- [ ] No broad rebaseline. +- [ ] Full suite status is honestly reported if run/not run. + +## Success Criteria + +- Environment operators can identify the primary readiness blocker and next action in the first viewport. +- Governance operators can identify baseline assignment/compare/drift posture before raw details. +- Missing proof and no-baseline states are explicit and action-oriented. +- Diagnostics/raw details stay secondary. +- Environment-owned route behavior remains sealed by tests and browser smoke. + +## Risks + +- Existing Environment Dashboard already has several productized components; the implementation must improve decision hierarchy without broad redesign or duplicate truth. +- Existing Baseline Compare has mature explanation and stats logic; the implementation must not flatten matrix/launch semantics or imply all-clear from zero findings. +- Existing support diagnostics and raw metadata paths may tempt broader disclosure work; keep them collapsed/authorized and defer structural diagnostics productization. + +## Assumptions + +- No schema change is required. +- Existing factories and browser fixture helpers can create enough environment, no-baseline, action-needed, and drift states for tests. +- Existing capabilities are sufficient for action visibility and raw diagnostics gating. +- Baseline Compare Matrix remains a separate secondary analysis surface. + +## Open Questions + +None blocking preparation. Implementation must verify exact authorized action availability before rendering each action and must downgrade unsupported elements to unavailable/deferred states. + +## Follow-Up Spec Candidates + +- Spec 331 - Restore Safety Workflow Productization. +- Spec 332 - Provider Readiness Productization. +- Spec 333 - Sidebar / Shell Native Filament Polish Pass. diff --git a/specs/330-environment-dashboard-baseline-compare-productization/tasks.md b/specs/330-environment-dashboard-baseline-compare-productization/tasks.md new file mode 100644 index 00000000..452903fd --- /dev/null +++ b/specs/330-environment-dashboard-baseline-compare-productization/tasks.md @@ -0,0 +1,191 @@ +# Tasks: Spec 330 - Environment Dashboard / Baseline Compare Productization + +**Input**: Design documents from `/specs/330-environment-dashboard-baseline-compare-productization/` +**Prerequisites**: `spec.md`, `plan.md`, `repo-truth-map.md` + +**Tests**: Required. This is a runtime UI/operator decision-surface productization with environment-owned route and browser smoke coverage. + +## Test Governance Checklist + +- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior. +- [x] New or changed tests stay in the smallest honest family, and the browser addition is explicit. +- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default. +- [x] Planned validation commands cover the change without pulling in unrelated lane cost. +- [x] The declared surface test profile (`global-context-shell`, `monitoring-state-page`, `shared-detail-family`) is explicit. +- [x] Any material budget, baseline, trend, or escalation note is recorded in the active spec or PR. + +## Phase 1: Preparation And Repo Truth + +**Purpose**: Confirm runtime truth and prevent invented readiness/drift claims before page edits. + +- [x] T001 Re-read `specs/330-environment-dashboard-baseline-compare-productization/spec.md`, `plan.md`, `tasks.md`, and `repo-truth-map.md`. +- [x] T002 Re-read related completed context only: Specs 314-329. Do not modify their artifacts. +- [x] T003 Verify current Environment Dashboard route/class/widgets/views before editing: `apps/platform/app/Filament/Pages/EnvironmentDashboard.php`, `apps/platform/app/Filament/Widgets/Dashboard/EnvironmentDashboardOverview.php`, `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php`, and `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-context-chips.blade.php`. +- [x] T004 Verify current Environment Dashboard source helper before editing: `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php` and `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummary.php`. +- [x] T005 Verify current Baseline Compare route/class/view before editing: `apps/platform/app/Filament/Pages/BaselineCompareLanding.php` and `apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php`. +- [x] T006 Verify current source models/services/helpers: `ManagedEnvironment`, `ProviderConnection`, `ManagedEnvironmentPermission`, `BackupSet`, `RestoreRun`, `BaselineTenantAssignment`, `BaselineProfile`, `BaselineSnapshot`, `Finding`, `FindingException`, `EvidenceSnapshot`, `EnvironmentReview`, `ReviewPack`, `OperationRun`, `BaselineCompareStats`, and `TenantGovernanceAggregateResolver`. +- [x] T007 Update `repo-truth-map.md` with any newly discovered source, capability, fallback, or classification before runtime changes. +- [x] T008 Confirm no migration/package/env/queue/storage/deployment asset change is required; if one appears necessary, stop and update spec/plan first. +- [x] T009 Confirm Filament v5 / Livewire v4.0+ compliance and no Livewire v3/Filament legacy API use. +- [x] T010 Confirm panel provider registration remains `apps/platform/bootstrap/providers.php`. +- [x] T011 Confirm no globally searchable resource is changed; if a resource is touched, verify View/Edit/global-search safety. +- [x] T012 Confirm existing Baseline Compare start action keeps `Action::make(...)->action(...)`, `->requiresConfirmation()`, `UiEnforcement`, `OperationRun`, and notifications. + +## Phase 2: Feature Tests First + +**Purpose**: Lock decision layout, false-green guard, RBAC, scope, and diagnostics behavior before UI refactor. + +- [x] T013 Add or update a Feature test asserting `specs/330-environment-dashboard-baseline-compare-productization/repo-truth-map.md` exists and lists Environment Dashboard and Baseline Compare sections plus required data areas. +- [x] T014 Add or update a Feature/Livewire test for Environment Dashboard layout text: `Environment Dashboard`, `Is this environment ready, blocked, stale, or requiring review?`, `Status`, `Reason`, `Impact`, `Next action`, `Readiness proof`, and `Diagnostics - Collapsed`. +- [x] T015 Add or update a Feature/Livewire test asserting Environment Dashboard missing-proof fixture shows `Action needed`, `Evidence missing` or `Backup proof missing`, and does not show false `Healthy`, `Fully ready`, `Customer-safe`, `Protected`, or `Compliant` claims. +- [x] T016 Add or update a Feature/Livewire test asserting Environment Dashboard shows one primary action plus a ranked next-action list when gaps exist. +- [x] T017 Add or update a Feature/Livewire test for Baseline Compare layout text: `Baseline Compare`, `Which baseline drift requires action?`, `Assigned baseline`, `Compare trust`, `Drift impact`, `Evidence path`, and `Diagnostics - Collapsed`. +- [x] T018 Add or update a Feature/Livewire test asserting Baseline Compare no-baseline state shows `Baseline not assigned`, an impact sentence that compare cannot be used for governance decisions, and an authorized assign/open-baseline action or honest unavailable state. +- [x] T019 Add or update a Feature/Livewire test asserting Baseline Compare drift/evidence state shows drift/evidence summary without raw diff/payload by default. +- [x] T020 Add or update a Feature/Livewire test asserting raw diagnostics are hidden/collapsed by default on both pages: `raw payload`, `raw diff`, `provider secret`, `stack trace`, `debug metadata`, `internal exception`, `provider response`, and raw OperationRun context must not be default-visible. +- [x] T021 Add or update RBAC tests covering evidence links, operation proof links, provider/permission links, backup/restore links, baseline profile/matrix/findings links, compare start, and diagnostics visibility where existing capabilities support coverage. +- [x] T022 Add or update environment-owned route tests for both pages: explicit environment route required, clean workspace URL does not establish environment ownership, remembered environment is not enough, and cross-workspace environment is rejected. +- [x] T023 Add or update legacy alias rejection tests for both pages covering `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and `tableFilters`. +- [x] T024 Add or update static tenant-copy guard asserting platform-context copy such as `current tenant`, `tenant filter`, `all tenants`, `choose tenant`, and `tenant scope` is not visible, while dynamic names containing `Tenant` remain allowed. + +## Phase 3: Environment Dashboard Productization + +**Purpose**: Refactor Environment Dashboard from dense dashboard to decision-first readiness workbench without new backend foundation. + +- [x] T025 Update `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummary.php` only if needed to carry repo-truth-bounded decision/proof payloads; do not add persisted state or public framework semantics. +- [x] T026 Update `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php` to expose derived readiness decision data: status, reason, impact, proof path, primary next action, ranked actions, readiness dimensions, and diagnostics disclosure. +- [x] T027 Update `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php` to render the main readiness question, decision card, proof/action panel, ranked next actions, readiness dimensions, and collapsed diagnostics before secondary details. +- [x] T028 Update `apps/platform/app/Filament/Pages/EnvironmentDashboard.php` only where needed for header/primary action hierarchy, while preserving existing support request and support diagnostics authorization. +- [x] T029 Ensure dashboard context shows Workspace, Environment, Provider when repo-supported, and latest activity/proof state when repo-supported. +- [x] T030 Ensure readiness dimensions render only repo-backed or honest unavailable states: provider readiness, required permissions, backup posture, recovery proof, baseline assignment, baseline compare/drift, evidence freshness, review freshness, accepted risk, and operations attention. +- [x] T031 Ensure one primary next action is visible when authorized and secondary actions are ranked and lower priority. +- [x] T032 Keep existing useful secondary cards/details/links and do not remove existing backup, provider, evidence, review, operations, or support access paths. +- [x] T033 Ensure Environment Dashboard diagnostics/raw details are collapsed, hidden, or capability-gated by default. + +## Phase 4: Baseline Compare Productization + +**Purpose**: Refactor Baseline Compare from compare/status detail to decision-first drift/action surface while preserving compare behavior. + +- [x] T034 Update `apps/platform/app/Filament/Pages/BaselineCompareLanding.php` to expose repo-truth-bounded decision/proof payloads for assignment, compare trust, drift impact, reason, evidence path, operation proof, raw disclosure, and primary next action. +- [x] T035 Update `apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php` to render the main drift/action question, decision card, assignment/compare trust/drift summary, evidence/proof panel, findings/evidence-gap summary, and collapsed raw diff/diagnostics before secondary details. +- [x] T036 Preserve existing compare start action semantics: confirmation, capability gating, `BaselineCompareService::startCompare()`, OperationRun, queued toast/browser event, and open-operation link. +- [x] T037 Render no-baseline, invalid-scope, no-snapshot, stale/missing compare, running compare, failed compare, zero-finding-with-gaps, and drift states as honest decision states. +- [x] T038 Ensure no-baseline state is actionable where a repo-real baseline profile route/action exists; otherwise show honest unavailable guidance without inventing assignment workflow. +- [x] T038a Add a no-baseline visual Compare readiness stepper/pipeline, compact available-inputs section, and assignment-unlocks copy while keeping duplicated `Assigned baseline`, `Compare trust`, `Drift impact`, and duplicate `Evidence path` summary blocks absent. +- [x] T039 Ensure drift findings/evidence gaps render before raw compare details and do not imply `0 findings` equals all-clear when trust/coverage/evidence gaps exist. +- [x] T040 Keep existing compare matrix, findings, run, evidence-gap, and summary sections available as secondary context where authorized. +- [x] T041 Ensure Baseline Compare raw diff/diagnostics are collapsed, hidden, or capability-gated by default. + +## Phase 5: Data Binding And Honest States + +**Purpose**: Bind both surfaces to repo-verified sources and avoid false claims. + +- [x] T042 Bind environment readiness to existing `EnvironmentDashboardSummaryBuilder`, backup/recovery helpers, provider permission view model, baseline aggregate, evidence/review/review pack state, exception stats, and OperationRun attention queries only. +- [x] T043 Bind baseline assignment to `BaselineTenantAssignment`, `BaselineProfile`, and `BaselineSnapshotTruthResolver` only. +- [x] T044 Bind compare trust/drift/evidence state to `BaselineCompareStats`, `TenantGovernanceAggregate`, operator explanation, findings, evidence gap summary, and existing OperationRun proof only. +- [x] T045 Bind proof links only through existing resource URLs, `ManagedEnvironmentLinks`, `OperationRunLinks`, and policy/capability checks. +- [x] T046 Render unavailable/missing/not generated/not applicable/deferred states for unsupported proof paths instead of inventing backend capabilities. +- [x] T047 Ensure no generic green success state, health/compliance/protected/customer-safe copy, restore confidence claim, or compare all-clear claim appears without exact repo proof. + +## Phase 6: Actions, RBAC, And Safety + +**Purpose**: Show only real, authorized actions and preserve read-first default behavior. + +- [x] T048 Keep primary actions singular and context-aware on each page. +- [x] T049 Show open required permissions, open backup posture, open operations, open evidence, open reviews/review pack, open baseline compare, open baseline profiles/matrix/findings, run compare, and open operation proof only when route and authorization are repo-real. +- [x] T050 Ensure unauthorized actions are hidden, disabled with existing convention, or represented as safe unavailable state without leaking sensitive details. +- [x] T051 Ensure raw diagnostics/metadata disclosure is unavailable without `support_diagnostics.view` or stricter existing raw/support capability. +- [x] T052 Verify no default action approves, rejects, accepts risk, deletes, restores, remediates, mutates provider state, or changes evidence/audit/storage. +- [x] T053 If any high-impact action is unexpectedly required, update spec/plan first, then implement it with `Action::make(...)->action(...)`, `->requiresConfirmation()`, server-side authorization, audit, notification, and tests. + +## Phase 7: Environment-Owned Route Contract + +**Purpose**: Preserve Specs 314-322 and Spec 319. + +- [x] T054 Verify Environment Dashboard explicit route opens with active Environment context. +- [x] T055 Verify Baseline Compare explicit route opens with active Environment context. +- [x] T056 Verify clean workspace-only URLs do not establish environment ownership for either surface. +- [x] T057 Verify remembered Environment / Filament tenant fallback is not enough to authorize either surface. +- [x] T058 Verify workspace route and environment route disagreement returns not found for both surfaces. +- [x] T059 Verify legacy aliases are removed/neutralized and do not set ownership or filter state. +- [x] T060 Verify no `/admin/t` route, redirect, or compatibility assumption is reintroduced. + +## Phase 8: Browser Smoke And Screenshots + +**Purpose**: Prove the user-facing contract in the integrated browser lane. + +- [x] T061 Create `apps/platform/tests/Browser/Spec330EnvironmentDashboardBaselineCompareSmokeTest.php` using existing Pest Browser conventions. +- [x] T062 Browser Flow A: Environment Dashboard explicit route non-empty state; assert Environment shell, readiness question, decision card, proof panel, ranked actions, diagnostics collapsed, and screenshot `environment-dashboard-readiness-workbench.png`. +- [x] T063 Browser Flow B: Environment Dashboard action-needed/missing-proof state; assert honest action-needed/missing-proof copy, no false green copy, diagnostics collapsed, and screenshot `environment-dashboard-action-needed.png`. +- [x] T064 Browser Flow C: Baseline Compare explicit route no-baseline state; assert drift question, baseline not assigned, impact, action/unavailable state, diagnostics collapsed, and screenshot `baseline-compare-no-baseline.png`. +- [x] T065 Browser Flow D: Baseline Compare compare/drift state if fixture-supported; assert assigned baseline, compare trust, drift/evidence summary, proof panel, raw details hidden, and screenshot `baseline-compare-decision-workbench.png`. +- [x] T066 Browser Flow E: cross-workspace or invalid environment safe denial for both surfaces. +- [x] T067 Browser Flow F: dynamic display name containing `Tenant` is allowed, static tenant platform-copy is absent. +- [x] T068 Browser Flow G: raw diff/provider payload/debug/stack trace text is absent by default on both surfaces. +- [x] T069 Save screenshots under `specs/330-environment-dashboard-baseline-compare-productization/artifacts/screenshots/` when generated and ensure they contain no secrets. + +## Phase 9: UI Coverage And Documentation Artifacts + +**Purpose**: Satisfy UI-COV without unrelated docs churn. + +- [x] T070 Decide after runtime diff whether `docs/ui-ux-enterprise-audit/route-inventory.md`, `design-coverage-matrix.md`, page reports, or unresolved pages need an update. +- [x] T071 If coverage docs are not changed, add a close-out note explaining why existing UI-002/UI-061 rows plus Spec 325 target artifacts and Spec 330 package artifacts remain sufficient. +- [x] T072 Update `repo-truth-map.md` final classifications for implemented/empty/deferred elements. +- [x] T073 Do not create general documentation files outside required Spec Kit/UI coverage artifacts unless explicitly requested. + +## Phase 10: Validation + +**Purpose**: Run narrow proof and report honestly. + +- [x] T074 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Filament tests/Feature/Rbac tests/Feature/Navigation --filter='EnvironmentDashboard|TenantDashboard|BaselineCompare|EnvironmentOwned|LegacyTenant|Spec322' --compact`. +- [x] T075 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec330EnvironmentDashboardBaselineCompareSmokeTest.php --compact`. +- [x] T076 Run `cd apps/platform && ./vendor/bin/sail artisan test --filter='EnvironmentDashboard|BaselineCompare|AdminSurfaceScope|EnvironmentOwned|LegacyTenant|Spec322' --compact`. +- [x] T077 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`. +- [x] T078 Run `git diff --check`. +- [x] T079 Report full-suite status honestly if not run. +- [x] T080 Confirm no migrations, seeders, packages, env vars, queues, scheduler, storage, deployment assets, backwards compatibility layer, or legacy tenant alias support were added. + +## Dependencies + +- Phase 1 blocks all runtime implementation. +- Phase 2 should be written before or alongside implementation to lock behavior. +- Phase 3 and Phase 4 can be implemented in parallel only if write scopes stay disjoint: + - Environment Dashboard write scope: dashboard page/widget/view/summary helper/tests. + - Baseline Compare write scope: compare page/view/tests. +- Phase 5 and Phase 6 depend on Phases 3-4 payload shape. +- Phase 7 must be validated after both surfaces are changed. +- Phase 8 depends on user-facing runtime changes. +- Phase 10 is final validation. + +## Non-Goals Checklist + +- [x] NT001 Do not build a new environment readiness backend. +- [x] NT002 Do not build a new baseline/drift engine. +- [x] NT003 Do not build a new evidence generator. +- [x] NT004 Do not build a new backup/restore proof engine. +- [x] NT005 Do not add AI summarization. +- [x] NT006 Do not redesign Operations Hub, Governance Inbox, Customer Review Workspace, Evidence/Audit, Restore Safety, or Provider Readiness. +- [x] NT007 Do not add migrations unless spec/plan are updated first with proof. +- [x] NT008 Do not rewrite completed Specs 314-329. +- [x] NT009 Do not add legacy tenant query alias support. +- [x] NT010 Do not expose raw diagnostics, raw diff, raw OperationRun context, or provider payloads by default. + +## Required Final Report Content + +When implementation later completes, report: + +- Changed behavior. +- Environment Dashboard readiness surface. +- Baseline Compare drift/action surface. +- Routing / scope. +- Disclosure / diagnostics default state. +- RBAC-visible/hidden actions. +- Repo-verified vs unavailable states. +- Files changed. +- Repo truth map status. +- Tests run and results. +- Browser verification and screenshots path. +- Known gaps. +- Remaining follow-ups. +- Full suite run/not run. +- Explicit no migrations/seeders/packages/env/queues/scheduler/storage/deployment assets/backcompat/legacy aliases statement.