feat: environment dashboard operator guidance consolidation (spec 352)
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m4s

Implemented the consolidated operator guidance panel for the environment dashboard. Updated EnvironmentDashboardSummaryBuilder to prioritize and select guidance based on the operator guidance contract. Added comprehensive unit, feature, and browser tests to verify the guidance selection logic and UI rendering.
This commit is contained in:
Ahmed Darrazi 2026-06-04 14:55:20 +02:00
parent d4e4d2d109
commit 69a9fb6796
31 changed files with 2907 additions and 107 deletions

View File

@ -75,8 +75,8 @@ public function getTitle(): string|Htmlable
return new HtmlString(sprintf(
'<span class="inline-flex flex-wrap items-center gap-3" data-testid="tenant-dashboard-heading"><span>%s</span><span data-testid="tenant-dashboard-posture-pill" class="%s">%s</span></span>',
e((string) $tenant->name),
e($this->posturePillClasses((string) ($summary->posture['tone'] ?? 'gray'))),
e((string) ($summary->posture['status'] ?? '')),
e($this->posturePillClasses((string) ($summary->operatorGuidance['tone'] ?? $summary->posture['tone'] ?? 'gray'))),
e((string) ($summary->operatorGuidance['status'] ?? $summary->posture['status'] ?? '')),
));
}
@ -142,42 +142,22 @@ public function getColumns(): int|array
*/
protected function getHeaderActions(): array
{
$actions = [];
if ($primaryAction = $this->primaryFollowUpHeaderAction()) {
$actions[] = $primaryAction;
}
$moreActions = array_values(array_filter([
$this->secondaryHeaderAction(),
$this->requestSupportAction(),
$this->openSupportDiagnosticsAction(),
]));
if ($moreActions !== []) {
$actions[] = ActionGroup::make($moreActions)
if ($moreActions === []) {
return [];
}
return [
ActionGroup::make($moreActions)
->label(__('localization.dashboard.more_actions'))
->icon('heroicon-o-ellipsis-horizontal')
->color('gray');
}
return $actions;
}
private function primaryFollowUpHeaderAction(): ?Action
{
$payload = $this->primaryFollowUpHeaderPayload();
if (! is_array($payload)) {
return $this->governanceInboxHeaderAction();
}
return $this->summaryHeaderAction(
name: 'primaryFollowUp',
payload: $payload,
color: 'primary',
icon: 'heroicon-o-bolt',
);
->color('gray'),
];
}
private function secondaryHeaderAction(): ?Action
@ -207,7 +187,7 @@ private function primaryFollowUpHeaderPayload(): ?array
return null;
}
$payload = $summary->recommendedActions[0] ?? null;
$payload = $summary->operatorGuidance;
return is_array($payload) && filled($payload['actionLabel'] ?? null)
? $payload
@ -227,6 +207,14 @@ private function secondaryHeaderPayload(): ?array
$primaryPayload = $this->primaryFollowUpHeaderPayload();
foreach ($summary->operatorGuidance['secondaryActions'] ?? [] as $secondaryAction) {
if (! is_array($secondaryAction) || ! filled($secondaryAction['actionLabel'] ?? null)) {
continue;
}
return $secondaryAction;
}
foreach ($summary->readinessCards as $payload) {
if (! is_array($payload) || ! filled($payload['actionLabel'] ?? null)) {
continue;

View File

@ -39,22 +39,38 @@ protected function getViewData(): array
'headline' => __('localization.dashboard.overview.tenant_context_unavailable_headline'),
'summary' => __('localization.dashboard.overview.tenant_context_unavailable_summary'),
],
'operatorGuidance' => [
'key' => 'environment.no_context',
'title' => __('localization.dashboard.overview.environment_context_unavailable_headline'),
'status' => __('localization.dashboard.overview.status_unavailable'),
'tone' => 'gray',
'reason' => __('localization.dashboard.overview.tenant_context_unavailable_headline'),
'impact' => __('localization.dashboard.overview.tenant_context_unavailable_summary'),
'actionLabel' => __('localization.dashboard.overview.action_review_environment'),
'actionUrl' => null,
'actionDisabled' => true,
'helperText' => __('localization.dashboard.overview.operator_guidance_unavailable_helper'),
'secondaryActions' => [],
'source' => ['type' => 'environment_context'],
],
'readinessDecision' => [
'question' => 'Is this environment ready, blocked, stale, or requiring review?',
'title' => __('localization.dashboard.overview.environment_context_unavailable_headline'),
'statusLabel' => 'Status',
'status' => __('localization.dashboard.overview.status_unavailable'),
'tone' => 'gray',
'reasonLabel' => 'Reason',
'reason' => __('localization.dashboard.overview.tenant_context_unavailable_headline'),
'impactLabel' => 'Impact',
'impactLabel' => __('localization.dashboard.overview.label_why_this_matters'),
'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',
'nextActionLabel' => __('localization.dashboard.overview.label_recommended_next_action'),
'actionLabel' => __('localization.dashboard.overview.action_review_environment'),
'actionUrl' => null,
'actionDisabled' => true,
'helperText' => 'No single repo-real follow-up is currently available.',
'helperText' => __('localization.dashboard.overview.operator_guidance_unavailable_helper'),
'secondaryActions' => [],
],
'kpis' => [],
'recommendedActions' => [],

View File

@ -9,6 +9,7 @@
/**
* @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<string, mixed> $operatorGuidance
* @param array<string, mixed> $readinessDecision
* @param list<array<string, mixed>> $kpis
* @param list<array<string, mixed>> $recommendedActions
@ -24,6 +25,7 @@
public function __construct(
public array $context,
public array $posture,
public array $operatorGuidance,
public array $readinessDecision,
public array $kpis,
public array $recommendedActions,
@ -42,6 +44,7 @@ public function __construct(
* @return array{
* context: array{workspace:string,tenant:string,provider:?string,providerKey:?string,latestActivity:?string},
* posture: array{status:string,tone:string,headline:string,summary:string},
* operatorGuidance: array<string, mixed>,
* readinessDecision: array<string, mixed>,
* kpis: list<array<string, mixed>>,
* recommendedActions: list<array<string, mixed>>,
@ -61,6 +64,7 @@ public function toArray(): array
return [
'context' => $this->context,
'posture' => $this->posture,
'operatorGuidance' => $this->operatorGuidance,
'readinessDecision' => $this->readinessDecision,
'kpis' => $this->kpis,
'recommendedActions' => $this->recommendedActions,

View File

@ -21,6 +21,7 @@
use App\Models\ProviderConnection;
use App\Models\ReviewPack;
use App\Models\User;
use App\Services\EnvironmentReviews\EnvironmentReviewReadinessGate;
use App\Services\Intune\ManagedEnvironmentRequiredPermissionsViewModelBuilder;
use App\Support\Auth\Capabilities;
use App\Support\BackupHealth\BackupHealthActionTarget;
@ -37,7 +38,9 @@
use App\Support\OperationRunOutcome;
use App\Support\OpsUx\ActiveRuns;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\ResolutionGuidance\Adapters\ReviewPackOutputResolutionAdapter;
use App\Support\RestoreSafety\RestoreSafetyResolver;
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
use App\Support\Tenants\TenantRecoveryTriagePresentation;
use App\Support\Verification\VerificationReportOverall;
use Illuminate\Database\Eloquent\Builder;
@ -84,6 +87,7 @@ public function build(ManagedEnvironment $tenant, ?User $user = null): Environme
]);
$latestReview = $this->latestEnvironmentReview($tenant);
$latestReviewOutputReview = $this->latestReviewOutputReview($tenant);
$latestReviewPack = $this->latestReviewPack($tenant);
$latestEvidenceSnapshot = $this->latestEvidenceSnapshot($tenant);
$exceptionStats = $this->exceptionStats($tenant);
@ -128,6 +132,14 @@ public function build(ManagedEnvironment $tenant, ?User $user = null): Environme
exceptionStats: $exceptionStats,
);
$activeOperationSummary = $this->activeOperationSummary($tenant, $user);
$operatorGuidance = $this->operatorGuidance(
tenant: $tenant,
user: $user,
posture: $posture,
recommendedActions: $recommendedActions,
latestReview: $latestReviewOutputReview,
latestReviewPack: $latestReviewPack,
);
$summary = new EnvironmentDashboardSummary(
context: [
@ -144,7 +156,8 @@ public function build(ManagedEnvironment $tenant, ?User $user = null): Environme
),
],
posture: $posture,
readinessDecision: $this->readinessDecision($posture, $recommendedActions),
operatorGuidance: $operatorGuidance,
readinessDecision: $this->readinessDecision($operatorGuidance),
kpis: $this->kpis($tenant, $user, $aggregate, $requiredPermissions),
recommendedActions: $recommendedActions,
governanceStatus: $governanceStatus,
@ -172,44 +185,516 @@ public function build(ManagedEnvironment $tenant, ?User $user = null): Environme
return $summary;
}
/**
* @param array<string, mixed> $operatorGuidance
* @return array<string, mixed>
*/
private function readinessDecision(array $operatorGuidance): array
{
$actionUrl = is_string($operatorGuidance['actionUrl'] ?? null)
? (string) $operatorGuidance['actionUrl']
: null;
$actionDisabled = (bool) ($operatorGuidance['actionDisabled'] ?? true);
$secondaryActions = is_array($operatorGuidance['secondaryActions'] ?? null)
? array_values(array_filter($operatorGuidance['secondaryActions'], static fn (mixed $action): bool => is_array($action)))
: [];
$helperText = is_string($operatorGuidance['helperText'] ?? null)
? (string) $operatorGuidance['helperText']
: (($actionDisabled && blank($actionUrl) && $secondaryActions === [])
? $this->overviewText('operator_guidance_unavailable_helper')
: null);
return [
'question' => 'Is this environment ready, blocked, stale, or requiring review?',
'title' => (string) ($operatorGuidance['title'] ?? $this->overviewText('tenant_context_unavailable_headline')),
'statusLabel' => 'Status',
'status' => (string) ($operatorGuidance['status'] ?? $this->overviewText('status_unavailable')),
'tone' => (string) ($operatorGuidance['tone'] ?? 'gray'),
'reasonLabel' => $this->overviewText('label_reason'),
'reason' => (string) ($operatorGuidance['reason'] ?? $this->overviewText('tenant_context_unavailable_headline')),
'impactLabel' => $this->overviewText('label_why_this_matters'),
'impact' => (string) ($operatorGuidance['impact'] ?? $this->overviewText('tenant_context_unavailable_summary')),
'proofLabel' => 'Readiness proof',
'proof' => 'Evidence, operation, review, provider, backup, and baseline signals are summarized before diagnostics.',
'nextActionLabel' => $this->overviewText('label_recommended_next_action'),
'actionLabel' => (string) ($operatorGuidance['actionLabel'] ?? $this->overviewText('action_review_environment')),
'actionUrl' => $actionUrl,
'actionDisabled' => $actionDisabled,
'helperText' => $helperText,
'secondaryActions' => $secondaryActions,
];
}
/**
* @param array{status:string,tone:string,headline:string,summary:string} $posture
* @param list<array<string, mixed>> $recommendedActions
* @return array<string, mixed>
*/
private function readinessDecision(array $posture, array $recommendedActions): array
private function operatorGuidance(
ManagedEnvironment $tenant,
?User $user,
array $posture,
array $recommendedActions,
?EnvironmentReview $latestReview,
?ReviewPack $latestReviewPack,
): array {
if ($providerGuidance = $this->providerOperatorGuidance($recommendedActions)) {
return $providerGuidance;
}
if ($reviewOutputGuidance = $this->reviewOutputOperatorGuidance($tenant, $user, $latestReview, $latestReviewPack)) {
return $reviewOutputGuidance;
}
foreach ([
'operations_requiring_attention',
'high_severity_findings',
'overdue_findings',
'risk_exceptions',
'recovery_posture',
'continue_review',
] as $key) {
$action = $this->recommendedActionByKey($recommendedActions, $key);
if (! is_array($action)) {
continue;
}
return $this->recommendedActionOperatorGuidance($action);
}
return $this->noUrgentOperatorGuidance($tenant, $user, $posture);
}
/**
* @param list<array<string, mixed>> $recommendedActions
* @return array<string, mixed>|null
*/
private function providerOperatorGuidance(array $recommendedActions): ?array
{
$primaryAction = $recommendedActions[0] ?? null;
foreach (['required_permissions', 'delegated_permissions'] as $key) {
$action = $this->recommendedActionByKey($recommendedActions, $key);
if (! is_array($action)) {
continue;
}
return [
'key' => 'provider_readiness.'.$key,
'title' => $this->operatorGuidanceTitleForRecommendedAction($key),
'status' => $key === 'required_permissions'
? $this->overviewText('status_blocked')
: $this->overviewText('status_action_needed'),
'tone' => (string) ($action['tone'] ?? 'warning'),
'reason' => (string) ($action['reason'] ?? $this->overviewText('posture_action_needed_fallback_summary')),
'impact' => (string) ($action['impact'] ?? $this->overviewText('posture_action_needed_fallback_summary')),
'actionLabel' => (string) ($action['actionLabel'] ?? $this->overviewText('action_open_required_permissions')),
'actionUrl' => is_string($action['actionUrl'] ?? null) ? (string) $action['actionUrl'] : null,
'actionDisabled' => (bool) ($action['actionDisabled'] ?? blank($action['actionUrl'] ?? null)),
'helperText' => is_string($action['helperText'] ?? null) ? (string) $action['helperText'] : null,
'secondaryActions' => [],
'source' => [
'type' => 'recommended_action',
'key' => $key,
],
];
}
return null;
}
/**
* @return array<string, mixed>|null
*/
private function reviewOutputOperatorGuidance(
ManagedEnvironment $tenant,
?User $user,
?EnvironmentReview $latestReview,
?ReviewPack $latestReviewPack,
): ?array {
$resolutionCase = $this->reviewOutputResolutionCaseForDashboard($tenant, $user, $latestReview);
if (! is_array($resolutionCase)) {
return null;
}
$status = (string) ($resolutionCase['status'] ?? 'unknown');
if (in_array($status, ['ready', 'unknown'], true)) {
return null;
}
$primaryAction = $this->operatorGuidanceActionFromResolutionAction($resolutionCase['primary_action'] ?? null);
if (blank($primaryAction['actionUrl'] ?? null)) {
$workspaceAction = $this->customerWorkspaceAction($tenant, $user, $latestReviewPack);
$primaryAction = [
'actionLabel' => (string) ($workspaceAction['actionLabel'] ?? $this->overviewText('action_open_customer_workspace')),
'actionUrl' => is_string($workspaceAction['actionUrl'] ?? null) ? (string) $workspaceAction['actionUrl'] : null,
'actionDisabled' => (bool) ($workspaceAction['actionDisabled'] ?? true),
'helperText' => is_string($workspaceAction['helperText'] ?? null)
? (string) $workspaceAction['helperText']
: $this->overviewText('operator_guidance_unavailable_helper'),
];
}
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.',
'key' => (string) ($resolutionCase['key'] ?? 'review_output.unknown'),
'title' => (string) ($resolutionCase['title'] ?? $this->overviewText('operator_guidance_review_output_title')),
'status' => $this->operatorGuidanceStatusFromResolutionCase($resolutionCase),
'tone' => $this->operatorGuidanceToneFromResolutionCase($resolutionCase),
'reason' => (string) ($resolutionCase['reason'] ?? $this->overviewText('reason_continue_review')),
'impact' => (string) ($resolutionCase['impact'] ?? $this->overviewText('impact_continue_review')),
'actionLabel' => (string) ($primaryAction['actionLabel'] ?? $this->overviewText('action_open_customer_workspace')),
'actionUrl' => is_string($primaryAction['actionUrl'] ?? null) ? (string) $primaryAction['actionUrl'] : null,
'actionDisabled' => (bool) ($primaryAction['actionDisabled'] ?? blank($primaryAction['actionUrl'] ?? null)),
'helperText' => is_string($primaryAction['helperText'] ?? null) ? (string) $primaryAction['helperText'] : null,
'secondaryActions' => $this->reviewOutputOperatorSecondaryActions(
resolutionCase: $resolutionCase,
tenant: $tenant,
user: $user,
latestReviewPack: $latestReviewPack,
primaryActionUrl: is_string($primaryAction['actionUrl'] ?? null) ? (string) $primaryAction['actionUrl'] : null,
),
'source' => [
'type' => 'review_output_resolution',
'key' => (string) ($resolutionCase['key'] ?? 'review_output.unknown'),
],
];
}
/**
* @param list<array<string, mixed>> $recommendedActions
* @return array<string, mixed>|null
*/
private function recommendedActionByKey(array $recommendedActions, string $key): ?array
{
return collect($recommendedActions)
->first(static fn (array $action): bool => ($action['key'] ?? null) === $key);
}
/**
* @param array<string, mixed> $action
* @return array<string, mixed>
*/
private function recommendedActionOperatorGuidance(array $action): array
{
$key = (string) ($action['key'] ?? 'dashboard_follow_up');
return [
'key' => 'recommended_action.'.$key,
'title' => $this->operatorGuidanceTitleForRecommendedAction($key),
'status' => $this->operatorGuidanceStatusForRecommendedAction($key),
'tone' => (string) ($action['tone'] ?? 'warning'),
'reason' => (string) ($action['reason'] ?? $this->overviewText('posture_action_needed_fallback_summary')),
'impact' => (string) ($action['impact'] ?? $this->overviewText('posture_action_needed_fallback_summary')),
'actionLabel' => (string) ($action['actionLabel'] ?? $this->overviewText('action_review_environment')),
'actionUrl' => is_string($action['actionUrl'] ?? null) ? (string) $action['actionUrl'] : null,
'actionDisabled' => (bool) ($action['actionDisabled'] ?? blank($action['actionUrl'] ?? null)),
'helperText' => is_string($action['helperText'] ?? null) ? (string) $action['helperText'] : null,
'secondaryActions' => [],
'source' => [
'type' => 'recommended_action',
'key' => $key,
],
];
}
/**
* @param array{status:string,tone:string,headline:string,summary:string} $posture
* @return array<string, mixed>
*/
private function noUrgentOperatorGuidance(ManagedEnvironment $tenant, ?User $user, array $posture): array
{
$reviewAction = $this->environmentReviewAction($tenant, $user, $this->overviewText('action_review_environment'));
return [
'key' => 'environment.no_urgent_action',
'title' => $this->overviewText('operator_guidance_no_urgent_title'),
'status' => (string) ($posture['status'] ?? $this->overviewText('status_calm')),
'tone' => (string) ($posture['tone'] ?? 'success'),
'reason' => (string) ($posture['headline'] ?? $this->overviewText('posture_calm_headline')),
'impact' => (string) ($posture['summary'] ?? $this->overviewText('posture_calm_summary')),
'actionLabel' => (string) ($reviewAction['actionLabel'] ?? $this->overviewText('action_review_environment')),
'actionUrl' => is_string($reviewAction['actionUrl'] ?? null) ? (string) $reviewAction['actionUrl'] : null,
'actionDisabled' => (bool) ($reviewAction['actionDisabled'] ?? blank($reviewAction['actionUrl'] ?? null)),
'helperText' => is_string($reviewAction['helperText'] ?? null) ? (string) $reviewAction['helperText'] : null,
'secondaryActions' => [],
'source' => [
'type' => 'dashboard_fallback',
],
];
}
/**
* @return array<string, mixed>|null
*/
private function reviewOutputResolutionCaseForDashboard(
ManagedEnvironment $tenant,
?User $user,
?EnvironmentReview $review,
): ?array {
if (! $review instanceof EnvironmentReview) {
return null;
}
$review->loadMissing([
'tenant',
'evidenceSnapshot',
'currentExportReviewPack.operationRun',
'operationRun',
'supersededByReview',
]);
$reviewUrl = $this->customerWorkspaceUrl($tenant, $user);
$evidenceUrl = $this->evidenceUrlForReviewOutput($tenant, $user, $review);
$operationUrl = $this->operationUrlForReviewOutput($review);
$successorReviewUrl = $this->successorReviewUrlForDashboard($tenant, $user, $review);
$guidance = ReviewPackOutputResolutionGuidance::fromReview($review, [
'download' => null,
'review' => $reviewUrl,
'evidence' => $evidenceUrl,
'operation' => $operationUrl,
]);
$resolutionCase = ReviewPackOutputResolutionAdapter::fromGuidance(
review: $review,
guidance: $guidance,
sourceSurface: 'environment_dashboard',
context: [
'urls' => [
'review' => $reviewUrl,
'evidence' => $evidenceUrl,
'operation' => $operationUrl,
'download' => null,
'successor_review' => $successorReviewUrl,
],
'execution' => [
'can_manage_review' => $this->canOpenTenantCapability($tenant, $user, Capabilities::ENVIRONMENT_REVIEW_MANAGE),
'successor_review_status' => $this->successorReviewStatusForDashboard($review),
],
],
);
return $this->decorateSuccessorDashboardResolutionCase($resolutionCase, $review);
}
/**
* @param array<string, mixed> $resolutionCase
* @return array<string, mixed>
*/
private function decorateSuccessorDashboardResolutionCase(array $resolutionCase, EnvironmentReview $review): array
{
if (data_get($resolutionCase, 'primary_action.key') !== 'open_successor_review') {
return $resolutionCase;
}
$successor = $this->successorReviewForDashboard($review);
if (! $successor instanceof EnvironmentReview || ! $successor->isMutable()) {
return $resolutionCase;
}
$canPublishSuccessor = app(EnvironmentReviewReadinessGate::class)->canPublish($successor);
return array_replace($resolutionCase, [
'title' => __('localization.review.draft_review_exists'),
'reason' => $canPublishSuccessor
? __('localization.review.draft_review_exists_ready_reason')
: __('localization.review.draft_review_exists_blocked_reason'),
'impact' => $canPublishSuccessor
? __('localization.review.draft_review_exists_ready_impact')
: __('localization.review.draft_review_exists_blocked_impact'),
]);
}
private function successorReviewForDashboard(EnvironmentReview $review): ?EnvironmentReview
{
if ($review->relationLoaded('supersededByReview')) {
return $review->supersededByReview instanceof EnvironmentReview
? $review->supersededByReview
: null;
}
if (! is_numeric($review->superseded_by_review_id)) {
return null;
}
return EnvironmentReview::query()
->with(['tenant', 'sections', 'evidenceSnapshot', 'currentExportReviewPack'])
->whereKey((int) $review->superseded_by_review_id)
->where('workspace_id', (int) $review->workspace_id)
->where('managed_environment_id', (int) $review->managed_environment_id)
->first();
}
private function successorReviewStatusForDashboard(EnvironmentReview $review): ?string
{
return $this->successorReviewForDashboard($review)?->status;
}
private function successorReviewUrlForDashboard(ManagedEnvironment $tenant, ?User $user, EnvironmentReview $review): ?string
{
if (! $this->canOpenTenantCapability($tenant, $user, Capabilities::ENVIRONMENT_REVIEW_VIEW)) {
return null;
}
if (! is_numeric($review->superseded_by_review_id)) {
return null;
}
return EnvironmentReviewResource::environmentScopedUrl('view', ['record' => (int) $review->superseded_by_review_id], $tenant);
}
private function customerWorkspaceUrl(ManagedEnvironment $tenant, ?User $user): ?string
{
$canOpenWorkspace = $user instanceof User
&& $user->canAccessTenant($tenant)
&& (
$user->can(Capabilities::ENVIRONMENT_REVIEW_VIEW, $tenant)
|| $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)
|| $user->can(Capabilities::EVIDENCE_VIEW, $tenant)
);
return $canOpenWorkspace ? CustomerReviewWorkspace::environmentFilterUrl($tenant) : null;
}
private function evidenceUrlForReviewOutput(ManagedEnvironment $tenant, ?User $user, EnvironmentReview $review): ?string
{
if (! $review->evidenceSnapshot instanceof EvidenceSnapshot) {
return $this->canOpenTenantCapability($tenant, $user, Capabilities::EVIDENCE_VIEW)
? EvidenceSnapshotResource::getUrl('index', tenant: $tenant)
: null;
}
if (! $this->canOpenTenantCapability($tenant, $user, Capabilities::EVIDENCE_VIEW)) {
return null;
}
return EvidenceSnapshotResource::getUrl('view', ['record' => $review->evidenceSnapshot], tenant: $tenant);
}
private function operationUrlForReviewOutput(EnvironmentReview $review): ?string
{
$operationRun = $review->currentExportReviewPack?->operationRun ?? $review->operationRun;
return $operationRun instanceof OperationRun
? OperationRunLinks::tenantlessView((int) $operationRun->getKey())
: null;
}
/**
* @param array<string, mixed>|null $action
* @return array{actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string}
*/
private function operatorGuidanceActionFromResolutionAction(?array $action): array
{
return [
'actionLabel' => is_string($action['label'] ?? null)
? (string) $action['label']
: $this->overviewText('action_review_environment'),
'actionUrl' => is_string($action['url'] ?? null) ? (string) $action['url'] : null,
'actionDisabled' => ! is_string($action['url'] ?? null) || trim((string) $action['url']) === '',
'helperText' => is_string($action['disabled_reason'] ?? null) ? (string) $action['disabled_reason'] : null,
];
}
/**
* @param array<string, mixed> $resolutionCase
* @return list<array{key:string,actionLabel:string,actionUrl:?string,actionDisabled:bool,helperText:?string}>
*/
private function reviewOutputOperatorSecondaryActions(
array $resolutionCase,
ManagedEnvironment $tenant,
?User $user,
?ReviewPack $latestReviewPack,
?string $primaryActionUrl,
): array {
$actions = collect(is_array($resolutionCase['secondary_actions'] ?? null) ? $resolutionCase['secondary_actions'] : [])
->filter(static fn (mixed $action): bool => is_array($action) && filled($action['url'] ?? null))
->map(function (array $action): array {
return [
'key' => (string) ($action['key'] ?? 'secondary_action'),
'actionLabel' => (string) ($action['label'] ?? $this->overviewText('action_review_environment')),
'actionUrl' => is_string($action['url'] ?? null) ? (string) $action['url'] : null,
'actionDisabled' => blank($action['url'] ?? null),
'helperText' => is_string($action['disabled_reason'] ?? null) ? (string) $action['disabled_reason'] : null,
];
})
->reject(static fn (array $action): bool => $primaryActionUrl !== null && $action['actionUrl'] === $primaryActionUrl)
->unique(static fn (array $action): string => $action['actionLabel'].'|'.$action['actionUrl'])
->values();
$workspaceAction = $this->customerWorkspaceAction($tenant, $user, $latestReviewPack);
if (
filled($workspaceAction['actionUrl'] ?? null)
&& $workspaceAction['actionUrl'] !== $primaryActionUrl
&& ! $actions->contains(static fn (array $action): bool => $action['actionUrl'] === $workspaceAction['actionUrl'])
) {
$actions->push([
'key' => 'customer_review_workspace',
'actionLabel' => $this->overviewText('action_open_customer_workspace'),
'actionUrl' => (string) $workspaceAction['actionUrl'],
'actionDisabled' => false,
'helperText' => null,
]);
}
return $actions
->take(3)
->all();
}
/**
* @param array<string, mixed> $resolutionCase
*/
private function operatorGuidanceStatusFromResolutionCase(array $resolutionCase): string
{
return match ((string) ($resolutionCase['status'] ?? 'action_required')) {
'blocked' => $this->overviewText('status_blocked'),
'ready' => $this->overviewText('status_calm'),
default => $this->overviewText('status_action_needed'),
};
}
/**
* @param array<string, mixed> $resolutionCase
*/
private function operatorGuidanceToneFromResolutionCase(array $resolutionCase): string
{
return match ((string) ($resolutionCase['severity'] ?? 'warning')) {
'critical' => 'danger',
'success' => 'success',
default => 'warning',
};
}
private function operatorGuidanceTitleForRecommendedAction(string $key): string
{
return match ($key) {
'required_permissions' => $this->overviewText('operator_guidance_provider_blocked_title'),
'delegated_permissions' => $this->overviewText('operator_guidance_provider_attention_title'),
'operations_requiring_attention' => $this->overviewText('operator_guidance_operations_title'),
'high_severity_findings', 'overdue_findings' => $this->overviewText('operator_guidance_findings_title'),
'risk_exceptions' => $this->overviewText('operator_guidance_risks_title'),
'recovery_posture' => $this->overviewText('operator_guidance_recovery_title'),
'continue_review' => $this->overviewText('operator_guidance_review_follow_up_title'),
default => $this->overviewText('operator_guidance_attention_title'),
};
}
private function operatorGuidanceStatusForRecommendedAction(string $key): string
{
return match ($key) {
'required_permissions' => $this->overviewText('status_blocked'),
'continue_review' => $this->overviewText('status_action_needed'),
default => $this->overviewText('status_action_needed'),
};
}
/**
* @param list<array<string, mixed>> $governanceStatus
* @param list<array<string, mixed>> $readinessCards
@ -545,6 +1030,26 @@ private function latestEnvironmentReview(ManagedEnvironment $tenant): ?Environme
->first();
}
private function latestReviewOutputReview(ManagedEnvironment $tenant): ?EnvironmentReview
{
$review = EnvironmentReview::query()
->with(['tenant', 'evidenceSnapshot', 'currentExportReviewPack', 'supersededByReview'])
->where('managed_environment_id', (int) $tenant->getKey())
->where(function (Builder $query): void {
$query
->whereNotNull('current_export_review_pack_id')
->orWhereIn('status', ['published', 'superseded']);
})
->latest('published_at')
->latest('generated_at')
->latest('id')
->first();
return $review instanceof EnvironmentReview
? $review
: $this->latestEnvironmentReview($tenant);
}
private function latestReviewPack(ManagedEnvironment $tenant): ?ReviewPack
{
return ReviewPack::query()

View File

@ -186,6 +186,8 @@
'posture_calm_summary' => 'Aktuelle Findings, Berechtigungen, Wiederherstellungsstatus und letzte Vorgänge zeigen derzeit keinen dringenden Folgeschritt.',
'posture_action_needed_fallback_summary' => 'Die Umgebung benötigt noch Operator-Nachverfolgung, bevor die Startseite als ruhig gelten kann.',
'section_recommended_actions' => 'Empfohlene nächste Aktionen',
'section_additional_follow_ups' => 'Weitere Folgeschritte',
'section_additional_follow_ups_summary' => 'Diese Folgeschritte bleiben bewusst sekundär zur dominanten Guidance oben.',
'section_governance_status' => 'Governance-Status',
'section_readiness' => 'Review- und Nachweisbereitschaft',
'section_recent_operations' => 'Letzte Vorgänge',
@ -193,6 +195,8 @@
'priority_label' => 'Priorität :priority',
'label_reason' => 'Grund',
'label_impact' => 'Auswirkung',
'label_why_this_matters' => 'Warum das wichtig ist',
'label_recommended_next_action' => 'Empfohlener nächster Schritt',
'empty_recommended_actions_headline' => 'Derzeit wartet keine unmittelbare Aktion.',
'empty_recommended_actions_summary' => 'Die Umgebung wirkt aktuell ruhig. Nutzen Sie die Status- und Bereitschaftsbereiche unten, um zu bestätigen, was gesund ist und was lediglich nicht verfügbar ist.',
'empty_recent_operations_headline' => 'Noch keine letzten Vorgänge.',
@ -241,6 +245,7 @@
'action_open_review_pack' => 'Review-Paket öffnen',
'action_open_review' => 'Review öffnen',
'action_open_reviews' => 'Reviews öffnen',
'action_review_environment' => 'Umgebung prüfen',
'reason_missing_application_permissions' => ':count Anwendungsberechtigung(en) fehlen noch.',
'impact_missing_application_permissions' => 'Provider-gestützte Inventarisierung, Verifikation und Berichte bleiben blockiert, bis die Zustimmung wiederhergestellt ist.',
'reason_missing_delegated_permissions' => ':count delegierte Berechtigung(en) benötigen noch Aufmerksamkeit.',
@ -258,6 +263,17 @@
'impact_operations_requiring_attention' => 'Die Umgebung sollte nicht als vollständig gesund betrachtet werden, bis das Vorgangsergebnis geprüft wurde.',
'reason_continue_review' => 'Kundensichere Ausgabe ist noch nicht vollständig bereit.',
'impact_continue_review' => 'Die Review-Ausgabe bleibt teilweise, bis Review-, Nachweis- und Paketflächen sauber zusammenpassen.',
'operator_guidance_attention_title' => 'Die Umgebung benötigt Aufmerksamkeit',
'operator_guidance_provider_blocked_title' => 'Provider-Bereitschaft blockiert die Evidence-Aktualisierung',
'operator_guidance_provider_attention_title' => 'Provider-Bereitschaft benötigt Aufmerksamkeit',
'operator_guidance_findings_title' => 'Governance-Findings benötigen Aufmerksamkeit',
'operator_guidance_operations_title' => 'Vorgänge benötigen Follow-up',
'operator_guidance_risks_title' => 'Risikoausnahmen müssen geprüft werden',
'operator_guidance_recovery_title' => 'Wiederherstellungsstatus muss geprüft werden',
'operator_guidance_review_follow_up_title' => 'Review-Follow-up läuft noch',
'operator_guidance_review_output_title' => 'Review-Output benötigt Aufmerksamkeit',
'operator_guidance_no_urgent_title' => 'Keine dringende Operator-Aktion',
'operator_guidance_unavailable_helper' => 'Derzeit ist kein einzelner repo-real belegter Folgeschritt verfügbar.',
'operations_attention_title' => 'Vorgänge mit Aufmerksamkeitsbedarf',
'operations_attention_badge_follow_up' => 'Follow-up erforderlich',
'operations_attention_badge_stale' => 'Aufmerksamkeit nötig',

View File

@ -186,6 +186,8 @@
'posture_calm_summary' => 'Current findings, permissions, recovery posture, and recent operations do not show an urgent follow-up path.',
'posture_action_needed_fallback_summary' => 'The environment still needs operator follow-up before the landing page can stay calm.',
'section_recommended_actions' => 'Recommended next actions',
'section_additional_follow_ups' => 'Additional follow-ups',
'section_additional_follow_ups_summary' => 'These follow-ups stay secondary to the dominant guidance above.',
'section_governance_status' => 'Governance status',
'section_readiness' => 'Review & evidence readiness',
'section_recent_operations' => 'Recent operations',
@ -193,6 +195,8 @@
'priority_label' => 'Priority :priority',
'label_reason' => 'Reason',
'label_impact' => 'Impact',
'label_why_this_matters' => 'Why this matters',
'label_recommended_next_action' => 'Recommended next action',
'empty_recommended_actions_headline' => 'No immediate action is waiting.',
'empty_recommended_actions_summary' => 'The environment currently looks calm. Use the status and readiness sections below to confirm what is healthy and what is simply unavailable.',
'empty_recent_operations_headline' => 'No recent operations yet.',
@ -241,6 +245,7 @@
'action_open_review_pack' => 'Open review pack',
'action_open_review' => 'Open review',
'action_open_reviews' => 'Open reviews',
'action_review_environment' => 'Review environment',
'reason_missing_application_permissions' => ':count application permission(s) are still missing.',
'impact_missing_application_permissions' => 'Provider-backed inventory, verification, and reporting flows stay blocked until consent is restored.',
'reason_missing_delegated_permissions' => ':count delegated permission(s) still need attention.',
@ -258,6 +263,17 @@
'impact_operations_requiring_attention' => 'The environment should not be treated as fully healthy until the operation outcome has been reviewed.',
'reason_continue_review' => 'Customer-safe output is not fully ready yet.',
'impact_continue_review' => 'Review output stays partial until the review, evidence, and pack surfaces line up cleanly.',
'operator_guidance_attention_title' => 'Environment needs attention',
'operator_guidance_provider_blocked_title' => 'Provider readiness blocks evidence refresh',
'operator_guidance_provider_attention_title' => 'Provider readiness needs attention',
'operator_guidance_findings_title' => 'Governance findings need attention',
'operator_guidance_operations_title' => 'Operations require follow-up',
'operator_guidance_risks_title' => 'Risk exceptions need review',
'operator_guidance_recovery_title' => 'Recovery posture needs review',
'operator_guidance_review_follow_up_title' => 'Review follow-up is in progress',
'operator_guidance_review_output_title' => 'Review output needs attention',
'operator_guidance_no_urgent_title' => 'No urgent operator action',
'operator_guidance_unavailable_helper' => 'No single repo-real follow-up is currently available.',
'operations_attention_title' => 'Operations requiring attention',
'operations_attention_badge_follow_up' => 'Follow-up required',
'operations_attention_badge_stale' => 'Needs attention',

View File

@ -7,6 +7,38 @@
'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',
};
$secondaryRecommendedActions = array_values(array_filter(
$recommendedActions,
static function (array $action) use ($operatorGuidance): bool {
$guidanceKey = $operatorGuidance['key'] ?? null;
$guidanceLabel = $operatorGuidance['actionLabel'] ?? null;
$guidanceUrl = $operatorGuidance['actionUrl'] ?? null;
if (($action['key'] ?? null) === $guidanceKey) {
return false;
}
if (
($action['actionLabel'] ?? null) === $guidanceLabel
&& ($action['actionUrl'] ?? null) === $guidanceUrl
) {
return false;
}
return true;
},
));
$operatorGuidanceKey = is_string($operatorGuidance['key'] ?? null) ? (string) $operatorGuidance['key'] : null;
$useCompactRecommendedActions = $secondaryRecommendedActions !== [] && (
($operatorGuidanceKey !== null && str_starts_with($operatorGuidanceKey, 'provider_readiness.'))
|| ($operatorGuidanceKey !== null && str_starts_with($operatorGuidanceKey, 'review_output.'))
);
$recommendedActionsHeading = $useCompactRecommendedActions
? __('localization.dashboard.overview.section_additional_follow_ups')
: __('localization.dashboard.overview.section_recommended_actions');
$recommendedActionsDescription = $useCompactRecommendedActions
? __('localization.dashboard.overview.section_additional_follow_ups_summary')
: 'Recommended next actions are derived from repo-backed blockers and proof gaps.';
@endphp
<div
@ -20,7 +52,7 @@ class="grid w-full min-w-0 gap-6 xl:grid-cols-12"
<!-- Left Column (Main) -->
<div data-testid="tenant-dashboard-overview-main" class="flex w-full min-w-0 flex-col gap-6 xl:col-span-8">
<x-filament::section>
<div data-testid="tenant-dashboard-readiness-decision" class="grid gap-5 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-start">
<div data-testid="tenant-dashboard-readiness-decision" class="grid gap-4 xl:grid-cols-[minmax(0,1fr)_18rem] xl:items-start">
<div class="min-w-0 space-y-4">
<div class="flex flex-wrap items-center gap-3">
<h2 class="text-base font-semibold text-gray-950 dark:text-white">
@ -31,40 +63,68 @@ class="grid w-full min-w-0 gap-6 xl:grid-cols-12"
</span>
</div>
<dl class="grid gap-3 md:grid-cols-3">
<div class="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-white/10 dark:bg-white/5">
<div data-testid="tenant-dashboard-operator-guidance-title" class="max-w-3xl">
<p class="text-lg font-semibold leading-7 text-gray-950 dark:text-white">
{{ $readinessDecision['title'] ?? ($operatorGuidance['title'] ?? __('localization.dashboard.overview.environment_context_unavailable_headline')) }}
</p>
</div>
<dl class="overflow-hidden rounded-xl border border-gray-200 bg-gray-50/80 dark:border-white/10 dark:bg-white/5">
<div class="grid gap-1 px-4 py-3 md:grid-cols-[9rem_minmax(0,1fr)] md:gap-4">
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $readinessDecision['statusLabel'] ?? 'Status' }}</dt>
<dd class="mt-1 text-sm font-medium text-gray-950 dark:text-white">{{ $readinessDecision['status'] ?? ($posture['status'] ?? __('localization.dashboard.overview.status_unavailable')) }}</dd>
<dd class="text-sm font-semibold text-gray-950 dark:text-white">{{ $readinessDecision['status'] ?? ($posture['status'] ?? __('localization.dashboard.overview.status_unavailable')) }}</dd>
</div>
<div class="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-white/10 dark:bg-white/5">
<div class="grid gap-1 border-t border-gray-200 px-4 py-3 md:grid-cols-[9rem_minmax(0,1fr)] md:gap-4 dark:border-white/10">
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $readinessDecision['reasonLabel'] ?? 'Reason' }}</dt>
<dd class="mt-1 text-sm text-gray-700 dark:text-gray-200">{{ $readinessDecision['reason'] ?? ($posture['headline'] ?? __('localization.dashboard.overview.tenant_context_unavailable_headline')) }}</dd>
<dd class="text-sm leading-6 text-gray-700 dark:text-gray-200">{{ $readinessDecision['reason'] ?? ($posture['headline'] ?? __('localization.dashboard.overview.tenant_context_unavailable_headline')) }}</dd>
</div>
<div class="rounded-xl border border-gray-200 bg-gray-50/80 p-4 dark:border-white/10 dark:bg-white/5">
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $readinessDecision['impactLabel'] ?? 'Impact' }}</dt>
<dd class="mt-1 text-sm text-gray-700 dark:text-gray-200">{{ $readinessDecision['impact'] ?? ($posture['summary'] ?? __('localization.dashboard.overview.tenant_context_unavailable_summary')) }}</dd>
<div class="grid gap-1 border-t border-gray-200 px-4 py-3 md:grid-cols-[9rem_minmax(0,1fr)] md:gap-4 dark:border-white/10">
<dt class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $readinessDecision['impactLabel'] ?? 'Why this matters' }}</dt>
<dd class="text-sm leading-6 text-gray-700 dark:text-gray-200">{{ $readinessDecision['impact'] ?? ($posture['summary'] ?? __('localization.dashboard.overview.tenant_context_unavailable_summary')) }}</dd>
</div>
</dl>
</div>
<div data-testid="tenant-dashboard-primary-next-action" class="min-w-0 rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5 lg:w-64">
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $readinessDecision['nextActionLabel'] ?? 'Next action' }}</div>
<div class="mt-2 text-sm font-semibold text-gray-950 dark:text-white">{{ $readinessDecision['actionLabel'] ?? 'Review readiness proof' }}</div>
<div data-testid="tenant-dashboard-primary-next-action" class="flex min-w-0 flex-col gap-4 rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5 xl:w-72 xl:self-start">
<div class="space-y-2">
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">{{ $readinessDecision['nextActionLabel'] ?? __('localization.dashboard.overview.label_recommended_next_action') }}</div>
<div class="text-sm font-semibold leading-6 text-gray-950 dark:text-white">{{ $readinessDecision['actionLabel'] ?? 'Review readiness proof' }}</div>
</div>
@if (filled($readinessDecision['actionUrl'] ?? null))
<x-filament::button class="mt-4" tag="a" :href="$readinessDecision['actionUrl']" size="sm">
<x-filament::button class="w-full justify-center" tag="a" :href="$readinessDecision['actionUrl']" size="sm">
{{ $readinessDecision['actionLabel'] ?? 'Review readiness proof' }}
</x-filament::button>
@else
<x-filament::button class="mt-4" size="sm" color="gray" disabled>
<x-filament::button class="w-full justify-center" size="sm" color="gray" disabled>
{{ $readinessDecision['actionLabel'] ?? 'Review readiness proof' }}
</x-filament::button>
@endif
@if (filled($readinessDecision['helperText'] ?? null))
<p class="mt-3 text-xs leading-5 text-gray-500 dark:text-gray-400">{{ $readinessDecision['helperText'] }}</p>
@if (filled($readinessDecision['helperText'] ?? null) || ($readinessDecision['secondaryActions'] ?? []) !== [])
<div class="space-y-3 border-t border-gray-100 pt-3 dark:border-white/10">
@if (filled($readinessDecision['helperText'] ?? null))
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">{{ $readinessDecision['helperText'] }}</p>
@endif
@if (($readinessDecision['secondaryActions'] ?? []) !== [])
<div data-testid="tenant-dashboard-operator-guidance-secondary-actions" class="flex flex-wrap gap-x-3 gap-y-2">
@foreach ($readinessDecision['secondaryActions'] as $secondaryAction)
@if (filled($secondaryAction['actionUrl'] ?? null))
<a
data-testid="tenant-dashboard-operator-guidance-secondary-action"
href="{{ $secondaryAction['actionUrl'] }}"
class="text-xs font-medium text-primary-600 underline-offset-2 hover:underline dark:text-primary-400"
>
{{ $secondaryAction['actionLabel'] ?? __('localization.dashboard.overview.action_review_environment') }}
</a>
@endif
@endforeach
</div>
@endif
</div>
@endif
</div>
</div>
@ -95,21 +155,61 @@ class="grid w-full min-w-0 gap-6 xl:grid-cols-12"
</x-filament::section>
<!-- Recommended Actions -->
<x-filament::section :heading="__('localization.dashboard.overview.section_recommended_actions')">
<x-filament::section :heading="$recommendedActionsHeading">
<x-slot name="description">
Recommended next actions are derived from repo-backed blockers and proof gaps.
{{ $recommendedActionsDescription }}
</x-slot>
@if ($recommendedActions === [])
@if ($secondaryRecommendedActions === [])
<div data-testid="tenant-dashboard-recommended-actions-empty" class="rounded-xl border border-success-200 bg-success-50/80 p-5 dark:border-success-800 dark:bg-success-500/10">
<div class="text-sm font-semibold text-success-700 dark:text-success-300">{{ __('localization.dashboard.overview.empty_recommended_actions_headline') }}</div>
<p class="mt-2 text-sm leading-6 text-success-700/90 dark:text-success-200/90">
{{ __('localization.dashboard.overview.empty_recommended_actions_summary') }}
</p>
</div>
@elseif ($useCompactRecommendedActions)
<div data-testid="tenant-dashboard-recommended-actions" data-recommended-actions-style="compact" class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm dark:border-white/10 dark:bg-white/5">
@foreach (array_slice($secondaryRecommendedActions, 0, 2) as $index => $action)
<div
data-testid="tenant-dashboard-recommended-action"
data-action-key="{{ $action['key'] }}"
class="flex min-w-0 flex-col gap-3 px-4 py-4 sm:flex-row sm:items-start sm:justify-between {{ $index > 0 ? 'border-t border-gray-100 dark:border-white/10' : '' }}"
>
<div class="min-w-0 space-y-2">
<div class="flex items-center gap-2">
@if (filled($action['icon'] ?? null))
<x-filament::icon
data-testid="tenant-dashboard-recommended-action-icon"
data-action-key="{{ $action['key'] }}"
data-icon="{{ $action['icon'] }}"
:icon="$action['icon']"
class="h-4 w-4 shrink-0 text-gray-400 dark:text-gray-500"
/>
@endif
<h3 class="min-w-0 text-sm font-semibold text-gray-950 dark:text-white">{{ $action['title'] }}</h3>
</div>
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">
<span class="font-medium text-gray-700 dark:text-gray-300">Reason:</span> {{ $action['reason'] }}
</p>
</div>
@if (filled($action['actionUrl'] ?? null))
<a
data-testid="tenant-dashboard-secondary-action"
href="{{ $action['actionUrl'] }}"
class="shrink-0 text-xs font-semibold text-primary-600 underline-offset-2 hover:underline dark:text-primary-400"
>
{{ $action['actionLabel'] ?? 'Review' }}
</a>
@endif
</div>
@endforeach
</div>
@else
<div data-testid="tenant-dashboard-recommended-actions" class="grid min-w-0 gap-4">
@foreach (array_slice($recommendedActions, 0, 3) as $index => $action)
@foreach (array_slice($secondaryRecommendedActions, 0, 3) as $index => $action)
<div data-testid="tenant-dashboard-recommended-action" data-action-key="{{ $action['key'] }}" class="flex min-w-0 items-start gap-4 rounded-xl border border-gray-200 bg-white p-5 shadow-sm dark:border-white/10 dark:bg-white/5 max-sm:flex-col max-sm:items-stretch">
<div data-testid="tenant-dashboard-recommended-action-priority" class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-gray-200 bg-gray-50 text-sm font-semibold text-gray-700 dark:border-white/10 dark:bg-white/5 dark:text-gray-200">
{{ $index + 1 }}

View File

@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\EnvironmentDashboard;
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentPermission;
use App\Models\ProviderConnection;
use App\Models\ReviewPack;
use App\Models\User;
use App\Services\EnvironmentReviews\EnvironmentReviewLifecycleService;
use App\Support\EnvironmentReviewStatus;
use App\Support\Workspaces\WorkspaceContext;
pest()->browser()->timeout(20_000);
function spec352BrowserScreenshotName(string $name): string
{
return 'spec352-environment-dashboard-'.$name;
}
/**
* @return array{
* review:\App\Models\EnvironmentReview,
* successor:\App\Models\EnvironmentReview|null,
* reviewPack:ReviewPack,
* }
*/
function spec352BrowserSeedBlockedReviewOutput(ManagedEnvironment $environment, User $user, bool $withSuccessorDraft = false): array
{
$snapshot = seedPartialEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0, operationRunCount: 0);
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
$review->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'published_at' => now()->subHour(),
'published_by_user_id' => (int) $user->getKey(),
'summary' => array_replace_recursive(is_array($review->summary) ? $review->summary : [], [
'publish_blockers' => ['Operator approval note is still missing.'],
]),
])->save();
$reviewPack = ReviewPack::factory()->ready()->create([
'managed_environment_id' => (int) $environment->getKey(),
'workspace_id' => (int) $environment->workspace_id,
'environment_review_id' => (int) $review->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'file_path' => 'review-packs/spec352-browser.zip',
'file_disk' => 'exports',
'generated_at' => now()->subMinutes(10),
'options' => [
'include_pii' => false,
'include_operations' => true,
],
]);
$review->forceFill([
'current_export_review_pack_id' => (int) $reviewPack->getKey(),
])->save();
$successor = null;
if ($withSuccessorDraft) {
$successor = app(EnvironmentReviewLifecycleService::class)->createNextReview($review->fresh(), $user, $snapshot);
}
return [
'review' => $review->fresh(),
'successor' => $successor?->fresh(),
'reviewPack' => $reviewPack->fresh(),
];
}
function spec352BrowserApplicationPermissionKey(): string
{
$permission = collect(spec283ConfiguredPermissionRows())
->first(static fn (mixed $row): bool => is_array($row) && ($row['type'] ?? null) === 'application');
expect($permission)->not->toBeNull();
return (string) $permission['key'];
}
function spec352BrowserSeedPermissionRows(
ManagedEnvironment $environment,
array $missingKeys = [],
array $errorKeys = [],
): void {
foreach (spec283ConfiguredPermissionRows() as $permission) {
if (! is_array($permission)) {
continue;
}
$permissionKey = (string) ($permission['key'] ?? '');
if ($permissionKey === '') {
continue;
}
ManagedEnvironmentPermission::query()->updateOrCreate(
[
'managed_environment_id' => (int) $environment->getKey(),
'permission_key' => $permissionKey,
'workspace_id' => (int) $environment->workspace_id,
],
[
'status' => in_array($permissionKey, $errorKeys, true)
? 'error'
: (in_array($permissionKey, $missingKeys, true) ? 'missing' : 'granted'),
'details' => ['source' => 'spec352-browser-test'],
'last_checked_at' => now(),
],
);
}
}
function spec352BrowserActAs(User $user, ManagedEnvironment $environment): void
{
test()->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id,
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
(string) $environment->workspace_id => (int) $environment->getKey(),
],
]);
}
it('smokes provider-blocker guidance as the dominant dashboard case', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
ProviderConnection::factory()->platform()->consentGranted()->create([
'managed_environment_id' => (int) $environment->getKey(),
'workspace_id' => (int) $environment->workspace_id,
'is_default' => true,
]);
spec352BrowserSeedBlockedReviewOutput($environment, $user, withSuccessorDraft: true);
$missingPermissionKey = spec352BrowserApplicationPermissionKey();
spec352BrowserSeedPermissionRows($environment, missingKeys: [$missingPermissionKey]);
spec352BrowserActAs($user, $environment);
visit(EnvironmentDashboard::getUrl(panel: 'admin', tenant: $environment))
->waitForText('Provider readiness blocks evidence refresh')
->assertSee('Recommended next action')
->assertSee('Review permissions')
->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-primary-next-action\"]')?.textContent?.includes('Review permissions') ?? false", true)
->assertScript("Array.from(document.querySelectorAll('a')).filter((node) => node.textContent?.trim().includes('Review permissions')).length === 1", true)
->assertScript("document.querySelectorAll('[data-testid=\"tenant-dashboard-operator-guidance-secondary-action\"]').length === 0", true)
->assertDontSee('No single repo-real follow-up is currently available.')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec352BrowserScreenshotName('provider-blocker'));
});
it('smokes review-output guidance with subordinate secondary links when provider blockers are absent', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
ProviderConnection::factory()->platform()->consentGranted()->create([
'managed_environment_id' => (int) $environment->getKey(),
'workspace_id' => (int) $environment->workspace_id,
'is_default' => true,
]);
spec352BrowserSeedBlockedReviewOutput($environment, $user, withSuccessorDraft: true);
spec352BrowserSeedPermissionRows($environment);
spec352BrowserActAs($user, $environment);
visit(EnvironmentDashboard::getUrl(panel: 'admin', tenant: $environment))
->waitForText('Draft review exists')
->assertSee('Open draft review')
->assertSee('Additional follow-ups')
->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-primary-next-action\"]')?.textContent?.includes('Open draft review') ?? false", true)
->assertScript("Array.from(document.querySelectorAll('a')).filter((node) => node.textContent?.trim().includes('Open draft review')).length === 1", true)
->assertScript("document.querySelectorAll('[data-testid=\"tenant-dashboard-operator-guidance-secondary-action\"]').length >= 1", true)
->assertScript("document.querySelector('[data-recommended-actions-style=\"compact\"]') !== null", true)
->assertDontSee('No single repo-real follow-up is currently available.')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec352BrowserScreenshotName('review-output'));
});
it('smokes the no-urgent-action dashboard state with preserved secondary proof surfaces', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
ProviderConnection::factory()->platform()->consentGranted()->create([
'managed_environment_id' => (int) $environment->getKey(),
'workspace_id' => (int) $environment->workspace_id,
'is_default' => true,
]);
spec352BrowserSeedPermissionRows($environment);
workspaceOverviewSeedQuietTenantTruth($environment);
$backupSet = workspaceOverviewSeedHealthyBackup($environment);
workspaceOverviewSeedRestoreHistory($environment, $backupSet, 'completed');
spec352BrowserActAs($user, $environment);
visit(EnvironmentDashboard::getUrl(panel: 'admin', tenant: $environment))
->waitForText('No urgent operator action')
->assertSee('Review environment')
->assertSee('Readiness proof')
->assertScript("document.querySelector('[data-testid=\"tenant-dashboard-recommended-actions-empty\"]') !== null", true)
->assertScript("Array.from(document.querySelectorAll('a')).filter((node) => node.textContent?.trim().includes('Review environment')).length === 1", true)
->assertDontSee('No single repo-real follow-up is currently available.')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec352BrowserScreenshotName('no-urgent-action'));
});

