From 72499a05d688d224150ee85ad0421fa383d46e01 Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Sat, 30 May 2026 00:07:18 +0200 Subject: [PATCH] feat: align baseline compare product process flow --- .../Filament/Pages/BaselineCompareLanding.php | 435 ++++++++++--- .../Baselines/BaselineCompareStats.php | 3 +- .../product-process-flow-horizontal.blade.php | 87 +++ .../pages/baseline-compare-landing.blade.php | 324 ++-------- ...nmentDashboardBaselineCompareSmokeTest.php | 7 +- ...reProductProcessFlowAlignmentSmokeTest.php | 220 +++++++ ...ineCompareEnvironmentRouteContractTest.php | 2 +- ...boardBaselineCompareProductizationTest.php | 16 +- ...CompareProductProcessFlowAlignmentTest.php | 351 ++++++++++ .../screenshots/01-no-baseline-assigned.png | Bin 0 -> 184388 bytes .../02-baseline-snapshot-required.png | Bin 0 -> 304556 bytes .../screenshots/03-compare-run-required.png | Bin 0 -> 299298 bytes .../04-compare-result-available.png | Bin 0 -> 346542 bytes .../screenshots/05-evidence-unavailable.png | Bin 0 -> 416848 bytes .../screenshots/06-diagnostics-collapsed.png | Bin 0 -> 416827 bytes .../artifacts/screenshots/07-dark-mode.png | Bin 0 -> 424492 bytes .../baseline-compare-state-contract.md | 165 +++++ .../checklists/requirements.md | 56 ++ .../plan.md | 123 ++++ .../repo-truth-map.md | 61 ++ .../spec.md | 599 ++++++++++++++++++ .../tasks.md | 99 +++ 22 files changed, 2180 insertions(+), 368 deletions(-) create mode 100644 apps/platform/resources/views/filament/components/product-process-flow-horizontal.blade.php create mode 100644 apps/platform/tests/Browser/Spec336BaselineCompareProductProcessFlowAlignmentSmokeTest.php create mode 100644 apps/platform/tests/Feature/Filament/Spec336BaselineCompareProductProcessFlowAlignmentTest.php create mode 100644 specs/336-baseline-compare-product-process-flow-alignment/artifacts/screenshots/01-no-baseline-assigned.png create mode 100644 specs/336-baseline-compare-product-process-flow-alignment/artifacts/screenshots/02-baseline-snapshot-required.png create mode 100644 specs/336-baseline-compare-product-process-flow-alignment/artifacts/screenshots/03-compare-run-required.png create mode 100644 specs/336-baseline-compare-product-process-flow-alignment/artifacts/screenshots/04-compare-result-available.png create mode 100644 specs/336-baseline-compare-product-process-flow-alignment/artifacts/screenshots/05-evidence-unavailable.png create mode 100644 specs/336-baseline-compare-product-process-flow-alignment/artifacts/screenshots/06-diagnostics-collapsed.png create mode 100644 specs/336-baseline-compare-product-process-flow-alignment/artifacts/screenshots/07-dark-mode.png create mode 100644 specs/336-baseline-compare-product-process-flow-alignment/baseline-compare-state-contract.md create mode 100644 specs/336-baseline-compare-product-process-flow-alignment/checklists/requirements.md create mode 100644 specs/336-baseline-compare-product-process-flow-alignment/plan.md create mode 100644 specs/336-baseline-compare-product-process-flow-alignment/repo-truth-map.md create mode 100644 specs/336-baseline-compare-product-process-flow-alignment/spec.md create mode 100644 specs/336-baseline-compare-product-process-flow-alignment/tasks.md diff --git a/apps/platform/app/Filament/Pages/BaselineCompareLanding.php b/apps/platform/app/Filament/Pages/BaselineCompareLanding.php index fabfe7a3..c9d29629 100644 --- a/apps/platform/app/Filament/Pages/BaselineCompareLanding.php +++ b/apps/platform/app/Filament/Pages/BaselineCompareLanding.php @@ -391,8 +391,8 @@ protected function getViewData(): array 'decisionCard' => $this->decisionCard($hasWarnings, $hasCoverageWarnings, $hasEvidenceGaps), 'decisionSummaryItems' => $this->decisionSummaryItems($hasWarnings, $hasCoverageWarnings, $hasEvidenceGaps), 'proofPanelItems' => $this->proofPanelItems($hasWarnings, $hasCoverageWarnings, $hasEvidenceGaps), - 'compareReadinessFlow' => $this->compareReadinessFlow(), - 'availableCompareInputs' => $this->availableCompareInputs(), + 'compareReadinessFlow' => $this->compareReadinessFlow($hasWarnings, $hasCoverageWarnings), + 'availableCompareInputs' => $this->availableCompareInputs($hasWarnings, $hasCoverageWarnings, $hasEvidenceGaps), 'assignmentUnlocks' => $this->assignmentUnlocks(), 'diagnosticsDisclosure' => $this->diagnosticsDisclosure($hasEvidenceGapDiagnostics), 'hasCoverageWarnings' => $hasCoverageWarnings, @@ -436,7 +436,7 @@ private function decisionCard(bool $hasWarnings, bool $hasCoverageWarnings, bool default => 'gray', }, 'reasonLabel' => 'Reason', - 'reason' => $this->decisionReason($state), + 'reason' => $this->decisionReason($state, $findingsCount), 'impactLabel' => 'Impact', 'impact' => $this->decisionImpact($state, $findingsCount, $hasCoverageWarnings, $hasEvidenceGaps), 'evidenceLabel' => 'Evidence path', @@ -450,39 +450,35 @@ private function decisionStatus(string $state, int $findingsCount, bool $hasWarn { return match ($state) { 'no_assignment' => 'Baseline not assigned', - 'no_snapshot' => 'Compare unavailable', - 'invalid_scope' => 'Compare unavailable', - 'comparing' => 'Compare running', + 'no_snapshot' => 'Baseline snapshot required', + 'invalid_scope' => 'Baseline scope requires review', + 'comparing' => 'Compare in progress', 'failed' => 'Compare failed', - 'idle' => 'Compare available', + 'idle' => 'Compare run required', 'ready' => $findingsCount > 0 - ? 'Drift requires review' - : ($hasWarnings ? 'Evidence requires review' : 'No confirmed drift'), + ? 'Drift findings available' + : ($hasWarnings ? 'Decision output needs review' : 'No drift detected'), default => 'Compare unavailable', }; } - private function decisionReason(string $state): string + private function decisionReason(string $state, int $findingsCount): 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.', + 'no_assignment' => 'This environment does not have an assigned baseline.', + 'no_snapshot' => 'A baseline is assigned, but no usable baseline snapshot is available.' + .($reason !== '' ? ' '.$reason : ($message !== '' ? ' '.$message : '')), + 'invalid_scope' => 'A baseline is assigned, but its scope cannot be used safely for compare.' + .($reason !== '' ? ' '.$reason : ($message !== '' ? ' '.$message : '')), + 'failed' => $this->failureReason ?: ($reason !== '' ? $reason : 'The compare operation ended with errors.'), + 'comparing' => 'Baseline comparison is currently running.', + 'idle' => 'Required inputs exist, but no compare run has been created for the current state.', + 'ready' => $findingsCount > 0 + ? 'Baseline comparison found governance-relevant differences. Drift requires review before a decision is recorded.' + : 'Current environment state matches the assigned baseline within available compare coverage.', default => 'Compare state is derived from the latest baseline assignment, snapshot, and operation proof.', }; } @@ -490,34 +486,42 @@ private function decisionReason(string $state): string 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.'; + return 'Baseline drift cannot be used for governance decisions until a baseline assignment exists.'; } - if ($state === 'no_snapshot' || $state === 'invalid_scope') { - return 'Drift decisions stay unavailable until the baseline source is usable.'; + if ($state === 'no_snapshot') { + return 'Compare cannot run until baseline snapshot input exists.'; + } + + if ($state === 'invalid_scope') { + return 'Compare cannot run safely until the assigned baseline scope is reviewed.'; } if ($state === 'failed') { - return 'Review operation proof before retrying or treating the latest compare as evidence.'; + return 'Drift findings cannot be trusted until the failure is resolved. Review operation proof before retrying.'; } if ($state === 'comparing') { - return 'Wait for operation proof before acting on drift or evidence state.'; + return 'Drift findings are not final yet. Wait for operation proof before acting on drift or evidence state.'; + } + + if ($state === 'idle') { + return 'Drift findings are not available yet. Run compare after the required inputs are confirmed.'; } if ($findingsCount > 0) { - return 'Review drift findings before presenting this environment as aligned to baseline.'; + return 'Review findings and decide the next governance action before presenting this environment as aligned to baseline.'; } if ($hasCoverageWarnings || $hasEvidenceGaps) { - return 'Zero findings must not be treated as an all-clear while coverage or evidence gaps remain.'; + return 'Zero findings must not be treated as final while coverage or evidence gaps remain.'; } - return 'The latest compare shows no confirmed drift for the assigned baseline profile.'; + return 'No governance action is required from this compare result within available compare coverage.'; } /** - * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string} + * @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string,actionName?:string} */ private function primaryDecisionAction(string $state, int $findingsCount): array { @@ -534,6 +538,51 @@ private function primaryDecisionAction(string $state, int $findingsCount): array ]; } + if (in_array($state, ['no_snapshot', 'invalid_scope'], true)) { + $profileUrl = $this->baselineProfileUrl(); + + return [ + 'actionLabel' => $profileUrl !== null ? 'Open baseline profile' : 'Baseline profile unavailable', + 'actionUrl' => $profileUrl, + 'actionDisabled' => $profileUrl === null, + 'helperText' => $profileUrl !== null + ? 'Open the assigned baseline profile to review capture and snapshot state.' + : 'No authorized baseline profile path is available from this page.', + ]; + } + + if ($state === 'idle') { + $canRunCompare = $this->canRunCompareAction(); + + return [ + 'actionLabel' => $canRunCompare ? 'Compare now' : 'Compare unavailable', + 'actionUrl' => null, + 'actionDisabled' => ! $canRunCompare, + 'actionName' => 'compareNow', + 'helperText' => $canRunCompare + ? 'Use the confirmed Compare now action to generate drift findings.' + : 'You are not authorized to start baseline compare from this environment.', + ]; + } + + if ($state === 'comparing' && $this->getRunUrl() !== null) { + return [ + 'actionLabel' => 'View operation progress', + 'actionUrl' => $this->getRunUrl(), + 'actionDisabled' => false, + 'helperText' => null, + ]; + } + + if ($state === 'failed' && $this->getRunUrl() !== null) { + return [ + 'actionLabel' => 'Review compare failure', + 'actionUrl' => $this->getRunUrl(), + 'actionDisabled' => false, + 'helperText' => null, + ]; + } + if ($state === 'ready' && $findingsCount > 0 && $this->getFindingsUrl() !== null) { return [ 'actionLabel' => 'Review drift findings', @@ -543,24 +592,15 @@ private function primaryDecisionAction(string $state, int $findingsCount): array ]; } - if (in_array($state, ['ready', 'failed'], true) && $this->getRunUrl() !== null) { + if ($state === 'ready' && $this->getRunUrl() !== null) { return [ - 'actionLabel' => 'Open operation proof', + 'actionLabel' => 'Review evidence', '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(), @@ -606,31 +646,56 @@ private function proofPanelItems(bool $hasWarnings, bool $hasCoverageWarnings, b return []; } + $environmentSnapshotState = $this->environmentSnapshotState($hasCoverageWarnings); + $operationProofAvailable = $this->operationRunId !== null && $this->getRunUrl() !== null; + $baselineSnapshotState = $this->snapshotId !== null ? 'Available' : ($this->state === 'no_snapshot' ? 'Missing' : 'Unavailable'); + $driftFindingsAvailable = $this->state === 'ready'; + return [ [ 'key' => 'assigned_baseline', 'label' => 'Assigned baseline', 'value' => $this->profileName ?? 'Baseline not assigned', 'tone' => $this->profileName !== null ? 'success' : 'warning', - 'description' => $this->snapshotId !== null ? 'Snapshot #'.$this->snapshotId : 'No complete baseline snapshot is linked.', + 'description' => $this->state === 'invalid_scope' + ? 'Assignment exists, but the baseline scope requires review.' + : 'Environment-owned baseline assignment state.', 'actionLabel' => $this->openCompareMatrixUrl() !== null ? 'Open compare matrix' : null, 'actionUrl' => $this->openCompareMatrixUrl(), ], [ - 'key' => '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' => 'baseline_snapshot', + 'label' => 'Baseline snapshot', + 'value' => $baselineSnapshotState, + 'tone' => $this->flowTone($baselineSnapshotState), + 'description' => $this->snapshotId !== null ? 'Snapshot #'.$this->snapshotId.' is the baseline compare input.' : 'No usable baseline snapshot input is linked.', + 'actionLabel' => $this->baselineProfileUrl() !== null ? 'Open baseline profile' : null, + 'actionUrl' => $this->baselineProfileUrl(), ], [ - 'key' => '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.', + 'key' => 'environment_snapshot', + 'label' => 'Environment snapshot', + 'value' => $environmentSnapshotState, + 'tone' => $this->flowTone($environmentSnapshotState), + 'description' => $this->environmentSnapshotDescription($environmentSnapshotState), + 'actionLabel' => null, + 'actionUrl' => null, + ], + [ + 'key' => 'operation_run_proof', + 'label' => 'OperationRun proof', + 'value' => $operationProofAvailable ? 'Available' : 'Unavailable', + 'tone' => $operationProofAvailable ? 'success' : 'gray', + 'description' => $operationProofAvailable ? 'Compare proof is linked to an OperationRun.' : 'No compare OperationRun proof is available yet.', + 'actionLabel' => $operationProofAvailable ? 'Open operation proof' : null, + 'actionUrl' => $operationProofAvailable ? $this->getRunUrl() : null, + ], + [ + 'key' => 'drift_findings', + 'label' => 'Drift findings', + 'value' => $driftFindingsAvailable ? $this->driftImpactLabel() : 'Unavailable', + 'tone' => $driftFindingsAvailable && ((int) ($this->findingsCount ?? 0)) > 0 ? 'warning' : ($driftFindingsAvailable ? 'success' : 'gray'), + 'description' => $this->driftFindingsDescription(), 'actionLabel' => $this->getFindingsUrl() !== null && ((int) ($this->findingsCount ?? 0)) > 0 ? 'Review findings' : null, 'actionUrl' => ((int) ($this->findingsCount ?? 0)) > 0 ? $this->getFindingsUrl() : null, ], @@ -639,7 +704,7 @@ private function proofPanelItems(bool $hasWarnings, bool $hasCoverageWarnings, b 'label' => 'Evidence path', 'value' => $this->evidencePathSummary($hasCoverageWarnings, $hasEvidenceGaps), 'tone' => ($hasCoverageWarnings || $hasEvidenceGaps) ? 'warning' : 'gray', - 'description' => 'Evidence gaps and coverage limits stay visible before diagnostics.', + 'description' => $this->evidenceInputDescription($hasCoverageWarnings, $hasEvidenceGaps), 'actionLabel' => null, 'actionUrl' => null, ], @@ -647,46 +712,93 @@ private function proofPanelItems(bool $hasWarnings, bool $hasCoverageWarnings, b } /** - * @return list> + * @return list> */ - private function compareReadinessFlow(): array + private function compareReadinessFlow(bool $hasWarnings, bool $hasCoverageWarnings): array { - if ($this->state !== 'no_assignment') { - return []; - } - - $environmentSnapshotState = $this->hasEnvironmentSnapshot() ? 'Available' : 'Unavailable'; + $state = (string) ($this->state ?? 'unknown'); + $environmentSnapshotState = $this->environmentSnapshotState($hasCoverageWarnings); + $baselineAssignedState = match ($state) { + 'no_assignment' => 'Missing', + 'invalid_scope' => 'Needs review', + 'no_tenant' => 'Unavailable', + default => 'Complete', + }; + $baselineSnapshotState = match ($state) { + 'no_assignment' => 'Unavailable', + 'no_snapshot' => 'Missing', + 'invalid_scope' => 'Unavailable', + default => $this->snapshotId !== null ? 'Available' : 'Unavailable', + }; + $compareRunState = match ($state) { + 'idle' => 'Required', + 'comparing' => 'In progress', + 'failed' => 'Failed', + 'ready' => 'Available', + default => 'Unavailable', + }; + $decisionOutputState = match ($state) { + 'ready' => $hasWarnings ? 'Needs review' : 'Available', + 'idle' => 'Required', + default => 'Unavailable', + }; return [ [ 'label' => 'Baseline assigned', - 'state' => 'Missing', - 'tone' => 'warning', - 'description' => 'No baseline is assigned to this environment.', + 'state' => $baselineAssignedState, + 'tone' => $this->flowTone($baselineAssignedState), + 'description' => match ($baselineAssignedState) { + 'Complete' => 'Baseline assignment exists.', + 'Needs review' => 'Assignment scope needs review.', + 'Missing' => 'No baseline is assigned.', + default => 'Assignment unavailable.', + }, + 'currentBlocker' => in_array($state, ['no_assignment', 'invalid_scope'], true), ], [ 'label' => 'Baseline snapshot', - 'state' => 'Unavailable', - 'tone' => 'gray', - 'description' => 'No baseline snapshot is linked.', + 'state' => $baselineSnapshotState, + 'tone' => $this->flowTone($baselineSnapshotState), + 'description' => match ($baselineSnapshotState) { + 'Available' => 'Snapshot #'.$this->snapshotId.' is available.', + 'Missing' => 'No usable snapshot.', + 'Needs review' => 'Snapshot needs review.', + default => 'No snapshot linked.', + }, + 'currentBlocker' => $state === 'no_snapshot', ], [ 'label' => 'Environment snapshot', 'state' => $environmentSnapshotState, - 'tone' => $environmentSnapshotState === 'Available' ? 'success' : 'gray', - 'description' => 'Environment snapshot state is required for compare.', + 'tone' => $this->flowTone($environmentSnapshotState), + 'description' => $this->environmentSnapshotDescription($environmentSnapshotState), + 'currentBlocker' => false, ], [ 'label' => 'Compare run', - 'state' => 'Unavailable', - 'tone' => 'gray', - 'description' => 'Compare cannot run until required inputs exist.', + 'state' => $compareRunState, + 'tone' => $this->flowTone($compareRunState), + 'description' => match ($compareRunState) { + 'Available' => 'Completed run available.', + 'Required' => 'Run compare.', + 'In progress' => 'Queued or running.', + 'Failed' => 'Latest run failed.', + default => 'Blocked by missing inputs.', + }, + 'currentBlocker' => in_array($state, ['idle', 'comparing', 'failed'], true), ], [ 'label' => 'Decision output', - 'state' => 'Unavailable', - 'tone' => 'gray', - 'description' => 'No drift decision output is available yet.', + 'state' => $decisionOutputState, + 'tone' => $this->flowTone($decisionOutputState), + 'description' => match ($decisionOutputState) { + 'Available' => 'Decision output available.', + 'Needs review' => 'Evidence or coverage needs review.', + 'Required' => 'Run compare first.', + default => 'No decision output.', + }, + 'currentBlocker' => $state === 'ready' && $hasWarnings, ], ]; } @@ -694,10 +806,61 @@ private function compareReadinessFlow(): array /** * @return list> */ - private function availableCompareInputs(): array + private function availableCompareInputs(bool $hasWarnings, bool $hasCoverageWarnings, bool $hasEvidenceGaps): array { if ($this->state !== 'no_assignment') { - return []; + $environmentSnapshotState = $this->environmentSnapshotState($hasCoverageWarnings); + $baselineSnapshotState = $this->state === 'no_snapshot' + ? 'Missing' + : ($this->snapshotId !== null ? 'Available' : 'Unavailable'); + $operationProofState = $this->operationRunId !== null && $this->getRunUrl() !== null ? 'Available' : 'Unavailable'; + $driftFindingsState = $this->state === 'ready' ? 'Available' : 'Unavailable'; + $evidencePathState = $this->evidenceInputState($hasWarnings); + + return [ + [ + 'label' => 'Assigned baseline', + 'state' => $this->state === 'invalid_scope' ? 'Needs review' : 'Available', + 'tone' => $this->state === 'invalid_scope' ? 'warning' : 'success', + 'description' => $this->profileName !== null + ? 'Assigned baseline: '.$this->profileName.'.' + : 'Baseline assignment exists but requires review.', + ], + [ + 'label' => 'Baseline snapshot', + 'state' => $baselineSnapshotState, + 'tone' => $this->flowTone($baselineSnapshotState), + 'description' => $this->snapshotId !== null + ? 'Snapshot #'.$this->snapshotId.' is the compare input.' + : 'No usable baseline snapshot input is linked.', + ], + [ + 'label' => 'Environment snapshot', + 'state' => $environmentSnapshotState, + 'tone' => $this->flowTone($environmentSnapshotState), + 'description' => $this->environmentSnapshotDescription($environmentSnapshotState), + ], + [ + 'label' => 'OperationRun proof', + 'state' => $operationProofState, + 'tone' => $this->flowTone($operationProofState), + 'description' => $operationProofState === 'Available' + ? 'A compare OperationRun proof link is available.' + : 'No compare OperationRun proof is available yet.', + ], + [ + 'label' => 'Drift findings', + 'state' => $driftFindingsState, + 'tone' => $driftFindingsState === 'Available' && ((int) ($this->findingsCount ?? 0)) > 0 ? 'warning' : $this->flowTone($driftFindingsState), + 'description' => $this->driftFindingsDescription(), + ], + [ + 'label' => 'Evidence path', + 'state' => $evidencePathState, + 'tone' => $this->flowTone($evidencePathState), + 'description' => $this->evidenceInputDescription($hasCoverageWarnings, $hasEvidenceGaps), + ], + ]; } $environmentSnapshotState = $this->hasEnvironmentSnapshot() ? 'Available' : 'Unavailable'; @@ -782,6 +945,7 @@ private function compareTrustLabel(bool $hasWarnings): string 'failed' => 'Failed', 'no_assignment' => 'Unavailable', 'no_snapshot' => 'Unavailable', + 'invalid_scope' => 'Needs review', default => 'Unavailable', }; } @@ -825,18 +989,117 @@ private function driftImpactLabel(): string private function evidencePathSummary(bool $hasCoverageWarnings, bool $hasEvidenceGaps): string { if ($hasEvidenceGaps) { - return 'Evidence gaps need review'; + return 'Evidence unavailable - Evidence gaps need review'; } if ($hasCoverageWarnings) { - return 'Coverage warning recorded'; + return 'Evidence unavailable - Coverage warning recorded'; } if ($this->operationRunId !== null) { - return 'Operation proof available'; + return 'Evidence unavailable - Operation proof available'; } - return 'Operation proof unavailable'; + return 'Evidence unavailable'; + } + + private function baselineProfileUrl(): ?string + { + if (! BaselineProfileResource::canViewAny()) { + return null; + } + + if ($this->profileId !== null) { + return BaselineProfileResource::getUrl('view', ['record' => $this->profileId], panel: 'admin'); + } + + return BaselineProfileResource::getUrl('index', panel: 'admin'); + } + + private function canRunCompareAction(): bool + { + if (! in_array($this->state, ['idle', 'ready', 'failed'], true)) { + return false; + } + + $user = auth()->user(); + $tenant = $this->currentEnvironment(); + + if (! $user instanceof User || ! $tenant instanceof ManagedEnvironment) { + return false; + } + + return app(CapabilityResolver::class)->can($user, $tenant, Capabilities::TENANT_SYNC); + } + + private function environmentSnapshotState(bool $hasCoverageWarnings): string + { + if ($hasCoverageWarnings) { + return 'Needs review'; + } + + return $this->hasEnvironmentSnapshot() ? 'Available' : 'Unavailable'; + } + + private function environmentSnapshotDescription(string $environmentSnapshotState): string + { + return match ($environmentSnapshotState) { + 'Available' => 'Environment snapshot evidence is present.', + 'Needs review' => 'Coverage proof has warnings.', + default => 'No environment snapshot yet.', + }; + } + + private function flowTone(string $state): string + { + return match ($state) { + 'Available', 'Complete' => 'success', + 'Missing', 'Required', 'Needs review' => 'warning', + 'In progress' => 'info', + 'Failed' => 'danger', + default => 'gray', + }; + } + + private function driftFindingsDescription(): string + { + if ($this->state !== 'ready') { + return 'Run compare after required inputs exist.'; + } + + $findingsCount = (int) ($this->findingsCount ?? 0); + + if ($findingsCount > 0) { + return $findingsCount.' '.\Illuminate\Support\Str::plural('open drift finding', $findingsCount).' available for review.'; + } + + return 'Zero open drift findings are recorded for the latest compare result.'; + } + + private function evidenceInputState(bool $hasWarnings): string + { + if ($this->operationRunId === null) { + return 'Unavailable'; + } + + return $hasWarnings ? 'Needs review' : 'Unavailable'; + } + + private function evidenceInputDescription(bool $hasCoverageWarnings, bool $hasEvidenceGaps): string + { + if ($hasEvidenceGaps) { + return 'Compare result exists, but evidence output is not available. Evidence gaps need review.'; + } + + if ($hasCoverageWarnings) { + return 'Compare result exists, but evidence output is not available. Coverage warnings need review.'; + } + + if ($this->operationRunId !== null) { + return 'Compare result exists, but no evidence output is linked yet.'; + } + + return 'No evidence output is linked yet.'; } /** diff --git a/apps/platform/app/Support/Baselines/BaselineCompareStats.php b/apps/platform/app/Support/Baselines/BaselineCompareStats.php index a32eacfa..730ec73e 100644 --- a/apps/platform/app/Support/Baselines/BaselineCompareStats.php +++ b/apps/platform/app/Support/Baselines/BaselineCompareStats.php @@ -9,8 +9,8 @@ use App\Models\BaselineTenantAssignment; use App\Models\Finding; use App\Models\InventoryItem; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Services\Baselines\BaselineSnapshotTruthResolver; use App\Support\OperationCatalog; use App\Support\OperationRunStatus; @@ -19,6 +19,7 @@ use App\Support\Ui\OperatorExplanation\CountDescriptor; use App\Support\Ui\OperatorExplanation\OperatorExplanationPattern; use Illuminate\Support\Facades\Cache; +use InvalidArgumentException; final class BaselineCompareStats { diff --git a/apps/platform/resources/views/filament/components/product-process-flow-horizontal.blade.php b/apps/platform/resources/views/filament/components/product-process-flow-horizontal.blade.php new file mode 100644 index 00000000..282ddde7 --- /dev/null +++ b/apps/platform/resources/views/filament/components/product-process-flow-horizontal.blade.php @@ -0,0 +1,87 @@ +@php + $steps = is_array($steps ?? null) ? $steps : []; + $flowTestId = $flowTestId ?? 'product-process-flow'; + $stepTestId = $stepTestId ?? 'product-process-flow-step'; + $connectorTestId = $connectorTestId ?? 'product-process-flow-connector'; + $badgeTestId = $badgeTestId ?? 'product-process-flow-badge'; + $statusBadgeClasses = $statusBadgeClasses ?? 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 + +
+
+

+ {{ $title ?? 'Product process flow' }} +

+ @if (filled($subtitle ?? null)) +

+ {{ $subtitle }} +

+ @endif +
+ +
    + @foreach ($steps as $step) + @php + $isCurrentBlocker = (bool) ($step['currentBlocker'] ?? false); + $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 +
+
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 a385d614..12c31b98 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 @@ -39,7 +39,7 @@ @endphp -
+

@@ -50,20 +50,32 @@

-
-
-
{{ $decisionCard['statusLabel'] ?? 'Status' }}
-
{{ $decisionCard['status'] ?? 'Compare unavailable' }}
+
+
+
+ {{ $decisionCard['statusLabel'] ?? 'Status' }} +
+
+ {{ $decisionCard['status'] ?? 'Compare unavailable' }} +
-
-
{{ $decisionCard['reasonLabel'] ?? 'Reason' }}
-
{{ $decisionCard['reason'] ?? 'Compare state is 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.' }}
+
+
+ {{ $decisionCard['impactLabel'] ?? 'Impact' }} +
+
+ {{ $decisionCard['impact'] ?? 'No governance decision should rely on this compare state yet.' }} +
@@ -83,18 +95,21 @@