View File

@ -253,9 +253,9 @@ function tenantDashboardButtonClassesForXPath(string $content, string $xpathExpr
->assertSuccessful()
->getContent();
$recommendedButtonClasses = tenantDashboardButtonClassesForXPath(
$compactActionLinkClasses = tenantDashboardButtonClassesForXPath(
$content,
"//*[@data-testid='tenant-dashboard-recommended-action']//*[self::a or self::button][contains(@class, 'fi-btn')]",
"//*[@data-recommended-actions-style='compact']//*[@data-testid='tenant-dashboard-secondary-action']",
);
$asideButtonClasses = tenantDashboardButtonClassesForXPath(
$content,
@ -266,16 +266,17 @@ function tenantDashboardButtonClassesForXPath(string $content, string $xpathExpr
"//*[@data-testid='tenant-dashboard-recommended-action-priority']",
);
expect(substr_count($content, 'data-testid="tenant-dashboard-recommended-action"'))->toBe(3)
->and(substr_count($content, 'data-testid="tenant-dashboard-recommended-action-icon"'))->toBe(3)
expect($content)->toContain('Additional follow-ups')
->and(substr_count($content, 'data-recommended-actions-style="compact"'))->toBe(1)
->and(substr_count($content, 'data-testid="tenant-dashboard-recommended-action"'))->toBe(2)
->and(substr_count($content, 'data-testid="tenant-dashboard-recommended-action-icon"'))->toBe(2)
->and($content)->toContain('data-icon="heroicon-m-shield-exclamation"')
->and($content)->toContain('data-icon="heroicon-o-exclamation-triangle"')
->and($recommendedButtonClasses)->not->toBeEmpty()
->and($asideButtonClasses)->not->toBeEmpty()
->and(collect([...$recommendedButtonClasses, ...$asideButtonClasses])->contains(static fn (string $classes): bool => str_contains($classes, 'fi-outlined')))->toBeFalse()
->and(collect($priorityMarkerClasses)->every(static fn (string $classes): bool => str_contains($classes, 'border-gray-200')
&& str_contains($classes, 'bg-gray-50')
&& str_contains($classes, 'text-gray-700')))->toBeTrue();
->and($compactActionLinkClasses)->not->toBeEmpty()
->and($asideButtonClasses)->toBe([])
->and(collect($compactActionLinkClasses)->every(static fn (string $classes): bool => str_contains($classes, 'text-primary-600')
&& ! str_contains($classes, 'fi-btn')))->toBeTrue()
->and($priorityMarkerClasses)->toBe([]);
});
it('assigns semantically distinct icons to overdue-findings and recovery-posture follow-ups', function (): void {

View File

@ -58,7 +58,7 @@
->assertSee('Reason')
->assertSee('Impact')
->assertSee('Readiness proof')
->assertSee('Next action')
->assertSee('Recommended next action')
->assertSee('Readiness dimensions')
->assertSee('Recommended next actions')
->assertSee('Supporting signals')

View File

@ -0,0 +1,274 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\EnvironmentDashboard;
use App\Filament\Resources\EnvironmentReviewResource;
use App\Filament\Widgets\Dashboard\EnvironmentDashboardOverview;
use App\Models\ManagedEnvironment;
use App\Models\ReviewPack;
use App\Models\User;
use App\Services\EnvironmentReviews\EnvironmentReviewLifecycleService;
use App\Services\Intune\ManagedEnvironmentRequiredPermissionsViewModelBuilder;
use App\Support\EnvironmentReviewStatus;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use function Pest\Laravel\mock;
uses(RefreshDatabase::class);
function mockSpec352FeatureDashboardPermissions(array $overview = []): void
{
mock(ManagedEnvironmentRequiredPermissionsViewModelBuilder::class, function ($mock) use ($overview): void {
$mock->shouldReceive('build')->andReturn([
'overview' => array_replace_recursive([
'overall' => 'ready',
'counts' => [
'missing_application' => 0,
'missing_delegated' => 0,
],
'freshness' => [
'is_stale' => false,
'last_refreshed_at' => now()->toIso8601String(),
],
], $overview),
]);
});
}
/**
* @return array{
* review:\App\Models\EnvironmentReview,
* successor:\App\Models\EnvironmentReview|null,
* reviewPack:ReviewPack,
* }
*/
function spec352FeatureSeedBlockedReviewOutput(ManagedEnvironment $environment, User $user, bool $withSuccessorDraft = false): array
{
$snapshot = seedPartialEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0, operationRunCount: 0);
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
$review->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'published_at' => now()->subHour(),
'published_by_user_id' => (int) $user->getKey(),
'summary' => array_replace_recursive(is_array($review->summary) ? $review->summary : [], [
'publish_blockers' => ['Operator approval note is still missing.'],
]),
])->save();
$reviewPack = ReviewPack::factory()->ready()->create([
'managed_environment_id' => (int) $environment->getKey(),
'workspace_id' => (int) $environment->workspace_id,
'environment_review_id' => (int) $review->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'file_path' => 'review-packs/spec352-feature.zip',
'file_disk' => 'exports',
'generated_at' => now()->subMinutes(10),
'options' => [
'include_pii' => false,
'include_operations' => true,
],
]);
$review->forceFill([
'current_export_review_pack_id' => (int) $reviewPack->getKey(),
])->save();
$successor = null;
if ($withSuccessorDraft) {
$successor = app(EnvironmentReviewLifecycleService::class)->createNextReview($review->fresh(), $user, $snapshot);
}
return [
'review' => $review->fresh(),
'successor' => $successor?->fresh(),
'reviewPack' => $reviewPack->fresh(),
];
}
/**
* @return list<string>
*/
function spec352DashboardHrefs(string $html, string $xpathExpression): array
{
$dom = new DOMDocument;
libxml_use_internal_errors(true);
$dom->loadHTML($html);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
$nodes = $xpath->query($xpathExpression);
if ($nodes === false) {
return [];
}
return collect(iterator_to_array($nodes))
->map(static fn (DOMNode $node): string => (string) $node->nodeValue)
->filter()
->values()
->all();
}
function spec352DashboardNodeCount(string $html, string $xpathExpression): int
{
$dom = new DOMDocument;
libxml_use_internal_errors(true);
$dom->loadHTML($html);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
$nodes = $xpath->query($xpathExpression);
if ($nodes === false) {
return 0;
}
return count(iterator_to_array($nodes));
}
it('renders review output guidance as one dominant top case while preserving proof surfaces', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
mockSpec352FeatureDashboardPermissions();
spec352FeatureSeedBlockedReviewOutput($environment, $user, withSuccessorDraft: true);
$this->actingAs($user);
setAdminPanelContext($environment);
$component = Livewire::test(EnvironmentDashboardOverview::class)
->assertSee('Draft review exists')
->assertSee('Open draft review')
->assertSee('Additional follow-ups')
->assertSee('Readiness dimensions')
->assertSee('Readiness proof')
->assertSee('Supporting signals')
->assertSee('Recommended next action');
$html = $component->html();
expect(substr_count($html, 'data-testid="tenant-dashboard-operator-guidance-title"'))->toBe(1)
->and(substr_count($html, 'data-testid="tenant-dashboard-primary-next-action"'))->toBe(1)
->and(substr_count($html, 'data-testid="tenant-dashboard-operator-guidance-secondary-actions"'))->toBe(1)
->and(substr_count($html, 'data-recommended-actions-style="compact"'))->toBe(1)
->and($html)->not->toContain('No single repo-real follow-up is currently available.')
->and(spec352DashboardNodeCount($html, "//*[@data-testid='tenant-dashboard-recommended-action']//*[contains(@class, 'fi-btn')]"))->toBe(0)
->and(substr_count($html, 'data-testid="tenant-dashboard-readiness-proof-panel"'))->toBe(1)
->and(substr_count($html, 'data-testid="tenant-dashboard-supporting-signals"'))->toBe(1);
});
it('keeps dashboard guidance links scoped to the current environment and avoids mutation CTAs in the top block', function (): void {
$environment = ManagedEnvironment::factory()->create(['name' => 'Spec352 Active Environment']);
$otherEnvironment = ManagedEnvironment::factory()->create(['name' => 'Spec352 Other Environment']);
[$user, $environment] = createUserWithTenant(
tenant: $environment,
role: 'owner',
workspaceRole: 'manager',
);
createUserWithTenant(
tenant: $otherEnvironment,
user: $user,
role: 'owner',
workspaceRole: 'manager',
);
mockSpec352FeatureDashboardPermissions();
$activeState = spec352FeatureSeedBlockedReviewOutput($environment, $user, withSuccessorDraft: true);
$otherState = spec352FeatureSeedBlockedReviewOutput($otherEnvironment, $user, withSuccessorDraft: true);
$this->actingAs($user);
setAdminPanelContext($environment);
$component = Livewire::test(EnvironmentDashboardOverview::class)
->assertSee('Draft review exists')
->assertSee('Open draft review');
$html = $component->html();
$primaryHref = spec352DashboardHrefs(
$html,
"//*[@data-testid='tenant-dashboard-primary-next-action']//a/@href",
);
$secondaryHrefs = spec352DashboardHrefs(
$html,
"//*[@data-testid='tenant-dashboard-operator-guidance-secondary-action']/@href",
);
expect($primaryHref)->not->toBe([])
->and(collect($primaryHref)->contains(EnvironmentReviewResource::environmentScopedUrl(
'view',
['record' => $activeState['successor']],
$environment,
)))->toBeTrue()
->and($secondaryHrefs)->not->toBe([])
->and(collect($secondaryHrefs)->contains(
fn (string $href): bool => str_contains($href, (string) $environment->getRouteKey())
|| str_contains($href, (string) $environment->workspace->slug)
))->toBeTrue()
->and(collect($secondaryHrefs)->contains(
fn (string $href): bool => str_contains($href, (string) $otherEnvironment->getRouteKey())
|| str_contains($href, (string) $otherEnvironment->workspace->slug)
|| str_contains($href, '/environment-reviews/'.($otherState['successor']?->getKey()))
))->toBeFalse()
->and($html)->not->toContain('createNextReview')
->and($html)->not->toContain('publishReview')
->and($html)->not->toContain('refreshReviewInputs');
});
it('keeps only one clickable primary CTA on the page when review-output guidance is dominant', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
mockSpec352FeatureDashboardPermissions();
spec352FeatureSeedBlockedReviewOutput($environment, $user, withSuccessorDraft: true);
$this->actingAs($user);
setAdminEnvironmentContext($environment);
$content = $this->get(EnvironmentDashboard::getUrl(panel: 'admin', tenant: $environment))
->assertSuccessful()
->assertSee('Draft review exists')
->assertSee('Open draft review')
->assertDontSee('No single repo-real follow-up is currently available.')
->getContent();
expect(spec352DashboardNodeCount(
$content,
"//a[contains(normalize-space(.), 'Open draft review')]",
))->toBe(1);
});
it('renders the no-urgent-action state without Graph or outbound HTTP during dashboard page render', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
mockSpec352FeatureDashboardPermissions();
bindFailHardGraphClient();
workspaceOverviewSeedQuietTenantTruth($environment);
$backupSet = workspaceOverviewSeedHealthyBackup($environment);
workspaceOverviewSeedRestoreHistory($environment, $backupSet, 'completed');
setAdminEnvironmentContext($environment);
assertNoOutboundHttp(function () use ($user, $environment): void {
$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(EnvironmentDashboard::getUrl(panel: 'admin', tenant: $environment))
->assertSuccessful()
->assertSeeText('No urgent operator action')
->assertSeeText('Review environment')
->assertSeeText('Readiness dimensions')
->assertSeeText('Readiness proof');
});
});

View File

@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
use App\Filament\Resources\EnvironmentReviewResource;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ReviewPack;
use App\Models\User;
use App\Services\EnvironmentReviews\EnvironmentReviewLifecycleService;
use App\Services\Intune\ManagedEnvironmentRequiredPermissionsViewModelBuilder;
use App\Support\EnvironmentDashboard\EnvironmentDashboardSummaryBuilder;
use App\Support\EnvironmentReviewStatus;
use App\Support\Links\RequiredPermissionsLinks;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\mock;
uses(RefreshDatabase::class);
function mockSpec352DashboardPermissions(array $overview = []): void
{
mock(ManagedEnvironmentRequiredPermissionsViewModelBuilder::class, function ($mock) use ($overview): void {
$mock->shouldReceive('build')->andReturn([
'overview' => array_replace_recursive([
'overall' => 'ready',
'counts' => [
'missing_application' => 0,
'missing_delegated' => 0,
],
'freshness' => [
'is_stale' => false,
'last_refreshed_at' => now()->toIso8601String(),
],
], $overview),
]);
});
}
/**
* @return array{
* review:\App\Models\EnvironmentReview,
* successor:\App\Models\EnvironmentReview|null,
* reviewPack:ReviewPack,
* }
*/
function spec352SeedBlockedReviewOutput(ManagedEnvironment $environment, User $user, bool $withSuccessorDraft = false): array
{
$snapshot = seedPartialEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0, operationRunCount: 0);
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
$review->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'published_at' => now()->subHour(),
'published_by_user_id' => (int) $user->getKey(),
'summary' => array_replace_recursive(is_array($review->summary) ? $review->summary : [], [
'publish_blockers' => ['Operator approval note is still missing.'],
]),
])->save();
$reviewPack = ReviewPack::factory()->ready()->create([
'managed_environment_id' => (int) $environment->getKey(),
'workspace_id' => (int) $environment->workspace_id,
'environment_review_id' => (int) $review->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'file_path' => 'review-packs/spec352-review-output.zip',
'file_disk' => 'exports',
'generated_at' => now()->subMinutes(10),
'options' => [
'include_pii' => false,
'include_operations' => true,
],
]);
$review->forceFill([
'current_export_review_pack_id' => (int) $reviewPack->getKey(),
])->save();
$successor = null;
if ($withSuccessorDraft) {
$successor = app(EnvironmentReviewLifecycleService::class)->createNextReview($review->fresh(), $user, $snapshot);
}
return [
'review' => $review->fresh(),
'successor' => $successor?->fresh(),
'reviewPack' => $reviewPack->fresh(),
];
}
it('prioritizes provider blockers over review output guidance when both are present', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
mockSpec352DashboardPermissions([
'overall' => 'blocked',
'counts' => [
'missing_application' => 1,
'missing_delegated' => 0,
],
]);
spec352SeedBlockedReviewOutput($environment, $user, withSuccessorDraft: true);
$summary = app(EnvironmentDashboardSummaryBuilder::class)
->build($environment, $user)
->toArray();
expect(data_get($summary, 'operatorGuidance.key'))->toBe('provider_readiness.required_permissions')
->and(data_get($summary, 'operatorGuidance.title'))->toBe(__('localization.dashboard.overview.operator_guidance_provider_blocked_title'))
->and(data_get($summary, 'operatorGuidance.actionLabel'))->toBe('Review permissions')
->and(data_get($summary, 'operatorGuidance.actionUrl'))->toBe(RequiredPermissionsLinks::requiredPermissions($environment))
->and(data_get($summary, 'readinessDecision.actionLabel'))->toBe('Review permissions')
->and(data_get($summary, 'readinessDecision.helperText'))->toBeNull();
});
it('surfaces review output guidance with open draft review when provider blockers are absent', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
mockSpec352DashboardPermissions();
$reviewState = spec352SeedBlockedReviewOutput($environment, $user, withSuccessorDraft: true);
$successor = $reviewState['successor'];
expect($successor)->not->toBeNull();
$summary = app(EnvironmentDashboardSummaryBuilder::class)
->build($environment, $user)
->toArray();
expect(data_get($summary, 'operatorGuidance.key'))->toBe('review_output.publication_blocked')
->and(data_get($summary, 'operatorGuidance.title'))->toBe(__('localization.review.draft_review_exists'))
->and(data_get($summary, 'operatorGuidance.actionLabel'))->toBe('Open draft review')
->and(data_get($summary, 'operatorGuidance.actionUrl'))->toContain('/environment-reviews/'.($successor?->getKey()))
->and(data_get($summary, 'readinessDecision.helperText'))->toBeNull()
->and(collect(data_get($summary, 'operatorGuidance.secondaryActions', []))->pluck('actionLabel')->intersect([
'Inspect review blockers',
'Open evidence basis',
'Open customer workspace',
])->isNotEmpty())->toBeTrue();
});
it('falls back to repo-backed operations attention when higher-priority guidance is absent', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
mockSpec352DashboardPermissions();
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()->subMinutes(5),
]);
$summary = app(EnvironmentDashboardSummaryBuilder::class)
->build($environment, $user)
->toArray();
expect(data_get($summary, 'operatorGuidance.key'))->toBe('recommended_action.operations_requiring_attention')
->and(data_get($summary, 'operatorGuidance.title'))->toBe(__('localization.dashboard.overview.operator_guidance_operations_title'))
->and(data_get($summary, 'operatorGuidance.actionLabel'))->toBe('Review operations')
->and(data_get($summary, 'operatorGuidance.actionUrl'))->toBe(OperationRunLinks::index(
$environment,
activeTab: OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
problemClass: OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
));
});
it('renders a calm no-urgent-action fallback when no dominant case exists', function (): void {
[$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
mockSpec352DashboardPermissions();
workspaceOverviewSeedQuietTenantTruth($environment);
$backupSet = workspaceOverviewSeedHealthyBackup($environment);
workspaceOverviewSeedRestoreHistory($environment, $backupSet, 'completed');
$summary = app(EnvironmentDashboardSummaryBuilder::class)
->build($environment, $user)
->toArray();
expect(data_get($summary, 'recommendedActions'))->toBe([])
->and(data_get($summary, 'operatorGuidance.key'))->toBe('environment.no_urgent_action')
->and(data_get($summary, 'operatorGuidance.title'))->toBe(__('localization.dashboard.overview.operator_guidance_no_urgent_title'))
->and(data_get($summary, 'operatorGuidance.actionLabel'))->toBe(__('localization.dashboard.overview.action_review_environment'))
->and(data_get($summary, 'operatorGuidance.actionUrl'))->toBe(EnvironmentReviewResource::environmentScopedUrl('index', tenant: $environment))
->and(data_get($summary, 'readinessDecision.helperText'))->toBeNull();
});

View File

@ -8,41 +8,79 @@ # UI-002 Environment Dashboard
| Archetype | Overview / Dashboard |
| Design depth | Strategic Surface |
| Repo truth | repo-verified |
| Screenshot | `../screenshots/desktop/ui-002-environment-dashboard.png` |
| Browser status | Reached with local Spec 180 smoke fixture. |
| Screenshot | `../../../specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/spec352-environment-dashboard-provider-blocker.png` |
| Browser status | Re-verified on 2026-06-04 with Spec 352 browser-smoke states for provider blocker, review-output follow-up, and no-urgent-action. |
## First Five Seconds
The page makes the selected environment visible and shows backup/recovery posture. It is clear that the operator is inside an environment, but several posture, verification, backup, and recovery messages compete for what should happen first.
The page now answers the first operator question immediately. The selected environment is visible, the dominant blocker or calm state is named explicitly, and the next safe navigation step is isolated into one primary action.
## Productization Review
- Decision-first: strong but dense.
- Evidence-first: good evidence/posture signals, but proof hierarchy needs simplification.
- Decision-first: strong.
- Evidence-first: good. Proof remains visible, but it no longer competes with the first decision.
- Context: workspace + environment route is explicit.
- Customer/auditor safety: operator-facing; customer-safe language should be checked before reuse.
- Customer/auditor safety: operator-facing. Review-output language is reused from the customer-review guidance path without introducing dashboard-local mutation semantics.
- Diagnostics: verification and health details should stay progressive.
## Information Inventory
Default content includes environment name, backup health, recovery readiness, operations, verification/reporting widgets, and navigation to environment-owned resources. Status signals include stale/non-healthy backup posture and recovery evidence concerns.
Default content now starts with one derived operator-guidance case:
- status
- title
- reason
- impact
- one primary navigation CTA
- compact secondary links when repo-real
Secondary surfaces remain visible below and beside the decision card:
- readiness dimensions
- readiness proof
- recommended next actions
- supporting signals
- collapsed diagnostics
The existing dashboard cards and proof rails remain repo-real, but they are clearly subordinate to the top guidance block.
## Dangerous Actions
Likely downstream actions include backup, restore, permission checks, provider fixes, and baseline compare. Target design must show mutation scope before action: TenantPilot only, Microsoft tenant, or simulation only.
The dashboard remains navigation-first in this slice. It does not execute publish, refresh, verification, restore, or provider mutations directly. Dangerous scope stays owned by the linked surfaces.
## Scores
| IA | Density | User Clarity | Sellability | Disclosure | Hierarchy | DS Fit | A11y | Responsive | Components | UX Writing | Perf |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 3 | 3 | 4 | 4 | 3 | 3 | 4 | 3 | 3 | 4 | 4 | 4 |
| 4 | 4 | 5 | 5 | 4 | 5 | 4 | 4 | 4 | 4 | 5 | 4 |
## Top Issues
1. Too many domain signals appear with similar weight.
2. Backup truth and recovery evidence need clearer separation.
3. Dangerous downstream actions need impact/confirmation/evidence review in target mockup.
1. Calm states can still show secondary proof gaps such as review freshness or evidence absence; this is acceptable repo truth, but it requires disciplined copy so the page does not feel contradictory.
2. Provider, review-output, and recovery guidance now share a common top slot, so future slices must keep the action-label language aligned.
3. The recommended-actions section is now intentionally subordinate; future additions must keep it from reintroducing a second primary rail.
## Guidance Hierarchy
Implemented in Spec 352:
1. provider blocker / required permissions
2. review-output resolution guidance
3. operation attention
4. findings / overdue findings
5. risk exceptions
6. recovery posture
7. continue review
8. calm no-urgent-action fallback
Review-output reuse is navigation-first and pulls from the existing `ReviewPackOutputResolutionAdapter`. Successor drafts no longer hide the published review-output state when the dashboard decides what should happen first.
## Screenshot Set
- `../../../specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/spec352-environment-dashboard-provider-blocker.png`
- `../../../specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/spec352-environment-dashboard-review-output.png`
- `../../../specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/spec352-environment-dashboard-no-urgent-action.png`
## Target Direction
P0 individual target mockup. The page should become the environment command surface: one primary posture decision, secondary domain drilldowns, and diagnostics behind deliberate disclosure.
Implemented by Spec 352 as a bounded follow-up: the Environment Dashboard is now the environment command surface with one dominant operator recommendation, preserved proof rails, and no new dashboard-local mutation workflow.

View File

@ -0,0 +1,264 @@
# Spec 352 Browser Flow Audit — Environment Dashboard Operator Guidance
## Executive Summary
- Overall readiness: nearly ready
- Main flow result: the dashboard consistently surfaces one dominant operator-guidance case, the primary dashboard CTAs route to the expected environment-scoped destinations, Provider Blocker outranks Review Output when both conditions are present, and the No-Urgent-Action state reads as intentional rather than broken.
- Top issues:
- `P2` Blocked states still carry avoidable action weight. The same primary CTA appears both in the page header and inside the guidance card, while the demoted recommended-actions rail remains visible below with additional buttons. The dominant case is still understandable, but the page is not yet as quiet as the spec intent.
- `P2` The helper line `No single repo-real follow-up is currently available.` appears beneath guidance cards that already expose a concrete primary CTA, and in the review-output case also expose secondary links. That copy weakens operator confidence.
- Recommendation: patch before close
## Repo State
- Branch: `352-environment-dashboard-operator-guidance-consolidation`
- Safety snapshot captured before audit artifact generation.
- Dirty tracked files:
- `apps/platform/app/Filament/Pages/EnvironmentDashboard.php`
- `apps/platform/app/Filament/Widgets/Dashboard/EnvironmentDashboardOverview.php`
- `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummary.php`
- `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php`
- `apps/platform/lang/de/localization.php`
- `apps/platform/lang/en/localization.php`
- `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php`
- `apps/platform/tests/Feature/Filament/Spec330EnvironmentDashboardBaselineCompareProductizationTest.php`
- `docs/ui-ux-enterprise-audit/page-reports/ui-002-environment-dashboard.md`
- Untracked files before this audit:
- `apps/platform/tests/Browser/Spec352EnvironmentDashboardGuidanceSmokeTest.php`
- `apps/platform/tests/Feature/Filament/Spec352EnvironmentDashboardGuidanceTest.php`
- `apps/platform/tests/Unit/EnvironmentDashboard/`
- `specs/352-environment-dashboard-operator-guidance-consolidation/`
- `git diff --stat` at snapshot time: `9 files changed, 707 insertions(+), 65 deletions(-)`
- Is Spec 352 active / uncommitted? yes
- Are only Spec-352 files changed? no
- Browser flow ran against the current working tree? yes
- Tests executed? no
## Browser Environment
- Base URL: `http://localhost`
- Login path used for local-only audit: `/admin/local/smoke-login`
- Audit user: `smoke-requester+352@tenantpilot.local`
- Workspace used: `Spec 352 Guidance Browser Audit` (`slug: spec-352-guidance-browser-audit`, `id: 33`)
- Environments used:
- `Spec 352 Audit Provider Blocker` (`slug: spec-352-audit-provider-blocker`, `id: 51`)
- `Spec 352 Audit Review Output` (`slug: spec-352-audit-review-output`, `id: 52`)
- `Spec 352 Audit No Urgent Action` (`slug: spec-352-audit-no-urgent`, `id: 53`)
- Scenarios tested:
- Provider blocker outranks review output
- Review output guidance on dashboard
- No urgent action
- Secondary action hierarchy
- Link and scope correctness
- Responsive / mobile-ish
## Read-only Repo Verification
- `operatorGuidance` is produced in `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php` via `operatorGuidance()`. `repo-verifiziert`
- Provider Blocker priority is enforced before review-output guidance by calling `providerOperatorGuidance()` before `reviewOutputOperatorGuidance()`. `repo-verifiziert`
- Review Output guidance is reused from the shared resolution-guidance model through `reviewOutputResolutionCaseForDashboard()`, `ReviewPackOutputResolutionAdapter::fromGuidance()`, and `buildOperatorGuidanceFromCase()`. `repo-verifiziert`
- No-Urgent-Action is produced by `noUrgentOperatorGuidance()` when no provider blocker, review-output case, or recommended action wins priority. `repo-verifiziert`
- Dashboard actions are rendered as navigation links. The top guidance CTA, secondary actions, and header action are URL-based. `ResolutionAction::fromArray()` additionally downgrades unsafe executable actions away from the dashboard context. `repo-verifiziert`
- No direct mutating action was introduced on the dashboard itself. `repo-verifiziert`
- The old recommended-actions rail was consciously demoted in `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php` by removing the chosen guidance action from the rail and rendering the remaining actions in a lower section under `Recommended next actions`. `repo-verifiziert`
## Scenario Results
### Provider blocker
- Scenario: Provider blocker outranks review output
- URL: `http://localhost/admin/workspaces/spec-352-guidance-browser-audit/environments/spec-352-audit-provider-blocker`
- Environment: `Spec 352 Audit Provider Blocker`
- Visible guidance title: `Provider readiness blocks evidence refresh`
- Guidance severity/status: `Blocked`
- Primary reason: `1 application permission(s) are still missing.`
- Impact: `Provider-backed inventory, verification, and reporting flows stay blocked until consent is restored.`
- Primary action: `Review permissions`
- Secondary actions: none inside the guidance card
- Old recommended rail visible? yes
- Does it compete? mildly. The blocker still dominates, but lower cards and extra buttons remain visible in the same scroll segment. `browser-verifiziert`
- Clicked action: `Review permissions`
- Target URL: `http://localhost/admin/workspaces/spec-352-guidance-browser-audit/environments/spec-352-audit-provider-blocker/required-permissions`
- Expected target: Required Permissions
- Actual target: Required Permissions
- Scope preserved? yes
- Workspace preserved? yes
- Environment preserved? yes
- Any 404/500/login redirect? no in the executed retest
- Back path clear? yes
- Console errors? no visible warn/error logs
- Network/server errors? no visible failure surfaced in the executed flow; no dedicated network trace captured
- Screenshot: `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/browser-flow-audit/01-provider-blocker-guidance.png`
- Finding: Provider readiness clearly dominates. Review-output guidance was seeded in the paired test pattern and does not surface as a second main decision here, matching the intended priority. `browser-verifiziert` + `repo-verifiziert`
- Severity: pass with `P2` polish on action density
### Review output guidance
- Scenario: Review output guidance on dashboard
- URL: `http://localhost/admin/workspaces/spec-352-guidance-browser-audit/environments/spec-352-audit-review-output`
- Environment: `Spec 352 Audit Review Output`
- Visible guidance title: `Draft review exists`
- Guidance severity/status: `Blocked`
- Primary reason: `A successor draft review already exists for this released output and is ready for publication. Open the draft review to publish the next governed outcome.`
- Impact: `The next review cycle is already in progress. Open the draft review and publish it when you are ready to replace the prior released review.`
- Primary action: `Open draft review`
- Secondary actions:
- `Inspect review blockers`
- `Open evidence basis`
- `Open operation proof`
- Old recommended rail visible? yes
- Does it compete? partially. The dominant CTA is still obvious, but the page shows a duplicated top-right primary CTA, three inline secondary links, and further rail buttons below. `browser-verifiziert`
- Clicked action: `Open draft review`
- Target URL: `http://localhost/admin/workspaces/33/environments/spec-352-audit-review-output/environment-reviews/31`
- Expected target: Draft review detail
- Actual target: Draft review detail
- Scope preserved? yes
- Workspace preserved? yes
- Environment preserved? yes
- Any 404/500/login redirect? no
- Back path clear? yes
- Console errors? no visible warn/error logs
- Network/server errors? no visible failure surfaced in the executed flow; no dedicated network trace captured
- Screenshot: `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/browser-flow-audit/03-review-output-guidance.png`
- Finding: Review-output guidance is correctly sourced from the shared resolution-guidance stack, and the browser flow exercised the `Open draft review` path successfully. This audit did not browser-exercise the alternative `Published with limitations` copy variant. `browser-verifiziert` for the successor-draft path, `repo-verifiziert` for the adapter reuse
- Severity: pass with `P2` polish on hierarchy density
### No urgent action
- Scenario: No urgent action
- URL: `http://localhost/admin/workspaces/spec-352-guidance-browser-audit/environments/spec-352-audit-no-urgent`
- Environment: `Spec 352 Audit No Urgent Action`
- Visible guidance title: `No urgent operator action`
- Guidance severity/status: `Calm`
- Primary reason: `No immediate environment blocker is visible.`
- Impact: `Current findings, permissions, recovery posture, and recent operations do not show an urgent follow-up path.`
- Primary action: `Review environment`
- Secondary actions: none inside the guidance card
- Old recommended rail visible? yes
- Does it compete? no. The rail collapses into a quiet informational state rather than another action stack. `browser-verifiziert`
- Clicked action: `Review environment`
- Target URL: `http://localhost/admin/workspaces/33/environments/spec-352-audit-no-urgent/environment-reviews`
- Expected target: Environment review index
- Actual target: Environment review index
- Scope preserved? yes
- Workspace preserved? yes
- Environment preserved? yes
- Any 404/500/login redirect? no
- Back path clear? yes
- Console errors? no visible warn/error logs
- Network/server errors? no visible failure surfaced in the executed flow; no dedicated network trace captured
- Screenshot: `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/browser-flow-audit/05-no-urgent-action.png`
- Finding: The calm state reads as intentional, not empty. Supporting signals remain usable, and the page still offers a neutral next step. `browser-verifiziert`
- Severity: pass
### Secondary action hierarchy
- Scenario: Secondary actions hierarchy
- URL: `http://localhost/admin/workspaces/spec-352-guidance-browser-audit/environments/spec-352-audit-review-output`
- Environment: `Spec 352 Audit Review Output`
- Visible guidance title: `Draft review exists`
- Guidance severity/status: `Blocked`
- Primary reason: same as review-output scenario
- Impact: same as review-output scenario
- Primary action: `Open draft review`
- Secondary actions: three text links below the primary button, plus lower recommended-action cards
- Old recommended rail visible? yes
- Does it compete? yes, mildly. The operator can still identify the next step within five seconds, but the visual system still presents more action affordances than necessary for a decision-first surface. `browser-verifiziert`
- Clicked action: none for this hierarchy-only observation
- Target URL: n/a
- Scope preserved? n/a
- Console errors? no visible warn/error logs
- Network/server errors? no visible failure surfaced in the executed flow; no dedicated network trace captured
- Screenshot: `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/browser-flow-audit/06-secondary-actions-hierarchy.png`
- Finding: The old recommended rail is demoted, but not yet fully quiet. The page still feels busier than the single-case operator-start goal. `browser-verifiziert` + `repo-verifiziert`
- Severity: `P2`
### Link/scope correctness
- Provider blocker:
- From URL: `http://localhost/admin/workspaces/spec-352-guidance-browser-audit/environments/spec-352-audit-provider-blocker`
- Clicked label: `Review permissions`
- Target URL: `http://localhost/admin/workspaces/spec-352-guidance-browser-audit/environments/spec-352-audit-provider-blocker/required-permissions`
- Expected target: Required Permissions
- Actual target: Required Permissions
- Workspace preserved? yes
- Environment preserved? yes
- Any 404/500/login redirect? no
- Back path clear? yes
- Classification: `browser-verifiziert`
- Review output:
- From URL: `http://localhost/admin/workspaces/spec-352-guidance-browser-audit/environments/spec-352-audit-review-output`
- Clicked label: `Open draft review`
- Target URL: `http://localhost/admin/workspaces/33/environments/spec-352-audit-review-output/environment-reviews/31`
- Expected target: draft review detail
- Actual target: draft review detail
- Workspace preserved? yes
- Environment preserved? yes
- Any 404/500/login redirect? no
- Back path clear? yes
- Classification: `browser-verifiziert`
- No urgent action:
- From URL: `http://localhost/admin/workspaces/spec-352-guidance-browser-audit/environments/spec-352-audit-no-urgent`
- Clicked label: `Review environment`
- Target URL: `http://localhost/admin/workspaces/33/environments/spec-352-audit-no-urgent/environment-reviews`
- Expected target: environment review index
- Actual target: environment review index
- Workspace preserved? yes
- Environment preserved? yes
- Any 404/500/login redirect? no
- Back path clear? yes
- Classification: `browser-verifiziert`
### Responsive check
- Scenarios checked:
- Provider blocker at narrow width
- Review output at narrow width
- Screenshots:
- `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/browser-flow-audit/07-mobile-provider-blocker.png`
- `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/browser-flow-audit/08-mobile-review-output.png`
- Result: the guidance card remains readable, the primary CTA stays visible, and the stacked layout avoids obvious horizontal overflow. `browser-verifiziert`
- Residual concern: the same duplicated CTA + extra lower actions create more scroll weight on small screens than strictly necessary. `browser-verifiziert`
- Severity: `P2`
## Screenshot Index
| Scenario | Screenshot | Notes |
| --- | --- | --- |
| Provider blocker guidance | `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/browser-flow-audit/01-provider-blocker-guidance.png` | Dominant blocker card |
| Provider blocker target | `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/browser-flow-audit/02-provider-blocker-target.png` | Required permissions target |
| Review output guidance | `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/browser-flow-audit/03-review-output-guidance.png` | Shared review-output guidance surfaced on dashboard |
| Review output target | `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/browser-flow-audit/04-review-output-target.png` | Draft review target |
| No urgent action | `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/browser-flow-audit/05-no-urgent-action.png` | Calm fallback |
| Secondary actions hierarchy | `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/browser-flow-audit/06-secondary-actions-hierarchy.png` | Action-density assessment |
| Mobile provider blocker | `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/browser-flow-audit/07-mobile-provider-blocker.png` | Narrow-width stack |
| Mobile review output | `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/browser-flow-audit/08-mobile-review-output.png` | Narrow-width stack |
## Findings
### P0 Blockers
- None observed in the executed browser flows.
### P1 High
- None observed in the executed browser flows.
### P2 Medium
- Blocked states still show more action weight than the spec intent suggests. The duplicated primary CTA and still-visible lower recommended-action buttons dilute the otherwise clear dominant case.
- The helper line `No single repo-real follow-up is currently available.` conflicts with the presence of a concrete CTA, and in the review-output state conflicts with visible secondary links as well.
### P3 Polish
- Review and calm targets use a numeric workspace segment (`/workspaces/33/...`) rather than the slug route used by the dashboard URL. Scope still resolves correctly, so this is consistency polish rather than a functional defect.
## Productization Assessment
- Decision-first quality: mostly good. The operator can identify the top case quickly in all tested states. `browser-verifiziert`
- Next-action clarity: good on destination correctness, slightly noisier than ideal on action count. `browser-verifiziert`
- Priority correctness: Provider Blocker outranks Review Output in code and in the exercised blocker scenario. `repo-verifiziert` + `browser-verifiziert`
- Scope correctness: workspace and environment scope held across all executed primary CTA paths. `browser-verifiziert`
- UI density: improved versus a flat recommended-actions rail, but still not fully quiet in blocked states. `browser-verifiziert`
- Resolve vs dashboard overview balance: healthy overall. The dashboard points to deeper operational surfaces instead of trying to resolve issues inline. `browser-verifiziert` + `repo-verifiziert`
## Recommended Fix Scope
- Must fix before close:
- Reduce action density in blocked states so the dashboard presents one obvious next step and quieter supporting actions.
- Replace or remove the contradictory helper copy under the guidance CTA.
- Can defer:
- Route-format consistency between slug-based dashboard URLs and numeric workspace target URLs.
- Should not change:
- Provider-over-review priority order
- Navigation-only dashboard action model
- Calm-state fallback structure
- Reuse of shared review-output resolution guidance
## Final Recommendation
- Close Spec 352? not yet
- Patch before close? yes
- Next suggested spec? none implied by this audit; the remaining work is a focused polish patch on hierarchy and helper copy, not a new feature spec

View File

@ -0,0 +1,47 @@
# Requirements Checklist: Spec 352 - Environment Dashboard Operator Guidance Consolidation
**Purpose**: Validate that Spec 352 is bounded, repo-based, constitution-aligned, and ready for a later implementation loop.
**Created**: 2026-06-04
**Feature**: `specs/352-environment-dashboard-operator-guidance-consolidation/spec.md`
## Candidate Selection And Guardrail
- [x] CHK001 The package names the direct user-provided Spec 352 draft as the candidate source and records that this is a manual promotion rather than an automatic `next-best-prep` selection from `docs/product/spec-candidates.md`.
- [x] CHK002 Spec 330 is treated as implemented historical context only and is not reopened or normalized.
- [x] CHK003 Specs 338, 350, and 351 are treated as dependency context only and are not rewritten or converted back into preparation-only wording.
- [x] CHK003A Spec 346 is treated as adjacent governance-owner dependency context only and is neither reopened nor closed by this prep package.
- [x] CHK004 The prep explicitly records that Spec 351 has repo-real action semantics but still carries residual browser-audit observations that stay out of scope for this dashboard follow-up.
- [x] CHK005 The scope is narrowed to the existing Environment Dashboard only; Baseline Compare, Governance Inbox, provider redesign, backup/restore follow-through, portal, PDF, PSA, and AI remain deferred.
## Repo Truth And Architecture
- [x] CHK006 The spec and plan anchor the work to `EnvironmentDashboard`, `EnvironmentDashboardSummaryBuilder`, `EnvironmentDashboardSummary`, and the existing dashboard Blade view.
- [x] CHK007 The artifacts record that the current dashboard already has `recommendedActions`, `readinessDecision`, readiness dimensions, proof panels, and supporting signals.
- [x] CHK008 The repo-truth map documents the current `recommendedActions()` ranking and the absence of an explicit dashboard `operatorGuidance` contract.
- [x] CHK009 The contract note keeps any new guidance payload derived-only and request-scoped; no new persistence is introduced.
- [x] CHK010 The plan forbids a generic new dashboard/workflow/provider framework and allows only a bounded page-local helper if necessary.
## UI/Productization Coverage
- [x] CHK011 UI Surface Impact is explicit and limited to the existing Environment Dashboard surface.
- [x] CHK012 UI/Productization Coverage reuses the existing `UI-002` Environment Dashboard page report and does not invent a new route taxonomy.
- [x] CHK013 The spec requires one dominant primary action, subordinate secondary links, and a calm `No urgent operator action` fallback.
- [x] CHK014 The spec and plan preserve current readiness dimensions, proof items, supporting signals, and collapsed diagnostics as secondary context.
## Testing And Validation
- [x] CHK015 Planned tests cover deterministic guidance selection, provider-priority behavior, review-output reuse, environment scope, and calm no-action rendering.
- [x] CHK016 Validation commands explicitly rerun Environment Dashboard plus Spec 330, Spec 350, Spec 351, and `ResolutionGuidance` regressions.
- [x] CHK017 The artifacts include `pint --dirty` and `git diff --check` as final validation steps.
## Readiness Gate
- [x] CHK018 Candidate Selection Gate passes: the candidate is direct user input, not already specced as `352`, and is scoped as a narrow follow-up over current repo truth.
- [x] CHK019 Spec Readiness Gate passes: `spec.md`, `plan.md`, `tasks.md`, `repo-truth-map.md`, and a bounded contract note exist and are aligned.
- [x] CHK020 No blocking product question remains; the remaining work is bounded implementation and later verification of the exact dominant-case behavior per fixture.
- [x] CHK021 No application implementation has been performed in this preparation step.
## Notes
- The package is intentionally framed as a Spec 330 follow-up rather than a fresh dashboard productization effort.
- The package is intentionally navigation-first and must not use the dashboard to hide unresolved owner-surface behavior or to execute new high-impact actions directly.

View File

@ -0,0 +1,111 @@
# Environment Dashboard Operator Guidance Contract
Status: implemented
Spec: `specs/352-environment-dashboard-operator-guidance-consolidation/spec.md`
Date: 2026-06-04
## Purpose
Define the narrowest derived payload needed for the Environment Dashboard top guidance area.
This contract is:
- derived-only
- request-scoped
- environment-scoped
- navigation-first
This contract is not:
- persisted truth
- a generic workflow engine
- a provider framework
- a replacement for current owner-surface action safety
## Implemented Payload Shape
```php
[
'key' => 'review_output.publication_blocked',
'tone' => 'warning',
'status' => 'Blocked',
'title' => 'Draft review exists',
'reason' => 'Open the draft review to refresh inputs before publication.',
'impact' => 'The latest released review output should not be treated as customer-ready until the draft is resolved.',
'actionLabel' => 'Open draft review',
'actionUrl' => '...',
'actionDisabled' => false,
'secondaryActions' => [
[
'key' => 'resolve_review_blockers',
'actionLabel' => 'Inspect review blockers',
'actionUrl' => '...',
'actionDisabled' => false,
'helperText' => null,
],
[
'key' => 'open_evidence_basis',
'actionLabel' => 'Open evidence basis',
'actionUrl' => '...',
'actionDisabled' => false,
'helperText' => null,
],
],
'source' => [
'type' => 'review_output_resolution',
'key' => 'review_output.publication_blocked',
],
'helperText' => null,
]
```
## Selection Rules
Use the current dashboard ranking as the baseline source of truth, then apply only bounded normalization where current repo truth clearly supports it.
Preferred order:
1. provider blocker / required permissions
2. stable review-output resolution case
3. operation attention
4. current dashboard findings / overdue findings
5. risk exceptions
6. recovery posture
7. continue review
8. calm no-action fallback
## Safety Rules
- The dashboard guidance block should prefer navigation over direct mutation.
- If a linked target is not repo-real or not authorized, degrade to a truthful unavailable or detail-review fallback.
- The dashboard must not invent review, provider, governance, or backup workflows that do not already exist on owner surfaces.
- The dashboard must not introduce a second equal-weight primary CTA outside the selected `primaryAction`.
In the implemented runtime this means:
- review-output `ResolutionAction`s are flattened into navigation-only `actionLabel` / `actionUrl` fields
- executable review actions such as `createNextReview`, `refreshReview`, or `publishReview` are not mounted from the dashboard
- source-owned actions remain available only on the linked owner surfaces
## Known Current Inputs
- current `recommendedActions`
- current `governanceStatus`
- current `readinessCards`
- current `activeOperationSummary`
- optional review-output `ResolutionCase`
- latest relevant review-output review selected by `latestReviewOutputReview()`
## Known Current Outputs To Preserve
- readiness dimensions
- readiness proof panel
- supporting signals
- collapsed diagnostics
- environment-scoped header follow-up mirror
## Implementation Notes
- The dashboard continues to keep `recommendedActions` for secondary follow-up, but the top card no longer mirrors `recommendedActions[0]`.
- `reviewOutputOperatorGuidance()` consumes `ReviewPackOutputResolutionAdapter` and decorates successor-draft cases with the same `Draft review exists` copy already used by `CustomerReviewWorkspace`.
- The Blade view removes any secondary recommended action whose label and URL match the dominant guidance CTA.

View File

@ -0,0 +1,346 @@
# Implementation Plan: Spec 352 - Environment Dashboard Operator Guidance Consolidation
**Branch**: `352-environment-dashboard-operator-guidance-consolidation` | **Date**: 2026-06-04 | **Spec**: `specs/352-environment-dashboard-operator-guidance-consolidation/spec.md`
**Input**: Direct user-provided Spec 352 draft plus repo truth from the implemented Spec 330 dashboard runtime and current review-output resolution-guidance surfaces.
## Summary
Add one bounded dashboard-level operator guidance case over the existing Environment Dashboard runtime so the first read becomes:
1. what needs attention first
2. why it matters
3. what the safest next action is
4. where to go next
This follow-up must:
- reuse the current Spec 330 dashboard layout and data model
- optionally consume the current review-output resolution contract from Specs 350/351
- preserve current proof panels, readiness dimensions, and secondary drilldowns
- keep direct mutation ownership in the existing source surfaces
This follow-up must not:
- rebuild the dashboard
- reopen Baseline Compare
- introduce a workflow engine or persistence
- roll out new provider/governance/backup adapter families
- hide residual Spec 351 browser follow-up under dashboard-local logic
## Technical Context
**Language/Version**: PHP 8.4.15, Laravel 12.52.x
**Primary Dependencies**: Filament 5.2.x, Livewire 4.1.x, Pest 4, Tailwind CSS 4
**Storage**: PostgreSQL; no schema change expected
**Testing**: Pest Unit + Feature/Livewire + one bounded Browser smoke
**Validation Lanes**: fast-feedback + confidence + browser
**Target Platform**: `apps/platform` Laravel monolith, Sail-first locally
**Project Type**: server-rendered Filament web application
**Performance Goals**: keep render DB-only and bounded to current dashboard queries; no new remote calls during render; no new queue family
**Constraints**: no dashboard rebuild, no new persistence, no fake mutation CTA, no broad adapter rollout, no Graph calls during render, no cross-environment leakage, no duplicate primary action rails
**Scale/Scope**: one existing page, one summary-builder follow-up, one optional page-local selector/payload, one page-report update, one repo-truth map, one contract note
## UI / Surface Guardrail Plan
- **Guardrail scope**: changed strategic operator-facing surface
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**:
- `/admin/workspaces/{workspace}/environments/{environment}`
- `App\Filament\Pages\EnvironmentDashboard`
- `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php`
- current Environment Dashboard header action mirror
- **No-impact class, if applicable**: N/A
- **Native vs custom classification summary**: native Filament page plus existing custom Blade composition
- **Shared-family relevance**: dashboard signals, action links, status messaging, proof links, header actions
- **State layers in scope**: page and derived summary payload
- **Audience modes in scope**: operator-MSP, manager, support where already authorized
- **Decision/diagnostic/raw hierarchy plan**: one guidance case first, proof second, diagnostics third
- **Raw/support gating plan**: keep current diagnostics disclosure collapsed; do not expand raw/support visibility
- **One-primary-action / duplicate-truth control**: the new guidance case owns the dominant CTA; any existing ranked action list must not restate the same decision as a competing primary block
- **Handling modes by drift class or surface**: review-mandatory
- **Repository-signal treatment**: review-mandatory because a strategic first-screen dashboard surface is changing
- **Special surface test profiles**: `monitoring-state-page` + `global-context-shell`
- **Required tests or manual smoke**: Unit + Feature + one bounded Browser smoke
- **Exception path and spread control**: if a generic cross-surface guidance framework appears necessary, stop and move that concern to a follow-up spec instead of widening the dashboard slice
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
- **UI/Productization coverage decision**: update the existing `UI-002` page report only
- **Coverage artifacts to update**: `docs/ui-ux-enterprise-audit/page-reports/ui-002-environment-dashboard.md`
- **No-impact rationale**: N/A
- **Navigation / Filament provider-panel handling**: no route or provider-panel change
- **Screenshot or page-report need**: yes; this is a strategic first-screen hierarchy change
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes
- **Systems touched**:
- `App\Support\EnvironmentDashboard\EnvironmentDashboardSummaryBuilder`
- `App\Support\EnvironmentDashboard\EnvironmentDashboardSummary`
- `App\Filament\Pages\EnvironmentDashboard`
- current Environment Dashboard Blade view
- `App\Support\ResolutionGuidance\ResolutionCase`
- `App\Support\ResolutionGuidance\ResolutionAction`
- `App\Support\ResolutionGuidance\Adapters\ReviewPackOutputResolutionAdapter`
- `App\Support\OperationRunLinks`
- **Shared abstractions reused**:
- current dashboard `recommendedActions()` and `readinessDecision()` derivation
- current dashboard proof and action helper methods
- Spec 350/351 review-output resolution contract
- current operation-proof link helper
- **New abstraction introduced? why?**: maybe one page-local selector or payload normalizer; it is justified only if the current summary-builder arrays cannot express one explicit dominant guidance case without duplicating logic
- **Why the existing abstraction was sufficient or insufficient**: current dashboard ranking is sufficient to detect important actions, but insufficient to expose one traceable top guidance case or to integrate review-output resolution semantics cleanly
- **Bounded deviation / spread control**: any new helper stays under `App\Support\EnvironmentDashboard\` and must not become a generic workflow or provider framework
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: yes, deep-link semantics only
- **Central contract reused**: `App\Support\OperationRunLinks`
- **Delegated UX behaviors**: unchanged; the dashboard can link to operation proof/detail, but it must not start or reshape run lifecycle behavior
- **Surface-owned behavior kept local**: selection and ranking of when operation proof becomes the dominant or secondary follow-up
- **Queued DB-notification policy**: unchanged
- **Terminal notification path**: unchanged
- **Exception path**: none
## Provider Boundary & Portability Fit
- **Shared provider/platform boundary touched?**: yes
- **Provider-owned seams**: required permissions, provider readiness detail, verification-owned follow-up routes
- **Platform-core seams**: environment guidance payload, dashboard ranking, operator-facing top guidance vocabulary
- **Neutral platform terms / contracts preserved**: environment, provider, required permissions, evidence, operation proof, review output, operator guidance
- **Retained provider-specific semantics and why**: provider-specific permission or verification language remains only where current provider-owned surfaces already require it
- **Bounded extraction or follow-up path**: none in this slice; any deeper provider productization stays a follow-up spec
## Current Repo Truth Summary
- Spec 330 already implemented the current dashboard contract:
- main readiness decision card
- readiness dimensions
- readiness proof panel
- supporting signals
- ranked recommended actions
- collapsed diagnostics
- `EnvironmentDashboardSummaryBuilder::recommendedActions()` currently ranks page-local candidates and returns the top three. Current observed candidate order is:
- required permissions
- delegated permissions
- high-severity findings
- operations requiring attention
- overdue findings
- risk exceptions
- recovery posture
- continue review
- `EnvironmentDashboardSummaryBuilder::readinessDecision()` currently mirrors the first ranked recommended action for reason, impact, and next-action text rather than using a dedicated dashboard guidance contract.
- `EnvironmentDashboard` header actions currently mirror the first recommended action and certain readiness cards, again without an explicit `operator_guidance` payload.
- `ReviewPackOutputResolutionAdapter` exists and already carries bounded review-output guidance, but the dashboard does not consume it today.
- `docs/ui-ux-enterprise-audit/page-reports/ui-002-environment-dashboard.md` still calls for one stronger primary posture decision and calmer proof hierarchy.
- Governance follow-up remains owned by the current Governance Inbox/runtime from Spec 346; this slice may route into that surface but must not reopen or close it.
- Spec 351 provides repo-real review-output action semantics, but its browser audit still records residual P2 follow-up observations. The dashboard may reuse stable action semantics but must not depend on unresolved workflow polish.
## Constitution Check
*GATE: Must pass before implementation. This prep keeps the design inside the already-proven dashboard and guidance boundaries.*
- Inventory-first: satisfied; all dashboard truth remains derived from current observed/environment-owned records
- Read/write separation: satisfied; the dashboard remains navigation-first in this slice
- Graph contract path: satisfied; no new Graph work is introduced
- Deterministic capabilities: satisfied; linked destinations keep current capability and policy checks
- Workspace isolation: satisfied; the surface remains route-owned and environment-bound
- Tenant isolation: satisfied; no cross-environment or cross-workspace shortcut is introduced
- OperationRun observability: satisfied; only existing run links are reused
- Test governance: satisfied; narrow Unit + Feature + Browser proof is planned explicitly
- Proportionality / anti-bloat: satisfied if any new helper remains page-local, derived-only, and narrowly justified
- Shared pattern first: satisfied by reusing the current summary builder and existing resolution contract before adding anything new
- Provider boundary: satisfied if provider-specific vocabulary stays inside existing provider-owned follow-up surfaces
- UI/Productization coverage: satisfied via the explicit `UI-002` follow-up and page-report update
## Test Governance Check
- **Test purpose / classification by changed surface**: Unit for ranking/selection, Feature for summary/view integration and scope, Browser for first-screen hierarchy
- **Affected validation lanes**: fast-feedback, confidence, browser
- **Why this lane mix is the narrowest sufficient proof**: the change is deterministic presentation and routing logic over an existing strategic page, not a schema or infrastructure change
- **Narrowest proving command(s)**:
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Unit/EnvironmentDashboard/Spec352EnvironmentDashboardGuidanceSelectionTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Filament/Spec352EnvironmentDashboardGuidanceTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec352EnvironmentDashboardGuidanceSmokeTest.php --compact`
- **Fixture / helper / factory / seed / context cost risks**: keep existing dashboard/review-output/provider fixtures narrow; do not widen default support contexts
- **Expensive defaults or shared helper growth introduced?**: no
- **Heavy-family additions, promotions, or visibility changes**: one explicit browser smoke only
- **Surface-class relief / special coverage rule**: `monitoring-state-page` + `global-context-shell`
- **Closing validation and reviewer handoff**: re-run Environment Dashboard, Spec 330, Spec 350, Spec 351, and `ResolutionGuidance` filtered proofs; verify no-action, provider-priority, and environment-scope behavior
- **Budget / baseline / trend follow-up**: none expected
- **Review-stop questions**: did we introduce a second framework, a fake CTA, or duplicate primary decision copy?
- **Escalation path**: `document-in-feature` unless broader domain rewrites are uncovered, then `follow-up-spec`
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
- **Why no dedicated follow-up spec is needed**: this slice stays limited to the Environment Dashboard and reuses current owner surfaces
## Implementation Approach
### Phase 0 - Repo Truth Gate
1. Re-read `spec.md`, `plan.md`, `tasks.md`, `repo-truth-map.md`, `contracts/environment-dashboard-operator-guidance-contract.md`, and `checklists/requirements.md`.
2. Re-read historical context only:
- `specs/330-environment-dashboard-baseline-compare-productization/*`
- `specs/338-workspace-environment-resource-scope-contract/*`
- `specs/346-governance-inbox-final-operator-workflow/*`
- `specs/350-operator-resolution-guidance-framework-v1/*`
- `specs/351-review-output-resolve-actions-v1/*`
3. Re-verify the current runtime truth in:
- `apps/platform/app/Filament/Pages/EnvironmentDashboard.php`
- `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php`
- `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummary.php`
- `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php`
- `apps/platform/app/Support/ResolutionGuidance/Adapters/ReviewPackOutputResolutionAdapter.php`
- existing review, evidence, operation, and required-permissions link helper paths used by the dashboard
4. Keep `repo-truth-map.md` and the dashboard contract note current if runtime inspection changes the narrowest safe selection model.
5. Confirm no schema, package, env var, queue family, scheduler, storage, panel/provider, or global-search change is required.
### Phase 1 - Tests First
1. Add `apps/platform/tests/Unit/EnvironmentDashboard/Spec352EnvironmentDashboardGuidanceSelectionTest.php`.
2. Cover deterministic selection for:
- provider blocker outranks review-output follow-up when current repo truth supports both
- review-output follow-up appears when provider blockers are absent and a stable case exists
- operation attention or existing ranked dashboard signals become dominant when higher-order cases are absent
- calm no-action fallback
3. Add `apps/platform/tests/Feature/Filament/Spec352EnvironmentDashboardGuidanceTest.php`.
4. Cover:
- one dominant top guidance case renders
- environment-scoped primary and secondary links
- no fake mutation CTA in the guidance block
- current proof panels/cards remain visible
5. Add `apps/platform/tests/Browser/Spec352EnvironmentDashboardGuidanceSmokeTest.php`.
6. Prove at least:
- action-needed/provider-blocker state
- review-output-driven state if fixture support is available
- no-urgent-action state if fixture support is available
7. Reuse Spec 330, Spec 350, and Spec 351 filtered coverage rather than copying their full assertions. Re-run Spec 346 only if the final dashboard mapping changes governance-owned link or action-shape behavior.
### Phase 2 - Guidance Contract And Selector
1. Choose the narrowest implementation shape:
- prefer extending the current summary-builder payload with `operatorGuidance`
- add a dedicated selector/helper only if the builder methods become hard to review
2. Build one derived payload with:
- `key`
- `severity` / `tone`
- `status`
- `title`
- `reason`
- `impact`
- one `primaryAction`
- optional `secondaryActions`
- source refs / helper details where useful
3. Feed the selector from existing dashboard/runtime truth:
- current `recommendedActions`
- current `governanceStatus`
- current `readinessCards`
- current `activeOperationSummary`
- current review/review-pack environment state for optional review-output reuse
4. Keep the selector derived-only and request-scoped.
5. Do not create a generic cross-page framework if a page-local dashboard helper is enough.
### Phase 3 - Candidate Ranking And Review-Output Reuse
1. Use the current dashboard ranking as the baseline ordering model.
2. Add bounded priority normalization only where current repo truth clearly supports it:
- provider blockers / required permissions
- stable review-output resolution case
- operation attention or evidence/proof follow-up
- current findings/risk/recovery/dashboard candidates
- calm no-action fallback
3. If a review-output case is consumed:
- only consume it when the target review/action is already repo-real and environment-scoped
- do not invent a dashboard-local review workflow
- do not depend on unresolved Spec 351 browser polish
4. Keep provider-specific detail inside the existing provider-owned routes and current dashboard helper text.
### Phase 4 - Dashboard Integration
1. Update `EnvironmentDashboardSummary` and `EnvironmentDashboardSummaryBuilder` so the top card can render from `operatorGuidance` rather than mirroring `recommendedActions[0]` directly.
2. Update `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php` to render:
- one dominant guidance block
- subordinate secondary links
- a productized no-action state
3. Keep current readiness dimensions, proof panel, supporting signals, and diagnostics disclosure intact as secondary context.
4. Update `App\Filament\Pages\EnvironmentDashboard` header actions so the mirrored primary action follows the new guidance payload instead of the raw ranked-action array.
5. Avoid duplicating the same reason/impact/next-action content in several equal-weight blocks.
### Phase 5 - Copy, Audit, And Browser Proof
1. Update only the required localization keys in:
- `apps/platform/lang/en/localization.php`
- `apps/platform/lang/de/localization.php`
2. Keep operator wording practical:
- `Needs attention`
- `No urgent operator action`
- `Recommended next action`
- existing source-owned labels where already repo-real
3. Update `docs/ui-ux-enterprise-audit/page-reports/ui-002-environment-dashboard.md` for the new top-guidance hierarchy.
4. Capture screenshots under `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/`.
### Phase 6 - Validation And Close-Out
1. Run focused Spec 352 Unit, Feature, and Browser coverage.
2. Re-run filtered regressions for:
- `EnvironmentDashboard`
- `Spec330`
- `Spec350`
- `Spec351`
- `ResolutionGuidance`
3. Run `pint --dirty` and `git diff --check`.
4. Report any broader failure honestly without widening scope.
## Project Structure
### Documentation (this feature)
```text
specs/352-environment-dashboard-operator-guidance-consolidation/
├── spec.md
├── plan.md
├── tasks.md
├── repo-truth-map.md
├── contracts/
│ └── environment-dashboard-operator-guidance-contract.md
└── checklists/
└── requirements.md
```
### Likely Runtime Surfaces
```text
apps/platform/app/Filament/Pages/EnvironmentDashboard.php
apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummary.php
apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php
apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php
apps/platform/lang/en/localization.php
apps/platform/lang/de/localization.php
apps/platform/tests/Unit/EnvironmentDashboard/Spec352EnvironmentDashboardGuidanceSelectionTest.php
apps/platform/tests/Feature/Filament/Spec352EnvironmentDashboardGuidanceTest.php
apps/platform/tests/Browser/Spec352EnvironmentDashboardGuidanceSmokeTest.php
docs/ui-ux-enterprise-audit/page-reports/ui-002-environment-dashboard.md
```
ios/ or android/
└── [platform-specific structure: feature modules, UI flows, platform tests]
```
**Structure Decision**: [Document the selected structure and reference the real
directories captured above]
## Complexity Tracking
> **Fill when Constitution Check has violations that must be justified OR when BLOAT-001 is triggered by new persistence, abstractions, states, or semantic frameworks.**
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
## Proportionality Review
> **Fill when the feature introduces a new enum/status family, DTO/presenter/envelope, persisted entity/table/artifact, interface/contract/registry/resolver, taxonomy/classification system, or cross-domain UI framework.**
- **Current operator problem**: [What present-day workflow or risk requires this?]
- **Existing structure is insufficient because**: [Why the current code cannot serve safely or clearly]
- **Narrowest correct implementation**: [Why this shape is the smallest viable one]
- **Ownership cost created**: [Maintenance, testing, cognitive load, migration, or review burden]
- **Alternative intentionally rejected**: [Simpler option and why it failed]
- **Release truth**: [Current-release truth or future-release preparation]

View File

@ -0,0 +1,140 @@
# Repo Truth Map: Spec 352 - Environment Dashboard Operator Guidance Consolidation
Status: implemented
Branch: `352-environment-dashboard-operator-guidance-consolidation`
Date: 2026-06-04
## Scope Boundary
Spec 352 is a narrow follow-up over the implemented Environment Dashboard runtime from Spec 330.
It may:
- refine the top guidance contract on the existing Environment Dashboard
- reuse current dashboard summary data
- optionally consume current review-output resolution guidance where the action target is already repo-real
It may not:
- rebuild the page
- reopen Baseline Compare
- create new persistence
- add new provider/governance/backup adapter families
- hide unresolved Spec 351 browser observations inside dashboard-local semantics
## Current Primary Surface
| Area | Current repo truth | Notes |
|---|---|---|
| Page class | `apps/platform/app/Filament/Pages/EnvironmentDashboard.php` | Existing environment-owned route/page |
| Summary builder | `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php` | Current source for dashboard payload |
| Summary payload | `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummary.php` | Existing derived request-scoped summary |
| Main view | `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php` | Existing decision-first layout from Spec 330 |
| Page report | `docs/ui-ux-enterprise-audit/page-reports/ui-002-environment-dashboard.md` | Still calls for a stronger single primary posture decision |
## Implemented Runtime Follow-up
- `EnvironmentDashboardSummary` now carries a derived `operatorGuidance` payload.
- `EnvironmentDashboardSummaryBuilder` now derives `operatorGuidance` before `readinessDecision`, so the top card and the page header mirror the same dominant case.
- `latestReviewOutputReview()` was added so a newer successor draft does not hide the published or superseded review-output state that should still drive dashboard follow-up.
- Review-output reuse is bounded to `ReviewPackOutputResolutionAdapter` plus dashboard-local mapping that strips direct mutation execution from the dashboard surface.
- The main widget view now renders secondary review-output links as compact subordinate links and filters the old recommended-actions rail so it does not restate the dominant decision.
- `EnvironmentDashboard` header actions now mirror `operatorGuidance` instead of the old first-ranked-action shortcut.
## Current Dashboard Guidance Truth
### What already exists
- `readinessDecision`
- `recommendedActions`
- `governanceStatus`
- `readinessCards`
- `readinessDimensions`
- `readinessProofPanel`
- header action mirroring inside `EnvironmentDashboard`
### How the current top decision is now chosen
- `EnvironmentDashboardSummaryBuilder::recommendedActions()` still assembles current repo-backed candidate actions for secondary follow-up.
- `EnvironmentDashboardSummaryBuilder::operatorGuidance()` now selects the dominant case in this order:
1. provider blockers from recommended actions
2. stable review-output resolution case from the latest relevant output review
3. operations attention
4. high-severity findings / overdue findings
5. risk exceptions
6. recovery posture
7. continue review
8. calm no-action fallback
- `EnvironmentDashboardSummaryBuilder::readinessDecision()` now mirrors `operatorGuidance`, not `recommendedActions[0]`.
- `EnvironmentDashboard` header actions mirror `operatorGuidance` and its secondary actions.
### Current observed candidate order in `recommendedActions()`
| Priority | Key | Source class / truth |
|---:|---|---|
| 10 | `required_permissions` | required-permissions overview / provider blocker |
| 20 | `delegated_permissions` | required-permissions overview / lower-severity provider blocker |
| 30 | `high_severity_findings` | governance aggregate / active findings |
| 35 | `operations_requiring_attention` | current environment `OperationRun` attention query |
| 40 | `overdue_findings` | governance aggregate / overdue findings |
| 50 | `risk_exceptions` | exception stats + governance counters |
| 60 | `recovery_posture` | backup/recovery posture helpers |
| 80 | `continue_review` | latest review / latest review pack state |
This ordering is now repo-real current behavior.
## Current Secondary Proof Truth
| Payload area | Current repo truth | Notes |
|---|---|---|
| `governanceStatus` | baseline compare, evidence coverage, review freshness, provider permissions, backup posture | Secondary proof/status signals |
| `readinessCards` | current review, risk exceptions, provider health, customer-safe output | Secondary summary cards |
| `readinessProofPanel` | selected proof-path items + optional review-pack + operation proof | Secondary proof panel |
| `supportingSignals` | additional readiness signals | Secondary table-style support surface |
| `diagnosticsDisclosure` | collapsed diagnostics summary | Raw/support detail remains collapsed |
## Review-Output Reuse Truth
| Area | Current repo truth | Status for Spec 352 |
|---|---|---|
| `ReviewPackOutputResolutionAdapter` | exists under `App\Support\ResolutionGuidance\Adapters\ReviewPackOutputResolutionAdapter` | available for bounded reuse |
| `ResolutionCase` / `ResolutionAction` | exist from Spec 350 | available for bounded reuse |
| Review-output resolve actions | repo-real through Spec 351 | available for bounded reuse |
| Dashboard consumer of resolution guidance | none found | gap this spec may close narrowly |
| Dashboard consumer of resolution guidance | `EnvironmentDashboardSummaryBuilder::reviewOutputOperatorGuidance()` | implemented narrow reuse |
## Dependency Guardrail Notes
| Dependency | Current repo truth | Guardrail for Spec 352 |
|---|---|---|
| Spec 330 | implemented; `repo-truth-map.md` says `Status: implemented` and tasks are checked | treat as completed baseline; do not reopen layout or Baseline Compare scope |
| Spec 338 | route/scope contract foundation | keep environment-owned route truth unchanged |
| Spec 346 | Governance Inbox operator workflow remains the adjacent governance-owner follow-up surface and is explicitly not closed | reuse linked destination truth only; do not absorb governance workflow ownership into the dashboard |
| Spec 350 | checked implementation tasks and shared guidance contract | reuse if helpful; do not broaden |
| Spec 351 | checked implementation tasks and committed runtime follow-through, but residual browser-audit notes remain | reuse only stable action semantics; do not hide unresolved follow-up |
## Implemented Narrow Slice
The current dashboard now has:
1. one explicit environment guidance contract
2. a stable merge point between dashboard ranking and review-output resolution guidance
3. a productized calm `No urgent operator action` state
4. a deduplicated relationship between the top guidance area, ranked actions, and mirrored header actions
Preserved:
- current route ownership
- current proof panels and secondary cards
- current source-owned action safety
- current collapsed diagnostics model
## Evidence
- Unit: `tests/Unit/EnvironmentDashboard/Spec352EnvironmentDashboardGuidanceSelectionTest.php`
- Feature: `tests/Feature/Filament/Spec352EnvironmentDashboardGuidanceTest.php`
- Browser: `tests/Browser/Spec352EnvironmentDashboardGuidanceSmokeTest.php`
- Screenshots:
- `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/spec352-environment-dashboard-provider-blocker.png`
- `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/spec352-environment-dashboard-review-output.png`
- `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/spec352-environment-dashboard-no-urgent-action.png`

View File

@ -0,0 +1,406 @@
# Feature Specification: Spec 352 - Environment Dashboard Operator Guidance Consolidation
**Feature Branch**: `352-environment-dashboard-operator-guidance-consolidation`
**Created**: 2026-06-04
**Status**: Draft
**Type**: Platform productization / environment dashboard follow-up / operator guidance consolidation
**Depends on**: Specs 330, 338, 346, 350, 351
**Runtime posture**: Bounded follow-up over the implemented Environment Dashboard runtime. No new persistence, workflow engine, provider/governance/backup adapter rollout, or dashboard rebuild.
**Input**: Direct user-provided Spec 352 draft plus repo truth from the implemented Spec 330 dashboard runtime and current review-output resolution-guidance surfaces.
## Dependencies And Repo-Truth Adjustments
This spec is a follow-up over already repo-real dashboard and guidance foundations:
- Spec 330 implemented the Environment Dashboard decision-first scaffold and the current readiness/recommended-action layout.
- Spec 338 hardened workspace/environment route ownership and remains the environment-scope baseline.
- Spec 346 productized the Governance Inbox operator workflow and remains the adjacent governance-owner follow-up surface when the dashboard routes into governance attention.
- Spec 350 introduced the bounded `ResolutionCase` / `ResolutionAction` contract and the review-output-first adapter path.
- Spec 351 added real review-output resolve actions and made the review surfaces more action-aware.
Repo-truth adjustments against the user draft:
- The current dashboard runtime is already productized through:
- `apps/platform/app/Filament/Pages/EnvironmentDashboard.php`
- `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php`
- `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php`
- `EnvironmentDashboardSummaryBuilder` already derives:
- `recommendedActions`
- `readinessDecision`
- `governanceStatus`
- `readinessCards`
- `readinessDimensions`
- `readinessProofPanel`
- The current dashboard already shows one main readiness question, but its top decision area still depends on the first entry in `recommendedActions` rather than on an explicit, traceable dashboard guidance contract.
- `ReviewPackOutputResolutionAdapter` and the Spec 350/351 resolution contract exist, but the Environment Dashboard does not currently consume them.
- Spec 330 is treated as implemented historical truth. Spec 352 must reuse its layout and payload foundations instead of reopening the dashboard productization from scratch.
- Spec 351 has checked implementation tasks and repo-real runtime follow-through, but its browser audit still records residual P2 observations. Spec 352 may consume already repo-real review-output action semantics, but it must not pretend those residual observations are resolved or reopen Spec 351 under a dashboard label.
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
- **Problem**: The Environment Dashboard is already the environment command surface, but its primary recommendation still comes from ad hoc ranked actions and equal-weight signals instead of one explicit operator guidance case that says what matters first and why.
- **Today's failure**: Operators can still see provider permissions, findings, review freshness, backup posture, evidence, and operations at once, then reconstruct for themselves which domain owns the next safe step. The current top decision area mirrors the first recommended action, but not a stable guidance contract.
- **User-visible improvement**: The Environment Dashboard shows one dominant operator guidance case with status, reason, impact, one primary action, and clearly subordinate secondary links, while preserving the current proof panels and secondary cards.
- **Smallest enterprise-capable version**: Reuse the existing Environment Dashboard route, summary builder, and decision-first layout; add one derived dashboard guidance selector/payload; optionally consume the existing review-output `ResolutionCase`; keep all detailed work in the linked owner surfaces.
- **Explicit non-goals**: No dashboard rebuild, no baseline-compare rewrite, no new provider-readiness engine, no Governance Inbox rewrite, no new backup/restore adapter, no workflow engine, no persistence, no portal, no PDF renderer, no PSA/ITSM handoff, no AI suggestions or execution.
- **Permanent complexity imported**: One bounded dashboard-level derived guidance payload or selector, focused Unit/Feature/Browser tests, one repo-truth map, one contract note, and one page-report update. No new table, no new persisted truth, no new cross-surface framework beyond a page-local follow-up abstraction if it is strictly necessary.
- **Why now**: Spec 330 made the dashboard decision-first, and Specs 350/351 made review-output guidance explicit. The remaining gap is now visible: the dashboard still ranks repo-backed actions locally instead of presenting one stable operator guidance case over the current summary data.
- **Why not local**: Copy-only tweaks would leave the dashboard driven by page-local action ranking and would not solve the missing contract between current dashboard signals and the newer resolution-guidance behavior.
- **Approval class**: Workflow Compression.
- **Red flags triggered**: cross-surface reuse and new support-layer naming. Defense: the slice is limited to one existing page, reuses existing summary data, forbids new persistence/framework rollout, and keeps all mutation ownership in existing source surfaces.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
- **Decision**: approve with strict follow-up-only and no-dashboard-rebuild constraints.
## Candidate Source And Completed-Spec Guardrail
- **Candidate source**:
- direct user-provided Spec 352 draft
- `docs/ui-ux-enterprise-audit/page-reports/ui-002-environment-dashboard.md`
- current repo truth in `EnvironmentDashboardSummaryBuilder`, `EnvironmentDashboard`, and the implemented Spec 330 dashboard runtime
- **Queue posture**:
- `docs/product/spec-candidates.md` currently says no safe automatic `next-best-prep` target remains in the active queue
- Spec 352 is therefore a manual promotion, not an auto-selected candidate from the active queue
- **Completed-spec guardrail result**:
- no `specs/352-*` package or branch existed before this preparation run
- Spec 330 contains implemented runtime truth and checked implementation tasks; treat it as completed historical context only
- Spec 346 remains an adjacent governance-owner workflow dependency context and is not reopened, normalized, or closed by this prep package
- Specs 338, 350, and 351 already contain checked implementation evidence or historical implementation signals and must not be normalized or rewritten
- Spec 351 additionally carries residual browser-audit follow-up notes; Spec 352 may depend on the repo-real action semantics but must not hide or absorb those unresolved observations
- **Close alternatives deferred**:
- provider readiness / provider connections follow-up
- finding exceptions / accepted-risk accountability follow-up
- review register or baseline-compare follow-up
- broader cross-domain dashboard indicator cleanup
- **Smallest viable implementation slice**: existing Environment Dashboard only: derive one `operatorGuidance` case over current summary data, optionally merge in current review-output resolution guidance, keep the current proof panels/cards as secondary context, and update the page report plus focused tests/browser smoke.
## Spec Scope Fields *(mandatory)*
- **Scope**: environment-owned dashboard follow-up
- **Primary Routes**:
- `/admin/workspaces/{workspace}/environments/{environment}`
- existing linked source routes only: required permissions, evidence, environment review/detail, customer review workspace, operation proof/detail, findings, risk exceptions, and current environment-owned drilldowns when repo-backed
- **Data Ownership**:
- `ManagedEnvironment`, `ProviderConnection`, `ManagedEnvironmentPermission`, `EvidenceSnapshot`, `EnvironmentReview`, `ReviewPack`, `Finding`, `FindingException`, `OperationRun`, `BackupSet`, `RestoreRun`, and current dashboard aggregate helpers remain the source truth
- `EnvironmentDashboardSummary` and any new dashboard guidance payload stay derived-only and request-scoped
- no new table, model, enum, or persisted artifact is introduced
- **RBAC**:
- workspace membership plus entitled environment access remain required
- dashboard guidance visibility remains environment-scoped
- linked primary/secondary actions must keep their existing policy/capability rules
- non-member or out-of-scope environment access stays 404; member-but-missing-capability stays 403 on source-owned action destinations
For canonical-view specs: N/A. The primary surface is environment-owned, not workspace-canonical.
## 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
- [ ] Workspace/environment context presentation changed
## UI/Productization Coverage *(mandatory when UI Surface Impact is not "No UI surface impact")*
- **Route/page/surface**:
- `App\Filament\Pages\EnvironmentDashboard`
- `App\Support\EnvironmentDashboard\EnvironmentDashboardSummaryBuilder`
- `App\Support\EnvironmentDashboard\EnvironmentDashboardSummary`
- `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php`
- current Environment Dashboard header action hierarchy where it mirrors the dominant follow-up
- **Current or new page archetype**: existing environment-owned Overview / Dashboard surface `UI-002`
- **Design depth**: Strategic Surface
- **Repo-truth level**: repo-verified existing runtime
- **Existing pattern reused**:
- `docs/ui-ux-enterprise-audit/page-reports/ui-002-environment-dashboard.md`
- implemented Spec 330 dashboard decision card, readiness dimensions, proof panel, and secondary signal pattern
- current dashboard action and proof-link helpers
- **New pattern required**: one bounded dashboard guidance selector/payload over current summary data; no new global dashboard taxonomy
- **Screenshot required**: yes, under `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/`
- **Page audit required**: yes, update `docs/ui-ux-enterprise-audit/page-reports/ui-002-environment-dashboard.md`
- **Customer-safe review required**: no new customer-facing surface, but review-output wording reused on the dashboard must stay consistent with customer-safe review semantics
- **Dangerous-action review required**: no new dangerous action is expected; the dashboard should remain navigation-first and must not start new high-impact mutations directly
- **Coverage files updated or explicitly not needed**:
- [ ] `docs/ui-ux-enterprise-audit/route-inventory.md`
- [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`
- [x] `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`
- **No-impact rationale when applicable**: N/A
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)*
- **Cross-cutting feature?**: yes
- **Interaction class(es)**: dashboard signals/cards, status messaging, action links, header actions, proof links, and review-output follow-up reuse
- **Systems touched**:
- `App\Support\EnvironmentDashboard\EnvironmentDashboardSummaryBuilder`
- `App\Support\EnvironmentDashboard\EnvironmentDashboardSummary`
- `App\Filament\Pages\EnvironmentDashboard`
- current Environment Dashboard Blade view
- `App\Support\ResolutionGuidance\ResolutionCase`
- `App\Support\ResolutionGuidance\ResolutionAction`
- `App\Support\ResolutionGuidance\Adapters\ReviewPackOutputResolutionAdapter`
- `App\Support\OperationRunLinks`
- current required-permissions, findings, risk, review, and proof-link action builders already used by the dashboard summary
- **Existing pattern(s) to extend**:
- current `recommendedActions()` ranking in `EnvironmentDashboardSummaryBuilder`
- current `readinessDecision()` top-card derivation
- existing review-output resolution adapter contract from Specs 350/351
- **Shared contract / presenter / builder / renderer to reuse**:
- `EnvironmentDashboardSummaryBuilder`
- `ReviewPackOutputResolutionAdapter`
- `ResolutionCase` / `ResolutionAction`
- `OperationRunLinks`
- existing dashboard action helper methods such as required-permissions, findings, operations, review, and backup posture links
- **Why the existing shared path is sufficient or insufficient**: the repo already has current dashboard ranking and a newer resolution-guidance contract, but the dashboard still lacks one explicit environment guidance envelope tying those sources together without reopening the page layout.
- **Allowed deviation and why**: one bounded helper such as `EnvironmentDashboardOperatorGuidanceSelector` or an equivalent summary-builder sub-method is allowed if it remains page-local, derived-only, and does not become a generic workflow framework.
- **Consistency impact**: title/reason/impact/primary-action structure, secondary-link hierarchy, scope-explicit deep links, and honest unavailable states must stay aligned with current review-output, provider, evidence, operation, and governance wording.
- **Review focus**: block any second dashboard framework, fake executable remediation, duplicated primary CTA rails, or regression of the Spec 330 decision-first structure.
## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)*
- **Touches OperationRun start/completion/link UX?**: yes, link semantics only
- **Shared OperationRun UX contract/layer reused**: `App\Support\OperationRunLinks`
- **Delegated start/completion UX behaviors**: unchanged; the dashboard may open existing operation proof/detail routes, but it does not start or change `OperationRun` lifecycle behavior
- **Local surface-owned behavior that remains**: candidate ranking, dominant guidance selection, and one-primary-action hierarchy
- **Queued DB-notification policy**: unchanged
- **Terminal notification path**: unchanged
- **Exception required?**: none
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
- **Shared provider/platform boundary touched?**: yes
- **Boundary classification**: mixed
- **Seams affected**: provider-readiness prioritization, required-permissions wording, and dashboard-level routing into provider-owned follow-up surfaces
- **Neutral platform terms preserved or introduced**: environment, provider, required permissions, evidence, operation proof, review output, operator guidance
- **Provider-specific semantics retained and why**: provider-owned permission or verification wording may remain only where the current required-permissions or provider connection surfaces already own that vocabulary
- **Why this does not deepen provider coupling accidentally**: the dashboard guidance layer stays derived and environment-owned, and it routes into existing provider-owned surfaces instead of creating new provider semantics in the dashboard core
- **Follow-up path**: deeper provider readiness productization remains a separate follow-up spec
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|---|---|---|---|---|---|---|
| Environment Dashboard top guidance area | yes | Native Filament page plus existing custom Blade composition | dashboard signals, action links, proof links | page, derived summary payload | no | Existing route only |
| Environment Dashboard header primary action mirror | yes | Native Filament action hierarchy | header actions, navigation-first follow-up | page, derived summary payload | no | Must stay subordinate to source-owned safety rules |
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
| 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 which environment issue deserves the first follow-up | one guidance case, reason, impact, one primary action, compact secondary links | readiness dimensions, proof panel, recent operations, supporting signals, linked owner surfaces | Primary because it is the environment command surface and already owns the first-read posture question | Follows the environment workflow before domain drilldown | Removes cross-card reconstruction and equal-weight signal scanning |
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
| 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 where already authorized | one guidance case, status, reason, impact, one primary action, compact secondary links | readiness dimensions, proof items, supporting signals, linked source detail | raw diagnostics and support tooling remain behind the existing disclosure/capability seams | yes | diagnostics stay collapsed; no new raw payload exposure is introduced | the top guidance tells the operator once what matters most; the rest of the page adds proof and route choices only |
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
| 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 workbench | open the dominant blocker or proof owner surface | explicit primary action in the guidance card | N/A | compact links inside the same guidance block and existing secondary sections | none introduced | `/admin/workspaces/{workspace}/environments/{environment}` | same route plus existing linked owner routes | active workspace + route-bound environment | Environment Dashboard | one dominant issue plus next step | none |
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
| 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 what to do first in this managed environment | environment command surface | What needs attention first, why does it matter, and where should I go next? | one dominant guidance case, status, reason, impact, one primary action, subordinate links | existing proof/detail/diagnostic paths only | dashboard posture, provider readiness, review freshness/output, evidence coverage, operations attention, governance state | navigation-first only in this slice | open required permissions, open draft/review output surface, open evidence, open operation proof, open findings/risk surface, or honest unavailable fallback | none introduced |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no
- **New persisted entity/table/artifact?**: no
- **New abstraction?**: yes, one bounded dashboard guidance selector/payload may be introduced if the current summary-builder methods are insufficient
- **New enum/state/reason family?**: no
- **New cross-domain UI framework/taxonomy?**: no
- **Current operator problem**: the current dashboard already exposes signals and ranked actions, but operators still reconstruct the top issue from page-local action ranking instead of seeing one explicit environment guidance case
- **Existing structure is insufficient because**: `recommendedActions` and `readinessDecision` are still separate arrays, and the dashboard does not currently consume the newer review-output resolution contract or a stable dashboard guidance envelope
- **Narrowest correct implementation**: add one derived guidance payload over current dashboard data and optional review-output resolution reuse, then drive the top guidance area and mirrored header action from that payload only
- **Ownership cost**: one local selector/presenter, focused tests, a repo-truth map, a small contract note, and one page-report update
- **Alternative intentionally rejected**: a broad dashboard redesign, a repo-wide guidance rollout, a provider or governance adapter expansion, or a new persisted dashboard state model
- **Release truth**: current-release runtime follow-up over implemented Environment Dashboard behavior
### Compatibility posture
This feature assumes a pre-production environment.
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec.
Canonical replacement is preferred over preservation.
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
- **Test purpose / classification**: Unit + Feature + Browser
- **Validation lane(s)**: fast-feedback + confidence + browser
- **Why this classification and these lanes are sufficient**: the core risk is deterministic guidance selection, environment-scoped link behavior, and first-screen action hierarchy; Unit tests prove ranking, Feature tests prove rendered/dashboard integration and scope, and one Browser smoke proves the visual hierarchy remains calm and honest
- **New or expanded test families**:
- `apps/platform/tests/Unit/EnvironmentDashboard/Spec352EnvironmentDashboardGuidanceSelectionTest.php`
- `apps/platform/tests/Feature/Filament/Spec352EnvironmentDashboardGuidanceTest.php`
- `apps/platform/tests/Browser/Spec352EnvironmentDashboardGuidanceSmokeTest.php`
- **Fixture / helper cost impact**: reuse existing Environment Dashboard, review-output, provider-permissions, and operations fixtures where available; do not widen default factories or browser setup
- **Heavy-family visibility / justification**: one explicit browser smoke is justified because this is a strategic first-screen operator surface
- **Special surface test profile**: `monitoring-state-page` + `global-context-shell`
- **Standard-native relief or required special coverage**: special coverage required because the feature changes one-primary-action hierarchy on a strategic dashboard surface
- **Reviewer handoff**: confirm no fake mutation CTA is added, no cross-environment link leakage exists, provider blockers outrank lower-priority cases where repo truth supports it, and the Spec 330 structure remains intact
- **Budget / baseline / trend impact**: low; one new bounded Unit family and one browser smoke file
- **Escalation needed**: `document-in-feature` if a bounded dashboard selector is needed; `follow-up-spec` only if implementation reveals that provider/governance/review-owner surfaces need broader rewrites
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
- **Planned validation commands**:
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Unit/EnvironmentDashboard/Spec352EnvironmentDashboardGuidanceSelectionTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Filament/Spec352EnvironmentDashboardGuidanceTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec352EnvironmentDashboardGuidanceSmokeTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=EnvironmentDashboard`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec330`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec350`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec351`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=ResolutionGuidance`
- `cd apps/platform && ./vendor/bin/sail artisan pint --dirty`
- `git diff --check`
## User Scenarios & Testing *(mandatory)*
### User Story 1 - See one dominant next step on the Environment Dashboard (Priority: P1)
As an MSP operator, I want the Environment Dashboard to show one dominant guidance case so I know what to do first without reconstructing the answer from multiple cards.
**Why this priority**: This is the core product value of the spec and the main remaining gap after Spec 330 and Specs 350/351.
**Independent Test**: Render the dashboard with competing repo-backed signals and assert that one guidance case wins, one primary action is shown, and supporting links remain clearly secondary.
**Acceptance Scenarios**:
1. **Given** an environment has a repo-backed dominant blocker, **When** the dashboard renders, **Then** the top guidance card shows one status, one reason, one impact, and one primary action.
2. **Given** secondary proof or follow-up links exist, **When** the top guidance renders, **Then** they appear as subordinate links and not as competing primary CTAs.
3. **Given** no repo-real primary action is available, **When** the dashboard renders, **Then** the top guidance shows an honest fallback such as review details or proof review instead of a fake executable action.
---
### User Story 2 - Route the operator to the right owner surface (Priority: P1)
As an MSP operator, I want dashboard guidance to route me into the correct owner surface so the dashboard stays calm and does not become a second governance inbox or troubleshooting console.
**Why this priority**: The dashboard is valuable only if it compresses the first decision and hands off cleanly to the owner workflow.
**Independent Test**: Seed provider, review-output, evidence, and operation-attention scenarios and assert that the primary/secondary links remain environment-scoped, repo-backed, and authorization-aware.
**Acceptance Scenarios**:
1. **Given** provider permissions block evidence or review freshness, **When** the dashboard renders, **Then** provider readiness outranks lower-priority review or evidence follow-up where the current repo truth supports that ordering.
2. **Given** the latest review output has a repo-real resolution case, **When** the dashboard renders, **Then** the dashboard may surface that case or its mapped action without inventing a new review workflow.
3. **Given** an action target is unauthorized or unavailable, **When** the dashboard renders, **Then** the action stays hidden, disabled by current convention, or degrades to an honest fallback without leaking protected details.
---
### User Story 3 - Show a calm no-action state without losing current proof surfaces (Priority: P2)
As an MSP operator, I want the dashboard to stay useful when no urgent action exists so the page does not feel broken or empty.
**Why this priority**: A sellable operator dashboard needs a truthful calm state, not only action-needed states.
**Independent Test**: Render an environment fixture with no dominant blocker and assert that the dashboard shows a productized no-urgent-action message while keeping the current cards and proof paths available.
**Acceptance Scenarios**:
1. **Given** no dominant guidance candidate exists, **When** the dashboard renders, **Then** the top guidance shows a calm `No urgent operator action` state and an honest next step such as reviewing the environment.
2. **Given** the no-action state is shown, **When** the rest of the page renders, **Then** readiness dimensions, proof items, and supporting cards remain visible and useful.
3. **Given** the page is in a calm state, **When** the operator scans the dashboard, **Then** the page does not promote several equal-weight actions or warnings above the top guidance.
## Functional Requirements
- **FR-352-001**: The Environment Dashboard MUST expose one explicit top-level operator guidance case.
- **FR-352-002**: The top guidance case MUST include status, reason, impact, one primary action, and optional secondary links.
- **FR-352-003**: The top guidance case MUST remain environment-scoped and must not depend on hidden shell context.
- **FR-352-004**: Guidance selection MUST derive only from repo-backed current dashboard signals and existing linked owner surfaces.
- **FR-352-005**: The implementation MUST reuse current `EnvironmentDashboardSummaryBuilder` inputs and existing proof/action helpers where possible.
- **FR-352-006**: The dashboard MAY consume a review-output `ResolutionCase` or mapped action from the current Spec 350/351 path when it is environment-scoped and repo-real.
- **FR-352-007**: Provider-readiness blockers SHOULD outrank lower-priority dashboard candidates when the current required-permissions/runtime truth supports that ordering.
- **FR-352-008**: Current dashboard `recommendedActions`, readiness dimensions, proof items, and supporting signals MUST remain available as secondary context unless explicitly superseded by a narrower improvement.
- **FR-352-009**: The top guidance area MUST keep exactly one dominant primary action.
- **FR-352-010**: Secondary links MUST not compete visually with the primary action.
- **FR-352-011**: If no primary action is safe or repo-real, the dashboard MUST render an honest fallback such as review details or proof review.
- **FR-352-012**: The dashboard MUST not introduce direct new mutations such as publish, refresh evidence, run verification, or restore from the top guidance area unless those actions are already source-owned, safe, and explicitly kept in-scope.
- **FR-352-013**: Existing high-impact or destructive actions MUST remain source-owned and continue to rely on their existing confirmation, authorization, audit, and test paths.
- **FR-352-014**: A productized `No urgent operator action` state MUST exist for environments without a dominant guidance candidate.
- **FR-352-015**: The implementation MUST not remove useful current dashboard proof cards, readiness dimensions, or secondary drilldowns.
- **FR-352-016**: Links generated by the guidance case MUST remain authorization-aware and environment-scoped.
- **FR-352-017**: The dashboard MUST not invent a new provider/governance/backup/review adapter family unless repo truth proves one is strictly necessary for this page-local follow-up.
- **FR-352-018**: Focused Unit, Feature, and Browser coverage MUST prove selection, rendering, scope, and no-action behavior.
## Non-Functional Requirements
- **NFR-352-001**: The page must remain DB-only during render; no Graph/provider API calls are allowed.
- **NFR-352-002**: No migrations, seeders, packages, env vars, queue families, scheduler changes, storage changes, or deployment asset changes are expected.
- **NFR-352-003**: Filament v5 and Livewire v4.0+ compliance MUST be preserved.
- **NFR-352-004**: Panel provider registration remains `apps/platform/bootstrap/providers.php`; no provider registration change is expected.
- **NFR-352-005**: No globally searchable resource is added or changed by this spec.
- **NFR-352-006**: Guidance selection must be deterministic and testable from the existing summary inputs.
- **NFR-352-007**: The dashboard must stay calm and decision-first; diagnostics and detailed proof remain secondary.
- **NFR-352-008**: Browser smoke screenshots must be captured under the Spec 352 artifacts path when generated.
## Success Criteria
- Operators can identify one clear first action from the Environment Dashboard without scanning multiple equal-weight cards.
- Provider, review-output, evidence, and operation-follow-up scenarios produce repo-backed dominant guidance or honest fallback behavior.
- A calm environment still shows a productized no-action state instead of an empty or noisy top area.
## Repo Truth / Data Requirements
- Current dashboard truth already includes:
- `recommendedActions`
- `readinessDecision`
- `governanceStatus`
- `readinessCards`
- `readinessDimensions`
- `readinessProofPanel`
- The current `recommendedActions()` ranking in `EnvironmentDashboardSummaryBuilder` already includes provider permissions, findings, operations attention, risk exceptions, recovery posture, and ongoing review follow-up; Spec 352 must document and tighten, not invent, the next-step model.
- No dedicated dashboard `operatorGuidance` or `dashboardGuidanceCase` payload exists today.
- `ReviewPackOutputResolutionAdapter` exists today, but the dashboard does not currently consume it.
- If a visible guidance candidate lacks a safe target, authorization path, or current-release truth, it MUST become unavailable or fall back honestly rather than being invented.
## Out Of Scope
- Rebuilding Environment Dashboard from scratch.
- Reopening Baseline Compare productization.
- Replacing Governance Inbox, Customer Review Workspace, Evidence Overview, or Required Permissions with dashboard-local workflow.
- New provider-readiness, backup/restore, or governance adapter families unless strict page-local proof makes one unavoidable.
- New persistence, workflow engines, customer portal, PDF/HTML renderer, PSA/ITSM handoff, or AI.
- New route architecture, navigation model, or shell/context rewrite.
## Risks
- **Risk 1 - Duplicate Spec 330 instead of following it up**: mitigate by reusing the existing dashboard layout and touching only the dominant guidance contract and its immediate rendering.
- **Risk 2 - Hide Spec 351 residual issues behind the dashboard**: mitigate by consuming only repo-real action semantics and documenting that unresolved Spec 351 browser observations remain out of scope.
- **Risk 3 - Turn the dashboard into a second queue or console**: mitigate by keeping one dominant action and routing into owner surfaces.
- **Risk 4 - Introduce fake remediation**: mitigate by navigation-first behavior and source-owned action safety rules only.
## Assumptions
- Spec 330 remains the canonical implemented dashboard foundation.
- The current review-output resolution contract from Specs 350/351 is stable enough to be consumed as dashboard input where the action target is already repo-real.
- Current required-permissions and operation-proof links remain the canonical owner paths for provider and operation follow-up.
## Acceptance Criteria
- [ ] Environment Dashboard shows one explicit top-level operator guidance case.
- [ ] One primary action is visually dominant.
- [ ] Secondary links are clearly subordinate.
- [ ] Guidance is derived from repo-backed dashboard/runtime truth only.
- [ ] Provider-readiness blockers outrank lower-priority candidates where repo truth supports the ordering.
- [ ] Review-output guidance can surface on the dashboard when a repo-real case/action exists.
- [ ] No fake direct mutation action is introduced in the dashboard guidance area.
- [ ] `No urgent operator action` renders as a calm productized state when no dominant case exists.
- [ ] Existing readiness dimensions, proof items, and supporting cards remain useful.
- [ ] The page stays environment-scoped and authorization-aware.
- [ ] No new persistence, workflow engine, or broad adapter rollout is introduced.
- [ ] Focused Unit, Feature, and Browser coverage is planned and sufficient for later implementation.

View File

@ -0,0 +1,125 @@
# Tasks: Spec 352 - Environment Dashboard Operator Guidance Consolidation
**Input**: `specs/352-environment-dashboard-operator-guidance-consolidation/spec.md`, `plan.md`, `repo-truth-map.md`, `contracts/environment-dashboard-operator-guidance-contract.md`, and `checklists/requirements.md`
**Tests**: Required. This is a strategic dashboard selection, hierarchy, and scope-safety change over an existing Environment Dashboard surface.
## Test Governance Checklist
- [x] Lane assignment is explicit and narrow: Unit for selection/ranking, Feature for dashboard integration and scope, Browser for first-screen hierarchy proof.
- [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 profiles (`monitoring-state-page` and `global-context-shell`) are explicit.
- [x] Any new helper remains derived-only and does not create hidden persistence or a workflow engine.
## Phase 1: Preparation And Repo Truth
**Purpose**: Keep the implementation bounded to the existing Environment Dashboard runtime and current resolution-guidance truth.
- [x] T001 Re-read `spec.md`, `plan.md`, `tasks.md`, `repo-truth-map.md`, `contracts/environment-dashboard-operator-guidance-contract.md`, and `checklists/requirements.md`.
- [x] T002 Re-read related historical context only: Specs 330, 338, 346, 350, and 351. Do not modify their artifacts.
- [x] T003 Re-verify the current Environment Dashboard runtime truth in `apps/platform/app/Filament/Pages/EnvironmentDashboard.php`, `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummary.php`, `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php`, and `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php`.
- [x] T004 Re-verify the current dashboard header-action mirror and follow-up hierarchy in `apps/platform/app/Filament/Pages/EnvironmentDashboard.php`.
- [x] T005 Re-verify the current review-output guidance truth in `apps/platform/app/Support/ResolutionGuidance/Adapters/ReviewPackOutputResolutionAdapter.php`, `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, and `apps/platform/app/Filament/Resources/EnvironmentReviewResource.php`.
- [x] T006 Confirm no migration, package, env var, queue family, scheduler, storage, deployment asset, panel/provider, or global-search change is required; confirm Filament v5 / Livewire v4.0+ and `apps/platform/bootstrap/providers.php` remain unchanged.
- [x] T007 Keep `repo-truth-map.md` and `contracts/environment-dashboard-operator-guidance-contract.md` current if runtime inspection reveals a narrower or broader safe selection model.
- [x] T008 Explicitly document whether residual Spec 351 browser observations affect dashboard reuse; if they do, keep dashboard mapping on stable repo-real navigation semantics only.
## Phase 2: Tests First
**Purpose**: Lock deterministic guidance selection, scope, and one-primary-action behavior before runtime changes.
- [x] T009 Add `apps/platform/tests/Unit/EnvironmentDashboard/Spec352EnvironmentDashboardGuidanceSelectionTest.php`.
- [x] T010 Add unit assertions that provider-readiness blockers outrank lower-priority review-output or dashboard follow-up when the current repo truth supports both candidates.
- [x] T011 Add unit assertions that review-output guidance can become dominant when provider blockers are absent and a stable environment-scoped case/action exists.
- [x] T012 Add unit assertions that existing dashboard signals such as operations attention or current ranked candidates can become dominant when higher-order cases are absent.
- [x] T013 Add unit assertions for the calm `No urgent operator action` fallback.
- [x] T014 Add `apps/platform/tests/Feature/Filament/Spec352EnvironmentDashboardGuidanceTest.php`.
- [x] T015 Add feature assertions that the dashboard renders one explicit top guidance case with one dominant primary action.
- [x] T016 Add feature assertions that primary and secondary links remain environment-scoped and authorization-aware.
- [x] T017 Add feature assertions that readiness dimensions, proof items, and supporting signals remain visible as secondary context.
- [x] T018 Add feature assertions that no fake direct mutation CTA is rendered in the top guidance area.
- [x] T019 Add `apps/platform/tests/Browser/Spec352EnvironmentDashboardGuidanceSmokeTest.php`.
- [x] T020 Browser Flow A: provider-blocker state; assert one dominant top guidance case, one primary CTA, subordinate secondary links, and collapsed diagnostics.
- [x] T021 Browser Flow B: review-output-driven state if fixture-supported; assert review follow-up is dominant only when provider blockers are absent and the action target is repo-real.
- [x] T022 Browser Flow C: calm no-action state if fixture-supported; assert a productized no-urgent-action message and preserved secondary proof surfaces.
## Phase 3: Guidance Contract And Selector
**Purpose**: Add the narrowest derived dashboard guidance payload over the current summary-builder truth.
- [x] T023 Update `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummary.php` only if needed to carry one derived `operatorGuidance` payload; do not add persisted state or public framework semantics.
- [x] T024 Update `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php` to derive one explicit dashboard guidance case from current dashboard/runtime truth.
- [ ] T025 Only if the summary builder becomes hard to review, add a bounded helper under `apps/platform/app/Support/EnvironmentDashboard/` for guidance selection; keep it page-local, derived-only, and request-scoped.
- [x] T026 Keep ranking grounded in current dashboard candidate priorities and add bounded precedence for provider blockers and stable review-output cases only where repo truth clearly supports it.
- [x] T027 Consume `ReviewPackOutputResolutionAdapter` only for stable environment-scoped dashboard reuse; do not invent open-draft discovery or a dashboard-local review workflow.
- [x] T028 Build the guidance payload with status, reason, impact, one primary action, subordinate secondary actions, source references where useful, and an honest no-action fallback.
- [x] T029 Do not introduce a new provider/governance/backup adapter family, a new persisted truth model, or a new enum/status family in this slice.
## Phase 4: Dashboard Integration
**Purpose**: Render the new guidance contract without reopening the Spec 330 dashboard productization work.
- [x] T030 Update `apps/platform/resources/views/filament/widgets/dashboard/environment-dashboard-overview.blade.php` so the top decision area renders from the new `operatorGuidance` payload.
- [x] T031 Preserve current readiness dimensions, readiness proof panel, supporting signals, and diagnostics disclosure as secondary context.
- [x] T032 Demote or deduplicate the current ranked `recommendedActions` section so it does not compete with the top guidance case.
- [x] T033 Update `apps/platform/app/Filament/Pages/EnvironmentDashboard.php` so the mirrored header primary/secondary follow-up actions use the new guidance payload rather than raw first-ranked action data.
- [x] T034 Keep the dashboard navigation-first in this slice; do not add direct new mutation execution in the top guidance block.
- [x] T035 Render a calm `No urgent operator action` state with an honest next step when no dominant case exists.
## Phase 5: Copy, Audit, And Browser Proof
**Purpose**: Align visible copy and coverage artifacts with the new guidance hierarchy.
- [x] T036 Update only the required dashboard guidance copy in `apps/platform/lang/en/localization.php`.
- [x] T037 Update matching dashboard guidance copy in `apps/platform/lang/de/localization.php`.
- [x] T038 Update `docs/ui-ux-enterprise-audit/page-reports/ui-002-environment-dashboard.md` for the new top-guidance hierarchy and preserved secondary proof model.
- [x] T039 Capture screenshots under `specs/352-environment-dashboard-operator-guidance-consolidation/artifacts/screenshots/`.
- [x] T040 Keep `repo-truth-map.md` and `contracts/environment-dashboard-operator-guidance-contract.md` aligned with the final bounded implementation shape.
## Phase 6: Validation
**Purpose**: Prove the guidance contract stays bounded and preserves current scope and trust rules.
- [x] T041 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Unit/EnvironmentDashboard/Spec352EnvironmentDashboardGuidanceSelectionTest.php --compact`.
- [x] T042 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Filament/Spec352EnvironmentDashboardGuidanceTest.php --compact`.
- [x] T043 Run `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec352EnvironmentDashboardGuidanceSmokeTest.php --compact`.
- [x] T044 Confirm the final implementation keeps the Environment Dashboard render path DB-local and does not introduce `GraphClientInterface` or provider API calls from `EnvironmentDashboard`, `EnvironmentDashboardSummaryBuilder`, or any newly touched guidance helper/view path.
- [x] T045 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=EnvironmentDashboard`.
- [x] T046 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec330`.
- [x] T047 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec350`.
- [x] T048 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec351`.
- [x] T049 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=ResolutionGuidance`.
- [x] T050 Governance-owned link or action-shape behavior did not change; `Spec346` rerun was not required for this slice.
- [x] T051 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`.
- [x] T052 Run `git diff --check`.
- [x] T053 Report any out-of-scope broader-suite or fixture gaps honestly if they remain outside this slice.
## Non-Goals Checklist
- [ ] NT001 Do not rebuild Environment Dashboard from scratch.
- [ ] NT002 Do not reopen Baseline Compare productization.
- [ ] NT003 Do not create a new persisted dashboard state, workflow engine, or adapter family.
- [ ] NT004 Do not hide residual Spec 351 browser observations by moving them into dashboard-local semantics.
- [ ] NT005 Do not add direct new mutation execution to the dashboard guidance area.
- [ ] NT006 Do not add portal, PDF/HTML, PSA, AI, queue, storage, or deployment follow-up work.
## Required Final Report Content
When implementation later completes, report:
- changed dashboard guidance behavior
- dominant-case selection model
- provider/review-output/no-action behavior
- environment-scope guarantees
- preserved secondary proof surfaces
- files changed
- repo-truth-map status
- contract note status
- tests run and results
- explicit render-path check result for no `GraphClientInterface` or provider API calls during dashboard render
- browser verification and screenshot path
- known gaps
- full-suite run/not run
- explicit no migrations/packages/env/queues/scheduler/storage/deployment-assets statement