Spec 326: productize customer review workspace
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m36s

This commit is contained in:
Ahmed Darrazi 2026-05-18 15:26:45 +02:00
parent 3eff4d8579
commit 562dfe049e
20 changed files with 2839 additions and 184 deletions

View File

@ -7,10 +7,12 @@
use App\Filament\Concerns\CleansAdminTenantQueryParameter;
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
use App\Filament\Resources\EnvironmentReviewResource;
use App\Filament\Resources\EvidenceSnapshotResource;
use App\Models\EnvironmentReview;
use App\Models\EvidenceSnapshot;
use App\Models\FindingException;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ReviewPack;
use App\Models\User;
use App\Models\Workspace;
@ -25,6 +27,7 @@
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
use App\Support\OperationRunLinks;
use App\Support\ReviewPackStatus;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
@ -59,6 +62,14 @@ class CustomerReviewWorkspace extends Page implements HasTable
public const string SOURCE_SURFACE = 'customer_review_workspace';
private const array ACCEPTED_RISK_FOLLOW_UP_STATES = [
'expiring_exception',
'expired_exception',
'revoked_exception',
'risk_accepted_without_valid_exception',
'pending_exception',
];
protected static bool $isDiscovered = false;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-text';
@ -280,12 +291,24 @@ public function latestReviewConsumptionPayload(): ?array
return null;
}
$review->loadMissing([
'currentExportReviewPack.operationRun',
'evidenceSnapshot.operationRun',
'operationRun',
]);
$publishedAt = $review->published_at ?? $review->generated_at ?? $review->created_at;
$packageAvailability = $this->governancePackageAvailability($tenant);
$downloadUrl = $this->reviewPackDownloadUrl($review, $tenant);
$reviewUrl = $this->latestReviewUrl($tenant);
$decision = $this->decisionSummaryForReview($review);
$acceptedRisks = $this->acceptedRisksForReview($review);
$hasAcceptedRiskFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review);
$evidencePath = $this->evidencePathForReview($review, $tenant, $packageAvailability, $downloadUrl, $decision, $acceptedRisks);
return [
'scope' => $this->reviewScopePayload($tenant),
'latest' => [
'review_label' => __('localization.review.released_review_for_environment', [
'environment' => $tenant->name,
@ -307,12 +330,610 @@ public function latestReviewConsumptionPayload(): ?array
'primary_action_icon' => $downloadUrl !== null
? 'heroicon-o-arrow-down-tray'
: 'heroicon-o-arrow-top-right-on-square',
'secondary_action_label' => $downloadUrl !== null ? __('localization.review.open_review') : null,
'secondary_action_url' => $downloadUrl !== null ? $reviewUrl : null,
'secondary_action_label' => $downloadUrl !== null
? ($hasAcceptedRiskFollowUp ? __('localization.review.download_review_pack') : __('localization.review.open_review'))
: null,
'secondary_action_url' => $downloadUrl !== null
? ($hasAcceptedRiskFollowUp ? $downloadUrl : $reviewUrl)
: null,
'secondary_action_icon' => $downloadUrl !== null && $hasAcceptedRiskFollowUp
? 'heroicon-o-arrow-down-tray'
: 'heroicon-o-arrow-top-right-on-square',
],
'decision' => $this->decisionSummaryForReview($review),
'accepted_risks' => $this->acceptedRisksForReview($review),
'readiness' => $this->reviewReadinessForTenant($tenant, $review, $packageAvailability, $downloadUrl, $reviewUrl),
'readiness_dimensions' => $this->readinessDimensionPayloads($tenant, $review, $packageAvailability),
'decision' => $decision,
'accepted_risks' => $acceptedRisks,
'accepted_risk_panel' => $this->acceptedRiskPanelForReview($review),
'evidence_basis' => $this->evidenceBasisForReview($review, $packageAvailability),
'evidence_path' => $evidencePath,
'aside_evidence_path' => $this->asideEvidencePath($evidencePath),
'review_pack_panel' => $this->reviewPackPanelForReview($review, $tenant, $packageAvailability, $downloadUrl),
'follow_ups' => $this->customerSafeFollowUpsForReview($decision),
'diagnostics' => $this->diagnosticsDisclosureForReview(),
'disclosure_rules' => $this->disclosureRuleRows(),
];
}
/**
* @return array{label:string,description:string,is_filtered:bool}
*/
private function reviewScopePayload(ManagedEnvironment $tenant): array
{
$filteredTenant = $this->filteredTenant();
if ($filteredTenant instanceof ManagedEnvironment) {
return [
'label' => __('localization.review.customer_workspace_scope_environment_filtered', [
'environment' => $filteredTenant->name,
]),
'description' => __('localization.review.customer_workspace_scope_environment_filtered_description'),
'is_filtered' => true,
];
}
return [
'label' => __('localization.review.customer_workspace_scope_workspace_wide'),
'description' => __('localization.review.customer_workspace_scope_workspace_wide_description', [
'environment' => $tenant->name,
]),
'is_filtered' => false,
];
}
/**
* @param array{state:string,label:string,description:string} $packageAvailability
* @return array{question:string,label:string,color:string,reason:string,impact:string,primary_action_label:string,primary_action_url:?string,primary_action_icon:string}
*/
private function reviewReadinessForTenant(
ManagedEnvironment $tenant,
EnvironmentReview $review,
array $packageAvailability,
?string $downloadUrl,
?string $reviewUrl,
): array {
$hasAcceptedRiskFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review);
$hasReadyPackage = $packageAvailability['state'] === 'available' && $downloadUrl !== null;
$hasCustomerSafeProof = $this->primaryControlSummary($tenant) !== null
&& $this->evidenceStatusState($tenant) === 'available';
$isReadyToShare = ! $hasAcceptedRiskFollowUp
&& ! $this->workspaceReviewNeedsAttention($tenant)
&& $hasReadyPackage;
$isShareableWithFollowUp = $hasAcceptedRiskFollowUp && $hasReadyPackage && $hasCustomerSafeProof;
return [
'question' => __('localization.review.is_review_ready_to_share'),
'label' => match (true) {
$isReadyToShare => __('localization.review.ready_to_share'),
$isShareableWithFollowUp => __('localization.review.shareable_with_follow_up'),
default => __('localization.review.follow_up_required_before_sharing'),
},
'color' => match (true) {
$isReadyToShare => 'success',
$isShareableWithFollowUp => 'warning',
default => $this->latestReviewStateColor($tenant),
},
'reason' => match (true) {
$isReadyToShare => __('localization.review.ready_to_share_reason'),
$isShareableWithFollowUp => __('localization.review.shareable_with_follow_up_reason'),
default => $this->customerSafeText(
$this->reviewOutcomeDescription($tenant) ?? $packageAvailability['description'],
__('localization.review.follow_up_required_before_sharing_reason'),
),
},
'impact' => match (true) {
$isReadyToShare => __('localization.review.ready_to_share_impact'),
$isShareableWithFollowUp => __('localization.review.shareable_with_follow_up_impact'),
default => __('localization.review.follow_up_required_before_sharing_impact'),
},
'primary_action_label' => $isShareableWithFollowUp
? __('localization.review.open_review')
: ($downloadUrl !== null ? __('localization.review.download_review_pack') : __('localization.review.open_latest_review')),
'primary_action_url' => $isShareableWithFollowUp
? ($reviewUrl ?? $downloadUrl)
: ($downloadUrl ?? $reviewUrl),
'primary_action_icon' => $isShareableWithFollowUp || $downloadUrl === null
? 'heroicon-o-arrow-top-right-on-square'
: 'heroicon-o-arrow-down-tray',
];
}
/**
* @param array{state:string,label:string,description:string} $packageAvailability
* @return list<array{title:string,label:string,color:string,description:string}>
*/
private function readinessDimensionPayloads(
ManagedEnvironment $tenant,
EnvironmentReview $review,
array $packageAvailability,
): array {
$acceptedRisk = $this->acceptedRiskDimensionForReview($review);
$evidenceState = $this->evidenceStatusState($tenant);
return [
[
'title' => __('localization.review.readiness'),
'label' => $this->latestReviewStateLabel($tenant),
'color' => $this->latestReviewStateColor($tenant),
'description' => $this->workspaceReviewNeedsAttention($tenant)
? __('localization.review.readiness_dimension_follow_up_description')
: __('localization.review.readiness_dimension_ready_description'),
],
[
'title' => __('localization.review.evidence'),
'label' => $this->evidenceStatusLabelForState($evidenceState),
'color' => $this->evidenceStatusColorForState($evidenceState),
'description' => $this->evidenceDimensionDescription($evidenceState),
],
[
'title' => __('localization.review.accepted_risk_status'),
'label' => $acceptedRisk['label'],
'color' => $acceptedRisk['color'],
'description' => $acceptedRisk['description'],
],
[
'title' => __('localization.review.review_pack'),
'label' => $packageAvailability['label'],
'color' => $this->governancePackageAvailabilityColor($tenant),
'description' => $this->reviewPackDimensionDescription($packageAvailability),
],
];
}
private function evidenceDimensionDescription(string $state): string
{
return match ($state) {
'available' => __('localization.review.evidence_dimension_available_description'),
'expired' => __('localization.review.evidence_dimension_expired_description'),
'restricted' => __('localization.review.evidence_dimension_restricted_description'),
default => __('localization.review.evidence_dimension_unavailable_description'),
};
}
/**
* @param array{state:string,label:string,description:string} $packageAvailability
*/
private function reviewPackDimensionDescription(array $packageAvailability): string
{
return match ($packageAvailability['state']) {
'available' => __('localization.review.review_pack_dimension_available_description'),
'not_available' => __('localization.review.review_pack_dimension_not_generated_description'),
'evidence_incomplete' => __('localization.review.review_pack_dimension_needs_refresh_description'),
'preparing' => __('localization.review.review_pack_dimension_preparing_description'),
'expired' => __('localization.review.review_pack_dimension_expired_description'),
default => __('localization.review.review_pack_dimension_unavailable_description'),
};
}
/**
* @return array{label:string,color:string,description:string}
*/
private function acceptedRiskDimensionForReview(EnvironmentReview $review): array
{
$acceptedRisks = $this->acceptedRisksForReview($review);
$hasFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review);
if ($hasFollowUp) {
return [
'label' => __('localization.review.accepted_risk_follow_up'),
'color' => 'warning',
'description' => __('localization.review.accepted_risk_dimension_follow_up_description'),
];
}
if ($acceptedRisks['count'] === 0) {
return [
'label' => __('localization.review.accepted_risk_no_action_needed'),
'color' => 'gray',
'description' => __('localization.review.accepted_risk_dimension_no_action_description'),
];
}
return [
'label' => __('localization.review.accepted_risk_on_record', ['count' => $acceptedRisks['count']]),
'color' => 'info',
'description' => __('localization.review.accepted_risk_dimension_on_record_description'),
];
}
private function acceptedRiskFollowUpRequiredForReview(EnvironmentReview $review): bool
{
$package = $this->governancePackageSummaryForReview($review);
$decisionSummary = is_array($package['decision_summary'] ?? null) ? $package['decision_summary'] : [];
if ((string) ($decisionSummary['status'] ?? '') === 'requires_awareness') {
return true;
}
$acceptedEntries = collect($package['accepted_risks'] ?? [])
->filter(static fn (mixed $entry): bool => is_array($entry));
$decisionEntries = collect($package['governance_decisions'] ?? [])
->filter(static fn (mixed $entry): bool => is_array($entry));
return $acceptedEntries
->merge($decisionEntries)
->contains(static fn (array $entry): bool => in_array(
(string) ($entry['governance_state'] ?? ''),
self::ACCEPTED_RISK_FOLLOW_UP_STATES,
true,
));
}
/**
* @param array{state:string,label:string,description:string} $packageAvailability
* @param array<string, mixed> $decision
* @param array{count:int,entries:list<array<string, string>>,empty_state:string} $acceptedRisks
* @return list<array{key:string,title:string,label:string,color:string,description:string,action_label:?string,action_url:?string}>
*/
private function evidencePathForReview(
EnvironmentReview $review,
ManagedEnvironment $tenant,
array $packageAvailability,
?string $downloadUrl,
array $decision,
array $acceptedRisks,
): array {
return [
$this->evidenceSnapshotProofForReview($review, $tenant),
$this->reviewPackProofForReview($packageAvailability, $downloadUrl),
$this->decisionTrailProofForReview($decision),
$this->acceptedRiskProofForReview($acceptedRisks),
$this->operationProofForReview($review, $tenant),
$this->exportArtifactProofForReview($packageAvailability, $downloadUrl),
];
}
/**
* @param list<array{key:string,title:string,label:string,color:string,description:string,action_label:?string,action_url:?string}> $evidencePath
* @return list<array{key:string,title:string,label:string,color:string,description:string,detail:string,action_label:?string,action_url:?string}>
*/
private function asideEvidencePath(array $evidencePath): array
{
$asideKeys = [
'evidence_snapshot',
'review_pack',
'decision_trail',
'operation_proof',
];
return collect($evidencePath)
->filter(static fn (array $proof): bool => in_array($proof['key'], $asideKeys, true))
->map(fn (array $proof): array => array_replace($proof, [
'label' => $this->asideEvidencePathLabel($proof),
'detail' => $this->asideEvidencePathDetail($proof),
]))
->values()
->all();
}
/**
* @param array{key:string,label:string,color:string} $proof
*/
private function asideEvidencePathLabel(array $proof): string
{
if ($proof['key'] !== 'decision_trail') {
return $proof['label'];
}
return match ($proof['color']) {
'success', 'info' => __('localization.review.available'),
'warning' => __('localization.review.limited'),
default => __('localization.review.unavailable'),
};
}
/**
* @param array{key:string,title:string,description:string} $proof
*/
private function asideEvidencePathDetail(array $proof): string
{
$description = (string) $proof['description'];
$titlePrefix = trim((string) $proof['title']).' ';
if (str_starts_with($description, $titlePrefix)) {
return Str::ucfirst(Str::replaceStart($titlePrefix, '', $description));
}
return $description;
}
/**
* @return array{key:string,title:string,label:string,color:string,description:string,action_label:?string,action_url:?string}
*/
private function evidenceSnapshotProofForReview(EnvironmentReview $review, ManagedEnvironment $tenant): array
{
$snapshot = $review->evidenceSnapshot;
$state = $this->evidenceStatusState($tenant);
$user = auth()->user();
$url = $snapshot instanceof EvidenceSnapshot && $user instanceof User && $user->can(Capabilities::EVIDENCE_VIEW, $tenant)
? EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'admin')
: null;
return [
'key' => 'evidence_snapshot',
'title' => __('localization.review.evidence_snapshot'),
'label' => $this->evidenceStatusLabelForState($state),
'color' => $this->evidenceStatusColorForState($state),
'description' => $snapshot instanceof EvidenceSnapshot && $snapshot->generated_at !== null
? __('localization.review.evidence_snapshot_available_description', [
'date' => $snapshot->generated_at->format('M j, Y H:i'),
])
: __('localization.review.evidence_proof_absent'),
'action_label' => $url !== null ? __('localization.review.view_evidence_snapshot') : null,
'action_url' => $url,
];
}
/**
* @param array{state:string,label:string,description:string} $packageAvailability
* @return array{key:string,title:string,label:string,color:string,description:string,action_label:?string,action_url:?string}
*/
private function reviewPackProofForReview(array $packageAvailability, ?string $downloadUrl): array
{
return [
'key' => 'review_pack',
'title' => __('localization.review.review_pack'),
'label' => $packageAvailability['label'],
'color' => match ($packageAvailability['state']) {
'available' => 'success',
'evidence_incomplete', 'preparing' => 'warning',
'expired', 'unavailable' => 'danger',
default => 'gray',
},
'description' => $packageAvailability['description'],
'action_label' => $downloadUrl !== null ? __('localization.review.download_review_pack') : null,
'action_url' => $downloadUrl,
];
}
/**
* @param array<string, mixed> $decision
* @return array{key:string,title:string,label:string,color:string,description:string,action_label:?string,action_url:?string}
*/
private function decisionTrailProofForReview(array $decision): array
{
return [
'key' => 'decision_trail',
'title' => __('localization.review.decision_trail'),
'label' => (string) $decision['label'],
'color' => (string) $decision['color'],
'description' => (string) $decision['summary'],
'action_label' => null,
'action_url' => null,
];
}
/**
* @param array{count:int,entries:list<array<string, string>>,empty_state:string} $acceptedRisks
* @return array{key:string,title:string,label:string,color:string,description:string,action_label:?string,action_url:?string}
*/
private function acceptedRiskProofForReview(array $acceptedRisks): array
{
return [
'key' => 'accepted_risk_records',
'title' => __('localization.review.accepted_risk_records'),
'label' => $acceptedRisks['count'] === 0
? __('localization.review.accepted_risk_none')
: __('localization.review.accepted_risk_on_record', ['count' => $acceptedRisks['count']]),
'color' => $acceptedRisks['count'] === 0 ? 'success' : 'info',
'description' => $acceptedRisks['count'] === 0
? $acceptedRisks['empty_state']
: __('localization.review.accepted_risk_records_description'),
'action_label' => null,
'action_url' => null,
];
}
/**
* @return array{key:string,title:string,label:string,color:string,description:string,action_label:?string,action_url:?string}
*/
private function operationProofForReview(EnvironmentReview $review, ManagedEnvironment $tenant): array
{
$run = collect([
$review->operationRun,
$review->evidenceSnapshot?->operationRun,
$review->currentExportReviewPack?->operationRun,
])->first(fn (mixed $candidate): bool => $candidate instanceof OperationRun);
if ($run instanceof OperationRun) {
return [
'key' => 'operation_proof',
'title' => __('localization.review.operation_proof'),
'label' => __('localization.review.available'),
'color' => 'info',
'description' => __('localization.review.operation_proof_available_description'),
'action_label' => OperationRunLinks::openLabel(),
'action_url' => OperationRunLinks::tenantlessView($run),
];
}
return [
'key' => 'operation_proof',
'title' => __('localization.review.operation_proof'),
'label' => __('localization.review.unavailable'),
'color' => 'gray',
'description' => __('localization.review.operation_proof_unavailable_description'),
'action_label' => null,
'action_url' => null,
];
}
/**
* @param array{state:string,label:string,description:string} $packageAvailability
* @return array{key:string,title:string,label:string,color:string,description:string,action_label:?string,action_url:?string}
*/
private function exportArtifactProofForReview(array $packageAvailability, ?string $downloadUrl): array
{
return [
'key' => 'export_artifact',
'title' => __('localization.review.export_artifact'),
'label' => $downloadUrl !== null
? __('localization.review.available')
: $packageAvailability['label'],
'color' => $downloadUrl !== null ? 'success' : 'gray',
'description' => $downloadUrl !== null
? __('localization.review.export_artifact_available_description')
: __('localization.review.export_artifact_unavailable_description'),
'action_label' => $downloadUrl !== null ? __('localization.review.download_review_pack') : null,
'action_url' => $downloadUrl,
];
}
/**
* @return array{summary_label:string,summary_color:string,items:list<array{label:string,value:string,color:string}>}
*/
private function acceptedRiskPanelForReview(EnvironmentReview $review): array
{
$package = $this->governancePackageSummaryForReview($review);
$hasFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review);
$acceptedEntries = collect($package['accepted_risks'] ?? [])
->filter(static fn (mixed $entry): bool => is_array($entry));
$decisionEntries = collect($package['governance_decisions'] ?? [])
->filter(static fn (mixed $entry): bool => is_array($entry));
$allEntries = $acceptedEntries->merge($decisionEntries);
$total = $allEntries->count();
$expiring = $allEntries->where('governance_state', 'expiring_exception')->count();
$expired = $allEntries->where('governance_state', 'expired_exception')->count();
$pending = $allEntries->where('governance_state', 'pending_exception')->count();
$needsReview = $allEntries
->filter(static fn (array $entry): bool => in_array(
(string) ($entry['governance_state'] ?? ''),
self::ACCEPTED_RISK_FOLLOW_UP_STATES,
true,
))
->count();
return [
'summary_label' => $hasFollowUp
? __('localization.review.accepted_risk_follow_up')
: __('localization.review.accepted_risk_no_action_needed'),
'summary_color' => $hasFollowUp ? ($expired > 0 ? 'danger' : 'warning') : 'gray',
'items' => [
[
'label' => __('localization.review.accepted_risks'),
'value' => (string) $total,
'color' => $total > 0 ? 'info' : 'gray',
],
[
'label' => __('localization.review.accepted_risks_expiring_soon'),
'value' => (string) $expiring,
'color' => $expiring > 0 ? 'warning' : 'gray',
],
[
'label' => __('localization.review.accepted_risks_expired'),
'value' => (string) $expired,
'color' => $expired > 0 ? 'danger' : 'gray',
],
[
'label' => __('localization.review.accepted_risks_pending_approval'),
'value' => (string) $pending,
'color' => $pending > 0 ? 'warning' : 'gray',
],
[
'label' => __('localization.review.accepted_risks_needs_review'),
'value' => (string) $needsReview,
'color' => $needsReview > 0 ? 'warning' : 'gray',
],
],
];
}
/**
* @param array{state:string,label:string,description:string} $packageAvailability
* @return array{status_label:string,status_color:string,description:string,last_generated_label:string,evidence_snapshot_label:string,export_label:string,operation_label:string,download_url:?string}
*/
private function reviewPackPanelForReview(
EnvironmentReview $review,
ManagedEnvironment $tenant,
array $packageAvailability,
?string $downloadUrl,
): array {
$pack = $review->currentExportReviewPack;
$snapshot = $review->evidenceSnapshot;
return [
'status_label' => $packageAvailability['label'],
'status_color' => $this->governancePackageAvailabilityColor($tenant),
'description' => $packageAvailability['description'],
'last_generated_label' => $pack instanceof ReviewPack && $pack->generated_at !== null
? $pack->generated_at->format('M j, Y H:i')
: __('localization.review.unavailable'),
'evidence_snapshot_label' => $snapshot instanceof EvidenceSnapshot && $snapshot->generated_at !== null
? $snapshot->generated_at->format('M j, Y H:i')
: __('localization.review.unavailable'),
'export_label' => $downloadUrl !== null
? __('localization.review.export_ready')
: __('localization.review.export_not_ready'),
'operation_label' => $pack instanceof ReviewPack && $pack->operationRun instanceof OperationRun
? OperationRunLinks::identifier($pack->operationRun)
: __('localization.review.operation_proof_unavailable'),
'download_url' => $downloadUrl,
];
}
/**
* @param array<string, mixed> $decision
* @return array{entries:list<array<string,string>>,empty_state:string}
*/
private function customerSafeFollowUpsForReview(array $decision): array
{
$entries = collect($decision['entries'] ?? [])
->filter(static fn (mixed $entry): bool => is_array($entry))
->map(static fn (array $entry): array => [
'title' => (string) ($entry['title'] ?? __('localization.review.follow_up')),
'priority' => (string) ($decision['label'] ?? __('localization.review.follow_up')),
'proof' => __('localization.review.decision_trail'),
'summary' => (string) ($entry['summary'] ?? __('localization.review.decision_entry_customer_safe_summary')),
'next_action' => (string) ($entry['next_action'] ?? __('localization.review.decision_summary_requires_awareness_next_action')),
])
->take(3)
->values()
->all();
return [
'entries' => $entries,
'empty_state' => __('localization.review.customer_safe_follow_ups_empty'),
];
}
/**
* @return array{label:string,summary:string}
*/
private function diagnosticsDisclosureForReview(): array
{
return [
'label' => __('localization.review.diagnostics'),
'summary' => __('localization.review.diagnostics_customer_workspace_default_hidden'),
];
}
/**
* @return list<array{label:string,value:string,color:string}>
*/
private function disclosureRuleRows(): array
{
return [
[
'label' => __('localization.review.disclosure_decision'),
'value' => __('localization.review.disclosure_visible'),
'color' => 'info',
],
[
'label' => __('localization.review.disclosure_evidence'),
'value' => __('localization.review.disclosure_visible'),
'color' => 'info',
],
[
'label' => __('localization.review.disclosure_diagnostics'),
'value' => __('localization.review.disclosure_collapsed'),
'color' => 'gray',
],
[
'label' => __('localization.review.disclosure_raw_support'),
'value' => __('localization.review.disclosure_hidden'),
'color' => 'gray',
],
];
}
@ -652,8 +1273,8 @@ private function latestReviewStateLabel(ManagedEnvironment $tenant): string
}
return $this->workspaceReviewNeedsAttention($tenant)
? __('localization.review.review_requires_attention')
: __('localization.review.ready_for_release');
? __('localization.review.review_needed')
: __('localization.review.ready_to_share');
}
private function latestReviewStateColor(ManagedEnvironment $tenant): string
@ -1165,6 +1786,10 @@ private function workspaceReviewNeedsAttention(ManagedEnvironment $tenant): bool
return true;
}
if ($this->acceptedRiskFollowUpRequiredForReview($review)) {
return true;
}
return $this->governancePackageAvailability($tenant)['state'] !== 'available';
}

View File

@ -328,18 +328,91 @@
'reporting' => 'Berichte',
'customer_reviews' => 'Kundenreviews',
'customer_review_workspace' => 'Kundenreview-Workspace',
'customer_safe_review_workspace' => 'Kundensicherer Governance-Paket-Index',
'customer_workspace_intro' => 'Prüfen Sie für jeden berechtigten ManagedEnvironment den executive-fähigen Status des Governance-Pakets und öffnen Sie bei Bedarf die kundensichere Detailansicht.',
'customer_safe_review_workspace' => 'Kundensichere Review-Pakete',
'customer_workspace_intro' => 'Prüfen Sie veröffentlichte Governance-Pakete, Evidence-Bereitschaft, akzeptierte Risiken und Übergabestatus über berechtigte Umgebungen hinweg.',
'customer_workspace_canonical_note' => 'Jede Zeile ist ein Einstieg in die Detailansicht: Dort sehen Sie Paketstatus, Executive-Einstieg, Nachweise, aktuelle Risiken und den nächsten kundensicheren Schritt.',
'customer_workspace_mapping_version' => 'Die Control-Readiness-Interpretation verwendet :version für diesen Workspace.',
'customer_workspace_non_certification_disclosure' => 'Dieser Workspace fasst die aktuelle Review- und Nachweislage für die Service-Auslieferung zusammen. Er ersetzt weder ein formales Auditurteil noch eine Zertifizierung oder rechtliche Attestierung.',
'customer_workspace_non_certification_disclosure' => 'Nur Service-Delivery-Zusammenfassung. Ersetzt weder formales Auditurteil noch Zertifizierung oder rechtliche Attestierung.',
'customer_workspace_scope_workspace_wide' => 'Workspace-weites Kundenreview',
'customer_workspace_scope_workspace_wide_description' => 'Zeigt das letzte veröffentlichte Review über berechtigte Umgebungen hinweg. Aktuelles erstes Review: :environment.',
'customer_workspace_scope_environment_filtered' => 'Umgebungsfilter: :environment',
'customer_workspace_scope_environment_filtered_description' => 'Die Seitendaten sind bewusst über den kanonischen environment_id-Filter eingegrenzt; die Shell bleibt Workspace-owned.',
'is_review_ready_to_share' => 'Ist dieses Review bereit zur Weitergabe?',
'ready_to_share' => 'Bereit zur Weitergabe',
'shareable_with_follow_up' => 'Teilbar mit Follow-up',
'follow_up_required_before_sharing' => 'Follow-up vor Weitergabe erforderlich',
'ready_to_share_reason' => 'Das veröffentlichte Review, der Nachweispfad und das aktuelle Review-Pack sind für die kundensichere Übergabe verfügbar.',
'shareable_with_follow_up_reason' => 'Das Review-Pack ist verfügbar, aber Accepted-Risk-Follow-up muss vor der Übergabe benannt werden.',
'follow_up_required_before_sharing_reason' => 'Review-Nachweis oder Paketverfügbarkeit benötigen noch Aufmerksamkeit, bevor dies geteilt werden kann.',
'ready_to_share_impact' => 'Stakeholder können das aktuelle Review-Pack und das veröffentlichte Review als Nachweispfad nutzen.',
'shareable_with_follow_up_impact' => 'Nutzen Sie das aktuelle Pack nur, wenn das Accepted-Risk-Follow-up in der Kundenübergabe enthalten ist.',
'follow_up_required_before_sharing_impact' => 'Behandeln Sie dieses Review erst als teilbar, wenn der nicht verfügbare Nachweis geprüft wurde.',
'impact' => 'Auswirkung',
'scope' => 'Scope',
'readiness' => 'Bereitschaft',
'evidence' => 'Evidence',
'readiness_dimension_description' => 'Die Bereitschaft wird aus veröffentlichtem Review, Evidence, Accepted-Risk- und Review-Pack-Status abgeleitet.',
'readiness_dimension_ready_description' => 'Veröffentlichtes Review ist verfügbar.',
'readiness_dimension_follow_up_description' => 'Follow-up vor Übergabe erforderlich.',
'accepted_risk_dimension_customer_safe_description' => 'Accepted-Risk-Status wird aus der veröffentlichten Review-Evidence ohne interne Verantwortlichkeitsdetails zusammengefasst.',
'accepted_risk_dimension_no_action_description' => 'Keine auslaufenden akzeptierten Risiken.',
'accepted_risk_dimension_follow_up_description' => 'Accepted-Risk-Follow-up ist erforderlich.',
'accepted_risk_dimension_on_record_description' => 'Akzeptierte Risiken sind erfasst.',
'evidence_dimension_available_description' => 'Evidence-Snapshot ist verknüpft.',
'evidence_dimension_unavailable_description' => 'Evidence-Nachweis ist nicht verfügbar.',
'evidence_dimension_expired_description' => 'Evidence-Snapshot ist abgelaufen.',
'evidence_dimension_restricted_description' => 'Evidence-Zugriff ist eingeschränkt.',
'review_pack_dimension_available_description' => 'Aktuelles Pack kann heruntergeladen werden.',
'review_pack_dimension_not_generated_description' => 'Review-Pack ist nicht erzeugt.',
'review_pack_dimension_needs_refresh_description' => 'Evidence-Aktualisierung erforderlich.',
'review_pack_dimension_preparing_description' => 'Review-Pack wird vorbereitet.',
'review_pack_dimension_expired_description' => 'Review-Pack ist abgelaufen.',
'review_pack_dimension_unavailable_description' => 'Review-Pack ist nicht bereit.',
'evidence_path' => 'Nachweispfad',
'decision_trail' => 'Entscheidungspfad',
'accepted_risk_records' => 'Accepted-Risk-Nachweise',
'accepted_risk_records_description' => 'Accepted-Risk-Entscheidungen sind in der Evidence-Basis des veröffentlichten Reviews vorhanden.',
'operation_proof' => 'Operation-Nachweis',
'operation_proof_available_description' => 'Ein zugehöriger Operationsdatensatz existiert für diesen Review-Nachweispfad.',
'operation_proof_unavailable' => 'Kein Operation-Nachweis verknüpft',
'operation_proof_unavailable_description' => 'Für diesen veröffentlichten Review-Pfad ist kein Operation-Nachweis verknüpft.',
'export_artifact' => 'Export-Artefakt',
'export_artifact_available_description' => 'Das aktuelle Export-Artefakt kann über den Review-Pack-Flow heruntergeladen werden.',
'export_artifact_unavailable_description' => 'In diesem kundensicheren Flow ist kein herunterladbares Export-Artefakt verfügbar.',
'last_generated' => 'Zuletzt erzeugt',
'evidence_snapshot_used' => 'Verwendeter Evidence-Snapshot',
'export_availability' => 'Export-Verfügbarkeit',
'export_ready' => 'Export bereit',
'export_not_ready' => 'Export nicht bereit',
'review_package_index' => 'Review-Paket-Index',
'review_package_index_description' => 'Veröffentlichte Reviews und kundensichere Paketeinträge, die in diesem Workspace verfügbar sind.',
'review_pack_state' => 'Review-Pack-Status',
'evidence_source' => 'Evidence-Quelle',
'customer_safe_follow_ups' => 'Kundensichere Follow-ups',
'customer_safe_follow_ups_empty' => 'Für dieses veröffentlichte Review sind keine kundensicheren Follow-ups aufgeführt.',
'diagnostics_customer_workspace_default_hidden' => 'Supportdetails bleiben auf autorisierten Diagnoseflächen und werden in diesem kundensicheren Workspace standardmäßig nicht angezeigt.',
'accepted_risk_summary' => 'Akzeptierte Risiken',
'accepted_risk_no_action_needed' => 'Keine Aktion erforderlich',
'accepted_risks_expiring_soon' => 'Läuft bald ab',
'accepted_risks_expired' => 'Abgelaufen',
'accepted_risks_pending_approval' => 'Freigabe ausstehend',
'accepted_risks_needs_review' => 'Review erforderlich',
'disclosure_rule' => 'Offenlegungsregel',
'disclosure_decision' => 'Entscheidung',
'disclosure_evidence' => 'Evidence',
'disclosure_diagnostics' => 'Diagnosen',
'disclosure_raw_support' => 'Rohdaten/Support',
'disclosure_visible' => 'Sichtbar',
'disclosure_collapsed' => 'Eingeklappt',
'disclosure_hidden' => 'Ausgeblendet',
'evidence_snapshot_available_description' => 'Evidence-Snapshot erzeugt am :date.',
'latest_released_review' => 'Letztes veröffentlichtes Review',
'released_review_for_environment' => 'Veröffentlichtes Review für :environment',
'filtered_by_environment' => 'Gefiltert nach Umgebung: :environment',
'published_date' => 'Veröffentlicht :date',
'reviews' => 'Reviews',
'clear_filters' => 'Filter löschen',
'tenant' => 'Tenant',
'tenant' => 'Umgebung',
'latest_review' => 'Letztes Review',
'review_status' => 'Review-Status',
'status' => 'Status',
@ -392,13 +465,13 @@
'governance_package_expired_description' => 'Das aktuelle Export-Review-Pack ist abgelaufen und kann aus diesem veröffentlichten Review nicht heruntergeladen werden.',
'governance_package_blocked' => 'Governance-Paket blockiert',
'governance_package_blocked_description' => 'Dieses Konto kann das veröffentlichte Review lesen, aber das aktuelle Export-Review-Pack nicht herunterladen.',
'no_entitled_tenants' => 'Keine berechtigten Tenants passen zu dieser Ansicht',
'no_entitled_tenants' => 'Keine berechtigten Umgebungen passen zu dieser Ansicht',
'no_released_customer_reviews' => 'Keine veröffentlichten Kundenreviews passen zu dieser Ansicht',
'no_released_customer_reviews_description' => 'Veröffentlichen Sie ein Tenant-Review, bevor es im kundensicheren Workspace erscheint.',
'no_released_customer_reviews_description' => 'Veröffentlichen Sie ein Environment-Review, bevor es im kundensicheren Workspace erscheint.',
'filtered_no_released_customer_reviews' => 'Keine veröffentlichten Kundenreviews passen zum aktiven Umgebungsfilter.',
'filtered_no_released_customer_reviews_description' => 'Löschen Sie den Umgebungsfilter, um andere veröffentlichte Reviews in diesem Workspace zu sehen.',
'clear_filters_description' => 'Löschen Sie die aktuellen Filter, um zum vollständigen Kundenreview-Workspace für Ihre berechtigten Tenants zurückzukehren.',
'adjust_filters_description' => 'Passen Sie die Filter an, um zum vollständigen Kundenreview-Workspace für Ihre berechtigten Tenants zurückzukehren.',
'clear_filters_description' => 'Löschen Sie die aktuellen Filter, um zum vollständigen Kundenreview-Workspace für Ihre berechtigten Umgebungen zurückzukehren.',
'adjust_filters_description' => 'Passen Sie die Filter an, um zum vollständigen Kundenreview-Workspace für Ihre berechtigten Umgebungen zurückzukehren.',
'no_published_review' => 'Kein veröffentlichtes Review',
'no_published_review_available' => 'Noch kein veröffentlichtes Review verfügbar',
'no_findings_recorded' => 'Im veröffentlichten Review sind keine Findings erfasst.',
@ -414,6 +487,7 @@
'accepted_risk_partial_accountability' => 'Die Verantwortlichkeit ist teilweise erfasst; Review-Owner-Details sind nicht vollständig verfügbar.',
'unavailable' => 'Nicht verfügbar',
'available' => 'Verfügbar',
'limited' => 'Eingeschränkt',
'partial' => 'Teilweise',
'blocked' => 'Blockiert',
'expired' => 'Abgelaufen',
@ -444,6 +518,7 @@
'assessment_basis_description' => 'Diese Prüfbereiche zeigen, wie die Aussagen des Pakets durch die aktuelle Review-Evidenz gestützt werden.',
'review_completed' => 'Review abgeschlossen',
'review_requires_attention' => 'Prüfung erforderlich',
'review_needed' => 'Review erforderlich',
'ready_for_release' => 'Zur Veröffentlichung bereit',
'accepted_risk_status' => 'Status akzeptierter Risiken',
'accepted_risk_none' => 'Keine erfasst',
@ -558,7 +633,7 @@
'executive_pack_description' => 'Öffnet den aktuellen Export, der zu diesem Review gehört.',
'customer_workspace' => 'Kunden-Workspace',
'open_customer_workspace' => 'Kunden-Workspace öffnen',
'customer_workspace_description' => 'Öffnet den kundensicheren Review-Workspace mit Filter auf diesen Tenant.',
'customer_workspace_description' => 'Öffnet den kundensicheren Review-Workspace mit Filter auf diese Umgebung.',
'view_evidence_snapshot' => 'Evidence-Snapshot anzeigen',
'evidence_snapshot_description' => 'Zur Evidence-Basis hinter diesem Review zurückkehren.',
],

View File

@ -328,18 +328,91 @@
'reporting' => 'Reporting',
'customer_reviews' => 'Customer reviews',
'customer_review_workspace' => 'Customer Review Workspace',
'customer_safe_review_workspace' => 'Customer-safe governance package index',
'customer_workspace_intro' => 'Review the executive-ready governance package status for each entitled tenant and open the customer-safe detail when follow-up is needed.',
'customer_safe_review_workspace' => 'Customer-safe review packages',
'customer_workspace_intro' => 'Review released governance packages, evidence readiness, accepted risks, and handoff status across entitled environments.',
'customer_workspace_canonical_note' => 'Each row is an index entry: open the review detail to inspect package status, the executive entrypoint, supporting evidence, current risks, and the next customer-safe action.',
'customer_workspace_mapping_version' => 'Control readiness interpretation uses :version for this workspace.',
'customer_workspace_non_certification_disclosure' => 'This workspace summarizes current review evidence for service delivery. It does not replace a formal audit opinion, certification, or legal attestation.',
'customer_workspace_non_certification_disclosure' => 'Service delivery summary only. Does not replace formal audit opinion, certification, or legal attestation.',
'customer_workspace_scope_workspace_wide' => 'Workspace-wide customer review',
'customer_workspace_scope_workspace_wide_description' => 'Showing the latest released review across entitled environments. Current first review: :environment.',
'customer_workspace_scope_environment_filtered' => 'Environment filter: :environment',
'customer_workspace_scope_environment_filtered_description' => 'The page data is intentionally narrowed by the canonical environment_id filter while the shell remains workspace-owned.',
'is_review_ready_to_share' => 'Is this review ready to share?',
'ready_to_share' => 'Ready to share',
'shareable_with_follow_up' => 'Shareable with follow-up',
'follow_up_required_before_sharing' => 'Follow-up required before sharing',
'ready_to_share_reason' => 'The released review, evidence path, and current review pack are available for customer-safe handoff.',
'shareable_with_follow_up_reason' => 'The review pack is available, but accepted-risk follow-up must be called out before handoff.',
'follow_up_required_before_sharing_reason' => 'Review proof or package availability still needs attention before this can be shared.',
'ready_to_share_impact' => 'Stakeholders can use the current review pack and released review as the evidence path.',
'shareable_with_follow_up_impact' => 'Use the current pack only with the accepted-risk follow-up included in the customer handoff.',
'follow_up_required_before_sharing_impact' => 'Do not treat this review as share-ready until the unavailable proof has been reviewed.',
'impact' => 'Impact',
'scope' => 'Scope',
'readiness' => 'Readiness',
'evidence' => 'Evidence',
'readiness_dimension_description' => 'Readiness is derived from the released review, evidence, accepted-risk, and review-pack state.',
'readiness_dimension_ready_description' => 'Released review is available.',
'readiness_dimension_follow_up_description' => 'Follow-up required before handoff.',
'accepted_risk_dimension_customer_safe_description' => 'Accepted-risk status is summarized from the released review evidence without internal accountability details.',
'accepted_risk_dimension_no_action_description' => 'No expiring accepted risks.',
'accepted_risk_dimension_follow_up_description' => 'Accepted-risk follow-up is required.',
'accepted_risk_dimension_on_record_description' => 'Accepted risks are recorded.',
'evidence_dimension_available_description' => 'Evidence snapshot is linked.',
'evidence_dimension_unavailable_description' => 'Evidence proof is not available.',
'evidence_dimension_expired_description' => 'Evidence snapshot is expired.',
'evidence_dimension_restricted_description' => 'Evidence access is restricted.',
'review_pack_dimension_available_description' => 'Current pack can be downloaded.',
'review_pack_dimension_not_generated_description' => 'Review pack is not generated.',
'review_pack_dimension_needs_refresh_description' => 'Evidence refresh needed.',
'review_pack_dimension_preparing_description' => 'Review pack is being prepared.',
'review_pack_dimension_expired_description' => 'Review pack is expired.',
'review_pack_dimension_unavailable_description' => 'Review pack is not ready.',
'evidence_path' => 'Evidence path',
'decision_trail' => 'Decision trail',
'accepted_risk_records' => 'Accepted risk records',
'accepted_risk_records_description' => 'Accepted-risk decisions are present in the released review evidence basis.',
'operation_proof' => 'Operation proof',
'operation_proof_available_description' => 'A related operation record exists for this review evidence path.',
'operation_proof_unavailable' => 'No operation proof linked',
'operation_proof_unavailable_description' => 'No operation proof link is attached to this released review path.',
'export_artifact' => 'Export artifact',
'export_artifact_available_description' => 'The current export artifact can be downloaded through the review pack flow.',
'export_artifact_unavailable_description' => 'No downloadable export artifact is available from this customer-safe flow.',
'last_generated' => 'Last generated',
'evidence_snapshot_used' => 'Evidence snapshot used',
'export_availability' => 'Export availability',
'export_ready' => 'Export ready',
'export_not_ready' => 'Export not ready',
'review_package_index' => 'Review package index',
'review_package_index_description' => 'Released reviews and customer-safe package entries available in this workspace.',
'review_pack_state' => 'Review pack state',
'evidence_source' => 'Evidence source',
'customer_safe_follow_ups' => 'Customer-safe follow-ups',
'customer_safe_follow_ups_empty' => 'No customer-safe follow-ups are listed for this released review.',
'diagnostics_customer_workspace_default_hidden' => 'Support details stay on authorized diagnostic surfaces and are not shown in this customer-safe workspace by default.',
'accepted_risk_summary' => 'Accepted risks',
'accepted_risk_no_action_needed' => 'No action needed',
'accepted_risks_expiring_soon' => 'Expiring soon',
'accepted_risks_expired' => 'Expired',
'accepted_risks_pending_approval' => 'Pending approval',
'accepted_risks_needs_review' => 'Needs review',
'disclosure_rule' => 'Disclosure rule',
'disclosure_decision' => 'Decision',
'disclosure_evidence' => 'Evidence',
'disclosure_diagnostics' => 'Diagnostics',
'disclosure_raw_support' => 'Raw/support',
'disclosure_visible' => 'Visible',
'disclosure_collapsed' => 'Collapsed',
'disclosure_hidden' => 'Hidden',
'evidence_snapshot_available_description' => 'Evidence snapshot generated :date.',
'latest_released_review' => 'Latest released review',
'released_review_for_environment' => 'Released review for :environment',
'filtered_by_environment' => 'Filtered by environment: :environment',
'published_date' => 'Published :date',
'reviews' => 'Reviews',
'clear_filters' => 'Clear filters',
'tenant' => 'Tenant',
'tenant' => 'Environment',
'latest_review' => 'Latest review',
'review_status' => 'Review status',
'status' => 'Status',
@ -392,13 +465,13 @@
'governance_package_expired_description' => 'The current export review pack has expired and cannot be downloaded from this released review.',
'governance_package_blocked' => 'Governance package blocked',
'governance_package_blocked_description' => 'This account can read the released review but cannot download the current export review pack.',
'no_entitled_tenants' => 'No entitled tenants match this view',
'no_entitled_tenants' => 'No entitled environments match this view',
'no_released_customer_reviews' => 'No released customer reviews match this view',
'no_released_customer_reviews_description' => 'Publish an environment review before it appears in the customer-safe workspace.',
'filtered_no_released_customer_reviews' => 'No released customer reviews match the active environment filter.',
'filtered_no_released_customer_reviews_description' => 'Clear the environment filter to view other released reviews in this workspace.',
'clear_filters_description' => 'Clear the current filters to return to the full customer review workspace for your entitled tenants.',
'adjust_filters_description' => 'Adjust filters to return to the full customer review workspace for your entitled tenants.',
'clear_filters_description' => 'Clear the current filters to return to the full customer review workspace for your entitled environments.',
'adjust_filters_description' => 'Adjust filters to return to the full customer review workspace for your entitled environments.',
'no_published_review' => 'No published review',
'no_published_review_available' => 'No published review available yet',
'no_findings_recorded' => 'No findings recorded in the published review.',
@ -414,6 +487,7 @@
'accepted_risk_partial_accountability' => 'Accountability is partially recorded; review owner details are not fully available.',
'unavailable' => 'Unavailable',
'available' => 'Available',
'limited' => 'Limited',
'partial' => 'Partial',
'blocked' => 'Blocked',
'expired' => 'Expired',
@ -444,6 +518,7 @@
'assessment_basis_description' => 'These assessment areas explain how the package statements are supported by the current review evidence.',
'review_completed' => 'Review completed',
'review_requires_attention' => 'Review required',
'review_needed' => 'Review needed',
'ready_for_release' => 'Ready for release',
'accepted_risk_status' => 'Accepted risk status',
'accepted_risk_none' => 'None on record',
@ -558,7 +633,7 @@
'executive_pack_description' => 'Open the current export that belongs to this review.',
'customer_workspace' => 'Customer workspace',
'open_customer_workspace' => 'Open customer workspace',
'customer_workspace_description' => 'Open the customer-safe review workspace prefiltered to this tenant.',
'customer_workspace_description' => 'Open the customer-safe review workspace prefiltered to this environment.',
'view_evidence_snapshot' => 'View evidence snapshot',
'evidence_snapshot_description' => 'Return to the evidence basis behind this review.',
],

View File

@ -4,163 +4,339 @@
$reviewPayload = $this->latestReviewConsumptionPayload();
@endphp
<x-filament::section>
<div class="flex flex-col gap-3">
<div class="text-lg font-semibold text-gray-900 dark:text-gray-100">
{{ __('localization.review.customer_safe_review_workspace') }}
</div>
<div class="text-sm text-gray-600 dark:text-gray-300">
{{ __('localization.review.customer_workspace_intro') }}
</div>
<div class="text-sm text-gray-600 dark:text-gray-300">
{{ __('localization.review.customer_workspace_canonical_note') }}
</div>
<div class="rounded-md border border-amber-100 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-900/40 dark:bg-amber-950/30 dark:text-amber-200">
{{ __('localization.review.customer_workspace_non_certification_disclosure') }}
</div>
@if ($environmentFilterChip !== null)
@include('filament.partials.workspace-hub-environment-filter-chip', [
'label' => $environmentFilterChip['label'],
'clearUrl' => $environmentFilterChip['clear_url'],
])
@endif
</div>
</x-filament::section>
@if ($reviewPayload)
@php
$latest = $reviewPayload['latest'];
$decision = $reviewPayload['decision'];
$acceptedRisks = $reviewPayload['accepted_risks'];
$evidenceBasis = $reviewPayload['evidence_basis'];
@endphp
<x-filament::section :heading="__('localization.review.latest_released_review')">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="flex flex-col gap-3">
<div>
<div class="text-base font-semibold text-gray-950 dark:text-white">
{{ $latest['review_label'] }}
</div>
<div class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ __('localization.review.published_date', ['date' => $latest['published_label']]) }}
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
<x-filament::badge :color="$latest['status_color']">
{{ $latest['status_label'] }}
</x-filament::badge>
<x-filament::badge :color="$latest['package_color']">
{{ $latest['package_badge_label'] }}
</x-filament::badge>
</div>
<div class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">
{{ __('localization.review.review_pack') }}
</div>
<p class="max-w-3xl text-sm text-gray-600 dark:text-gray-300">
{{ $latest['package_description'] }}
</p>
</div>
<div class="flex flex-wrap gap-2 lg:justify-end">
@if ($latest['primary_action_url'])
<x-filament::button
tag="a"
:href="$latest['primary_action_url']"
:icon="$latest['primary_action_icon']"
target="_blank"
>
{{ $latest['primary_action_label'] }}
</x-filament::button>
@endif
@if ($latest['secondary_action_url'])
<x-filament::button
tag="a"
:href="$latest['secondary_action_url']"
color="gray"
icon="heroicon-o-arrow-top-right-on-square"
>
{{ $latest['secondary_action_label'] }}
</x-filament::button>
@endif
</div>
</div>
</x-filament::section>
<div class="grid gap-4 lg:grid-cols-3">
<x-filament::section :heading="__('localization.review.decision_summary')">
<div class="flex flex-col gap-3">
<x-filament::badge :color="$decision['color']">
{{ $decision['label'] }}
</x-filament::badge>
<div class="space-y-4">
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900">
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
<div class="max-w-4xl space-y-1.5">
<h2 class="text-base font-semibold text-gray-950 dark:text-white">
{{ __('localization.review.customer_safe_review_workspace') }}
</h2>
<p class="text-sm text-gray-600 dark:text-gray-300">
{{ $decision['summary'] }}
{{ __('localization.review.customer_workspace_intro') }}
</p>
<p class="text-sm font-medium text-gray-800 dark:text-gray-100">
{{ $decision['next_action'] }}
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ __('localization.review.customer_workspace_non_certification_disclosure') }}
</p>
@foreach ($decision['entries'] as $entry)
<div class="border-t border-gray-200 pt-3 text-sm dark:border-white/10">
<div class="font-medium text-gray-950 dark:text-white">
{{ $entry['title'] }}
</div>
<div class="mt-1 text-gray-600 dark:text-gray-300">
{{ $entry['summary'] }}
</div>
<div class="mt-1 text-gray-500 dark:text-gray-400">
{{ $entry['next_action'] }}
</div>
</div>
@endforeach
</div>
</x-filament::section>
<x-filament::section :heading="__('localization.review.accepted_risks')">
<div class="flex flex-col gap-3">
@forelse ($acceptedRisks['entries'] as $risk)
<div class="text-sm">
@if ($environmentFilterChip !== null)
<div class="shrink-0">
@include('filament.partials.workspace-hub-environment-filter-chip', [
'label' => $environmentFilterChip['label'],
'clearUrl' => $environmentFilterChip['clear_url'],
])
</div>
@endif
</div>
</div>
@if ($reviewPayload)
@php
$latest = $reviewPayload['latest'];
$scope = $reviewPayload['scope'];
$readiness = $reviewPayload['readiness'];
$dimensions = $reviewPayload['readiness_dimensions'];
$asideEvidencePath = $reviewPayload['aside_evidence_path'];
$reviewPackPanel = $reviewPayload['review_pack_panel'];
$acceptedRisks = $reviewPayload['accepted_risks'];
$acceptedRiskPanel = $reviewPayload['accepted_risk_panel'];
$followUps = $reviewPayload['follow_ups'];
$diagnostics = $reviewPayload['diagnostics'];
$disclosureRules = $reviewPayload['disclosure_rules'];
@endphp
<div class="grid grid-cols-1 gap-4 md:grid-cols-3 xl:grid-cols-12">
<main class="min-w-0 space-y-4 md:col-span-2 xl:col-span-8">
<div
class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900"
data-testid="customer-review-decision-card"
>
<div class="flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-2">
<span class="font-medium text-gray-950 dark:text-white">{{ $risk['title'] }}</span>
<x-filament::badge color="gray" size="sm">
{{ $risk['state_label'] }}
<x-filament::badge :color="$readiness['color']">
{{ $readiness['label'] }}
</x-filament::badge>
<x-filament::badge color="gray">
{{ __('localization.review.customer_safe') }}
</x-filament::badge>
</div>
<div class="mt-1 text-gray-600 dark:text-gray-300">
{{ $risk['summary'] }}
<div class="space-y-2">
<h2 class="text-xl font-semibold text-gray-950 dark:text-white">
{{ $readiness['question'] }}
</h2>
<p class="max-w-3xl text-sm text-gray-600 dark:text-gray-300">
{{ $readiness['reason'] }}
</p>
</div>
<div class="grid gap-3 md:grid-cols-[minmax(0,1fr)_16rem]">
<div class="flex h-full flex-col gap-2 rounded-lg border border-gray-200 bg-gray-50 p-4 text-sm dark:border-white/10 dark:bg-white/5">
<div class="text-[0.7rem] font-semibold uppercase leading-4 tracking-wide text-gray-500 dark:text-gray-400">
{{ __('localization.review.impact') }}
</div>
<p class="text-sm leading-6 text-gray-700 dark:text-gray-200">
{{ $readiness['impact'] }}
</p>
</div>
<div class="flex h-full flex-col rounded-lg border border-gray-200 p-4 text-sm dark:border-white/10">
<div class="text-[0.7rem] font-semibold uppercase leading-4 tracking-wide text-gray-500 dark:text-gray-400">
{{ __('localization.review.latest_released_review') }}
</div>
<div class="mt-2 font-medium leading-5 text-gray-950 dark:text-white">
{{ $latest['environment_label'] }}
</div>
<div class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-400">
{{ __('localization.review.published_date', ['date' => $latest['published_label']]) }}
</div>
</div>
</div>
<div class="flex flex-wrap gap-2">
@if ($readiness['primary_action_url'])
<x-filament::button
tag="a"
:href="$readiness['primary_action_url']"
:icon="$readiness['primary_action_icon']"
target="_blank"
>
{{ $readiness['primary_action_label'] }}
</x-filament::button>
@endif
@if ($latest['secondary_action_url'])
<x-filament::button
tag="a"
:href="$latest['secondary_action_url']"
color="gray"
:icon="$latest['secondary_action_icon']"
>
{{ $latest['secondary_action_label'] }}
</x-filament::button>
@endif
</div>
</div>
@empty
<p class="text-sm text-gray-600 dark:text-gray-300">
{{ $acceptedRisks['empty_state'] }}
</div>
<div class="grid gap-3 sm:grid-cols-2 2xl:grid-cols-4" data-testid="customer-review-readiness-dimensions">
@foreach ($dimensions as $dimension)
<div class="rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900">
<div class="flex min-h-32 flex-col gap-2">
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
{{ $dimension['title'] }}
</div>
<div>
<x-filament::badge :color="$dimension['color']">
{{ $dimension['label'] }}
</x-filament::badge>
</div>
<p class="text-xs leading-5 text-gray-600 dark:text-gray-300">
{{ $dimension['description'] }}
</p>
</div>
</div>
@endforeach
</div>
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900">
<div class="flex flex-col gap-3">
<div>
<h2 class="text-sm font-semibold text-gray-950 dark:text-white">
{{ __('localization.review.customer_safe_follow_ups') }}
</h2>
</div>
<div class="grid gap-3 md:grid-cols-2">
@forelse ($followUps['entries'] as $followUp)
<div class="rounded-lg border border-gray-200 p-3 text-sm dark:border-white/10">
<div class="flex flex-wrap items-center gap-2">
<span class="font-medium text-gray-950 dark:text-white">{{ $followUp['title'] }}</span>
<x-filament::badge color="gray" size="sm">
{{ $followUp['priority'] }}
</x-filament::badge>
</div>
<div class="mt-1 text-gray-600 dark:text-gray-300">
{{ $followUp['summary'] }}
</div>
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{{ $followUp['proof'] }} · {{ $followUp['next_action'] }}
</div>
</div>
@empty
<p class="text-sm text-gray-600 dark:text-gray-300">
{{ $followUps['empty_state'] }}
</p>
@endforelse
</div>
</div>
</div>
<div class="space-y-3" data-testid="customer-review-package-index">
<div>
<h2 class="text-sm font-semibold text-gray-950 dark:text-white">
{{ __('localization.review.review_package_index') }}
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300">
{{ __('localization.review.review_package_index_description') }}
</p>
</div>
{{ $this->table }}
</div>
</main>
<aside class="space-y-3 md:col-span-1 md:sticky md:top-4 md:self-start xl:col-span-4" data-testid="customer-review-evidence-aside">
<div class="rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900">
<h2 class="text-sm font-semibold text-gray-950 dark:text-white">
{{ __('localization.review.evidence_path') }}
</h2>
<dl class="mt-2 divide-y divide-gray-100 dark:divide-white/10">
@foreach ($asideEvidencePath as $proof)
<div class="py-2 first:pt-0 last:pb-0">
<dt class="flex min-w-0 items-center justify-between gap-3 text-xs">
<span class="min-w-0 font-medium text-gray-700 dark:text-gray-200">
{{ $proof['title'] }}
</span>
<span class="shrink-0">
<x-filament::badge :color="$proof['color']" size="sm">
{{ $proof['label'] }}
</x-filament::badge>
</span>
</dt>
<dd class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-400">
{{ $proof['detail'] }}
</dd>
</div>
@endforeach
</dl>
</div>
<div class="rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900">
<div class="flex items-start justify-between gap-3">
<h2 class="text-sm font-semibold text-gray-950 dark:text-white">
{{ __('localization.review.review_pack_state') }}
</h2>
<x-filament::badge :color="$reviewPackPanel['status_color']" size="sm">
{{ $reviewPackPanel['status_label'] }}
</x-filament::badge>
</div>
<p class="mt-2 text-xs leading-5 text-gray-600 dark:text-gray-300">
{{ $reviewPackPanel['description'] }}
</p>
@endforelse
</div>
</x-filament::section>
<x-filament::section :heading="__('localization.review.evidence_basis')">
<div class="flex flex-col gap-3">
<x-filament::badge :color="$evidenceBasis['color']">
{{ $evidenceBasis['label'] }}
</x-filament::badge>
<dl class="mt-2 space-y-1.5 text-xs">
<div class="flex justify-between gap-3">
<dt class="text-gray-500 dark:text-gray-400">{{ __('localization.review.last_generated') }}</dt>
<dd class="text-right font-medium text-gray-900 dark:text-gray-100">{{ $reviewPackPanel['last_generated_label'] }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt class="text-gray-500 dark:text-gray-400">{{ __('localization.review.evidence_source') }}</dt>
<dd class="text-right font-medium text-gray-900 dark:text-gray-100">{{ $reviewPackPanel['evidence_snapshot_label'] }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt class="text-gray-500 dark:text-gray-400">{{ __('localization.review.export_availability') }}</dt>
<dd class="text-right font-medium text-gray-900 dark:text-gray-100">{{ $reviewPackPanel['export_label'] }}</dd>
</div>
<div class="flex justify-between gap-3">
<dt class="text-gray-500 dark:text-gray-400">{{ __('localization.review.operation_proof') }}</dt>
<dd class="text-right font-medium text-gray-900 dark:text-gray-100">{{ $reviewPackPanel['operation_label'] }}</dd>
</div>
</dl>
</div>
<p class="text-sm text-gray-600 dark:text-gray-300">
{{ $evidenceBasis['summary'] }}
<div class="rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900">
<div class="flex items-start justify-between gap-3">
<h2 class="text-sm font-semibold text-gray-950 dark:text-white">
{{ __('localization.review.accepted_risk_summary') }}
</h2>
<x-filament::badge :color="$acceptedRiskPanel['summary_color']" size="sm">
{{ $acceptedRiskPanel['summary_label'] }}
</x-filament::badge>
</div>
<div class="mt-3 space-y-2">
@foreach ($acceptedRiskPanel['items'] as $item)
<div class="flex items-center justify-between gap-3 text-xs">
<span class="text-gray-500 dark:text-gray-400">{{ $item['label'] }}</span>
<x-filament::badge :color="$item['color']" size="sm">
{{ $item['value'] }}
</x-filament::badge>
</div>
@endforeach
</div>
<details class="group mt-3 border-t border-gray-200 pt-3 dark:border-white/10">
<summary class="cursor-pointer text-xs font-semibold uppercase text-gray-500 marker:text-gray-400 dark:text-gray-400">
{{ __('localization.review.accepted_risk_records') }}
</summary>
<div class="mt-2 space-y-2">
@forelse ($acceptedRisks['entries'] as $risk)
<div class="text-xs">
<div class="flex flex-wrap items-center gap-2">
<span class="font-medium text-gray-900 dark:text-gray-100">{{ $risk['title'] }}</span>
<x-filament::badge color="gray" size="sm">
{{ $risk['state_label'] }}
</x-filament::badge>
</div>
<div class="mt-1 leading-5 text-gray-500 dark:text-gray-400">
{{ $risk['summary'] }}
</div>
</div>
@empty
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">
{{ $acceptedRisks['empty_state'] }}
</p>
@endforelse
</div>
</details>
</div>
<div class="rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900">
<h2 class="text-sm font-semibold text-gray-950 dark:text-white">
{{ __('localization.review.disclosure_rule') }}
</h2>
<div class="mt-2 space-y-1.5">
@foreach ($disclosureRules as $rule)
<div class="flex items-center justify-between gap-3 text-xs">
<span class="text-gray-500 dark:text-gray-400">{{ $rule['label'] }}</span>
<x-filament::badge :color="$rule['color']" size="sm">
{{ $rule['value'] }}
</x-filament::badge>
</div>
@endforeach
</div>
<details class="group mt-3 border-t border-gray-200 pt-3 dark:border-white/10" data-testid="customer-review-diagnostics">
<summary class="cursor-pointer text-sm font-medium text-gray-700 marker:text-gray-400 dark:text-gray-200">
{{ $diagnostics['label'] }}
</summary>
<p class="mt-2 text-xs leading-5 text-gray-600 dark:text-gray-300">
{{ $diagnostics['summary'] }}
</p>
</details>
</div>
</aside>
</div>
@else
<div class="space-y-3" data-testid="customer-review-package-index">
<div>
<h2 class="text-sm font-semibold text-gray-950 dark:text-white">
{{ __('localization.review.review_package_index') }}
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300">
{{ __('localization.review.review_package_index_description') }}
</p>
</div>
</x-filament::section>
</div>
@endif
{{ $this->table }}
{{ $this->table }}
</div>
@endif
</div>
</x-filament-panels::page>

View File

@ -83,19 +83,22 @@
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->click('Kunden-Workspace öffnen')
->waitForText('Kundensicherer Governance-Paket-Index')
->waitForText('Kundensichere Review-Pakete')
->assertSee('Filter löschen')
->assertSee('Review öffnen')
->assertSee('Governance-Paket')
->assertSee('Status')
->assertSee('Nachweise')
->assertSee('Prüfen Sie für jeden berechtigten ManagedEnvironment den executive-fähigen Status des Governance-Pakets')
->assertSee('Dieser Workspace fasst die aktuelle Review- und Nachweislage für die Service-Auslieferung zusammen. Er ersetzt weder ein formales Auditurteil noch eine Zertifizierung oder rechtliche Attestierung.')
->assertSee('Prüfen Sie veröffentlichte Governance-Pakete, Evidence-Bereitschaft, akzeptierte Risiken und Übergabestatus über berechtigte Umgebungen hinweg.')
->assertSee('Nur Service-Delivery-Zusammenfassung. Ersetzt weder formales Auditurteil noch Zertifizierung oder rechtliche Attestierung.')
->assertSee('Letztes veröffentlichtes Review')
->assertSee('Review-Paket-Index')
->assertSee('Offenlegungsregel')
->assertSee('Eingeklappt')
->assertSee('Review-Pack herunterladen')
->assertSee('Das aktuelle Review-Pack ist zum Download bereit.')
->assertSee('Keine Entscheidungen mit Aufmerksamkeitsbedarf')
->assertSee('Zur Veröffentlichung bereit')
->assertSee('Bereit zur Weitergabe')
->assertSee('Verfügbar')
->assertDontSee('Customer-safe governance package index')
->assertDontSee('localization.review.customer_safe_review_workspace')

View File

@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Models\ManagedEnvironment;
use App\Models\ReviewPack;
use App\Models\User;
use App\Support\EnvironmentReviewStatus;
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
use App\Support\Workspaces\WorkspaceContext;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Storage;
uses(RefreshDatabase::class);
pest()->browser()->timeout(60_000);
beforeEach(function (): void {
Storage::fake('exports');
});
it('Spec326 smokes clean customer review workspace entry', function (): void {
[$user, $environmentA, $environmentB] = spec326CustomerReviewWorkspaceFixture();
spec326AuthenticateCustomerReviewWorkspaceBrowser($this, $user, $environmentA);
visit(CustomerReviewWorkspace::getUrl(panel: 'admin'))
->waitForText('Customer-safe review packages')
->assertSee('No environment selected')
->assertDontSee('Environment filter:')
->assertSee('Is this review ready to share?')
->assertSee('Evidence path')
->assertSee('Review pack state')
->assertSee('Accepted risks')
->assertSee('Disclosure rule')
->assertSee('Diagnostics')
->assertSee('Collapsed')
->assertSee('Raw/support')
->assertSee('Review package index')
->assertSee('Decision trail')
->assertSee('Customer-safe follow-ups')
->assertScript('(() => {
const decision = document.querySelector("[data-testid=\"customer-review-decision-card\"]");
const aside = document.querySelector("[data-testid=\"customer-review-evidence-aside\"]");
if (! decision || ! aside) {
return false;
}
const decisionBox = decision.getBoundingClientRect();
const asideBox = aside.getBoundingClientRect();
return window.innerWidth < 768 || asideBox.left > decisionBox.left;
})()', true)
->assertSee($environmentB->name)
->assertDontSee('entitled tenant')
->assertDontSee('tenant filter')
->assertDontSee('raw payload should stay hidden')
->assertDontSee('stack trace should stay hidden')
->assertDontSee('provider secret should stay hidden')
->assertDontSee('debug metadata should stay hidden')
->assertDontSee('source fingerprint should stay hidden')
->assertScript('document.querySelector("[data-testid=\"customer-review-diagnostics\"]")?.open === false', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec326CustomerReviewWorkspaceScreenshot('customer-review-workspace--clean'));
});
it('Spec326 smokes filtered customer review workspace clear and reload behavior', function (): void {
[$user, $environmentA, $environmentB] = spec326CustomerReviewWorkspaceFixture();
$cleanPath = json_encode((string) parse_url(CustomerReviewWorkspace::getUrl(panel: 'admin'), PHP_URL_PATH), JSON_THROW_ON_ERROR);
spec326AuthenticateCustomerReviewWorkspaceBrowser($this, $user, $environmentA);
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($environmentA))
->waitForText('Environment filter:')
->assertSee($environmentA->name)
->assertDontSee($environmentB->name)
->assertSee('Environment filter: '.$environmentA->name)
->assertSee('Is this review ready to share?')
->assertSee('Evidence path')
->assertSee('Review package index')
->assertScript('document.querySelector("[data-testid=\"customer-review-diagnostics\"]")?.open === false', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec326CustomerReviewWorkspaceScreenshot('customer-review-workspace--filtered'));
$page
->click('[data-testid="workspace-hub-environment-filter-clear"]')
->waitForText('No environment selected')
->assertDontSee('Environment filter:')
->assertSee($environmentB->name)
->assertScript("window.location.pathname === {$cleanPath}", true)
->assertScript('! window.location.search.includes("environment_id=")', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec326CustomerReviewWorkspaceScreenshot('customer-review-workspace--after-clear'));
$page->script('window.location.reload();');
$page
->waitForText('No environment selected')
->assertDontSee('Environment filter:')
->assertSee($environmentB->name)
->assertScript("window.location.pathname === {$cleanPath}", true)
->assertScript('! window.location.search.includes("environment_id=")', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec326CustomerReviewWorkspaceScreenshot('customer-review-workspace--after-reload'));
});
it('Spec326 smokes customer-safe diagnostics disclosure', function (): void {
[$user, $environmentA] = spec326CustomerReviewWorkspaceFixture();
spec326AuthenticateCustomerReviewWorkspaceBrowser($this, $user, $environmentA);
visit(CustomerReviewWorkspace::getUrl(panel: 'admin'))
->waitForText('Diagnostics')
->assertScript('document.querySelector("[data-testid=\"customer-review-diagnostics\"]")?.open === false', true)
->click('[data-testid="customer-review-diagnostics"] summary')
->assertScript('document.querySelector("[data-testid=\"customer-review-diagnostics\"]")?.open === true', true)
->assertSee('Support details stay on authorized diagnostic surfaces')
->assertDontSee('raw payload should stay hidden')
->assertDontSee('internal exception should stay hidden')
->assertDontSee('source fingerprint should stay hidden')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec326CustomerReviewWorkspaceScreenshot('customer-review-workspace--diagnostics'));
});
/**
* @return array{0: User, 1: ManagedEnvironment, 2: ManagedEnvironment}
*/
function spec326CustomerReviewWorkspaceFixture(): array
{
$environmentA = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec326 Browser Environment A',
'external_id' => 'spec326-browser-environment-a',
]);
[$user, $environmentA] = createUserWithTenant(
tenant: $environmentA,
role: 'owner',
workspaceRole: 'manager',
);
$environmentB = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $environmentA->workspace_id,
'name' => 'Spec326 Browser Environment B',
'external_id' => 'spec326-browser-environment-b',
]);
createUserWithTenant(
tenant: $environmentB,
user: $user,
role: 'owner',
workspaceRole: 'manager',
);
spec326CreatePublishedReview($environmentA, $user, 'review-packs/spec326-browser-a.zip', now());
spec326CreatePublishedReview($environmentB, $user, 'review-packs/spec326-browser-b.zip', now()->subHour());
return [$user, $environmentA, $environmentB];
}
function spec326AuthenticateCustomerReviewWorkspaceBrowser(
mixed $test,
User $user,
ManagedEnvironment $environment,
): void {
$workspaceId = (int) $environment->workspace_id;
$test->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => $workspaceId,
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
(string) $workspaceId => (int) $environment->getKey(),
],
]);
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
(string) $workspaceId => (int) $environment->getKey(),
]);
}
function spec326CreatePublishedReview(
ManagedEnvironment $environment,
User $user,
string $filePath,
\Illuminate\Support\Carbon $publishedAt,
): void {
$snapshot = seedEnvironmentReviewEvidence($environment);
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
$summary = is_array($review->summary) ? $review->summary : [];
$summary['debug_payload'] = 'raw payload should stay hidden';
$summary['stack_trace'] = 'stack trace should stay hidden';
$summary['provider_secret'] = 'provider secret should stay hidden';
$summary['debug_metadata'] = 'debug metadata should stay hidden';
$summary['internal_exception'] = 'internal exception should stay hidden';
$summary['source_fingerprint'] = 'source fingerprint should stay hidden';
$summary['control_interpretation']['version_key'] = ComplianceEvidenceMappingV1::VERSION_KEY;
$summary['control_interpretation']['controls'] = [
[
'control_key' => 'customer-handoff-readiness',
'title' => 'Customer handoff readiness',
'readiness_bucket' => 'evidence_on_record',
'readiness_label' => 'Evidence on record',
'primary_reason' => 'Evidence path is complete.',
'recommended_next_action' => 'Share the current review pack.',
],
];
$review->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'summary' => $summary,
'generated_at' => $publishedAt,
'published_at' => $publishedAt,
'published_by_user_id' => (int) $user->getKey(),
])->save();
Storage::disk('exports')->put($filePath, 'PK-test');
$pack = 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' => $filePath,
'file_disk' => 'exports',
'generated_at' => $publishedAt,
]);
$review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
}
function spec326CustomerReviewWorkspaceScreenshot(string $name): string
{
return 'spec326-'.$name;
}

View File

@ -40,10 +40,11 @@
Livewire::actingAs($user)
->test(CustomerReviewWorkspace::class)
->assertCanSeeTableRecords([$tenant->fresh()])
->assertSee('Kundensicherer Governance-Paket-Index')
->assertSee('Prüfen Sie für jeden berechtigten ManagedEnvironment den executive-fähigen Status des Governance-Pakets und öffnen Sie bei Bedarf die kundensichere Detailansicht.')
->assertSee('Jede Zeile ist ein Einstieg in die Detailansicht: Dort sehen Sie Paketstatus, Executive-Einstieg, Nachweise, aktuelle Risiken und den nächsten kundensicheren Schritt.')
->assertSee('Dieser Workspace fasst die aktuelle Review- und Nachweislage für die Service-Auslieferung zusammen. Er ersetzt weder ein formales Auditurteil noch eine Zertifizierung oder rechtliche Attestierung.')
->assertSee('Kundensichere Review-Pakete')
->assertSee('Prüfen Sie veröffentlichte Governance-Pakete, Evidence-Bereitschaft, akzeptierte Risiken und Übergabestatus über berechtigte Umgebungen hinweg.')
->assertSee('Nur Service-Delivery-Zusammenfassung. Ersetzt weder formales Auditurteil noch Zertifizierung oder rechtliche Attestierung.')
->assertSee('Review-Paket-Index')
->assertSee('Offenlegungsregel')
->assertSee('Governance-Paket')
->assertSee('Nachweise')
->assertSee('Nächster Schritt')

View File

@ -78,7 +78,8 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t
->assertSee('Open review')
->assertDontSee('Generate pack')
->assertDontSee('Regenerate')
->assertDontSee('Expire');
->assertDontSee('Expire snapshot')
->assertDontSee('Expire review pack');
});
it('keeps the customer review workspace download action visible while suspended read-only', function (): void {
@ -119,7 +120,8 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t
->assertSee('Open review')
->assertDontSee('Generate pack')
->assertDontSee('Regenerate')
->assertDontSee('Expire');
->assertDontSee('Expire snapshot')
->assertDontSee('Expire review pack');
});
it('shows a customer-safe missing review-pack state without exposing pack mutation actions', function (): void {
@ -147,7 +149,8 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t
->assertDontSee('Download review pack')
->assertDontSee('Generate pack')
->assertDontSee('Regenerate')
->assertDontSee('Expire');
->assertDontSee('Expire snapshot')
->assertDontSee('Expire review pack');
});
it('shows a partial governance-package state when the released review basis is limitation-aware', function (): void {

View File

@ -7,6 +7,7 @@
use App\Models\Finding;
use App\Models\FindingException;
use App\Models\ManagedEnvironment;
use App\Models\ReviewPack;
use App\Models\User;
use App\Support\EnvironmentReviewStatus;
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
@ -16,7 +17,22 @@
uses(RefreshDatabase::class);
it('lists only the latest published review per entitled tenant on the customer review workspace', function (): void {
it('keeps the Spec 326 repo truth map available for customer review workspace productization', function (): void {
$path = repo_path('specs/326-customer-review-workspace-v1-productization/repo-truth-map.md');
expect($path)->toBeFile();
$contents = file_get_contents($path);
expect($contents)->toContain('Tenant Reviews / Environment Reviews')
->and($contents)->toContain('Evidence Snapshots')
->and($contents)->toContain('Review Packs / exports')
->and($contents)->toContain('Accepted Risks / Risk Exceptions')
->and($contents)->toContain('OperationRuns')
->and($contents)->toContain('Audit log');
});
it('lists only the latest published review per entitled environment on the customer review workspace', function (): void {
$tenantA = ManagedEnvironment::factory()->create(['name' => 'Alpha ManagedEnvironment']);
[$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'readonly');
@ -92,9 +108,13 @@
->assertCanSeeTableRecords([$tenantA->fresh(), $tenantB->fresh()])
->assertCanNotSeeTableRecords([$tenantDenied->fresh()])
->assertSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $latestPublishedReview->fresh()], $tenantA), false)
->assertSee('Review the executive-ready governance package status for each entitled tenant and open the customer-safe detail when follow-up is needed.')
->assertSee('Each row is an index entry: open the review detail to inspect package status, the executive entrypoint, supporting evidence, current risks, and the next customer-safe action.')
->assertSee('This workspace summarizes current review evidence for service delivery. It does not replace a formal audit opinion, certification, or legal attestation.')
->assertSee('Customer-safe review packages')
->assertSee('Review released governance packages, evidence readiness, accepted risks, and handoff status across entitled environments.')
->assertSee('Service delivery summary only. Does not replace formal audit opinion, certification, or legal attestation.')
->assertSee('Review package index')
->assertSee('Released reviews and customer-safe package entries available in this workspace.')
->assertDontSee('for each entitled tenant')
->assertDontSee('Each row is an index entry')
->assertSee('Governance package')
->assertSee('Status')
->assertSee('Evidence')
@ -117,6 +137,231 @@
->assertDontSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $deniedPublishedReview->fresh()], $tenantDenied), false);
});
it('renders the Spec 326 decision-first review workspace without default raw diagnostics', function (): void {
$tenant = ManagedEnvironment::factory()->create(['name' => 'Decision Ready ManagedEnvironment']);
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
$snapshot = seedEnvironmentReviewEvidence($tenant);
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
$summary = is_array($review->summary) ? $review->summary : [];
$summary['debug_payload'] = 'raw payload should stay hidden';
$summary['stack_trace'] = 'stack trace should stay hidden';
$summary['internal_exception'] = 'internal exception should stay hidden';
$summary['provider_secret'] = 'provider secret should stay hidden';
$summary['debug_metadata'] = 'debug metadata should stay hidden';
$summary['source_fingerprint'] = 'source fingerprint should stay hidden';
$summary['control_interpretation']['version_key'] = ComplianceEvidenceMappingV1::VERSION_KEY;
$summary['control_interpretation']['controls'] = [
[
'control_key' => 'customer-handoff-readiness',
'title' => 'Customer handoff readiness',
'readiness_bucket' => 'evidence_on_record',
'readiness_label' => 'Evidence on record',
'primary_reason' => 'Evidence path is complete.',
'recommended_next_action' => 'Share the current review pack.',
],
];
$summary['governance_package']['decision_summary']['entries'][] = [
'title' => 'Customer acceptance checkpoint',
'summary' => 'Confirm stakeholder awareness before handoff.',
'next_action' => 'Record the customer decision trail before the next review.',
];
$review->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'summary' => $summary,
'published_at' => now(),
'published_by_user_id' => (int) $user->getKey(),
])->save();
$pack = ReviewPack::factory()->ready()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'environment_review_id' => (int) $review->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
]);
$review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
$this->actingAs($user);
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
Livewire::actingAs($user)
->test(CustomerReviewWorkspace::class)
->assertSee('Customer Review Workspace')
->assertSee('Is this review ready to share?')
->assertSee('Readiness')
->assertSee('Evidence')
->assertSee('Accepted risk status')
->assertSee('Evidence path')
->assertSeeHtml('data-testid="customer-review-evidence-aside"')
->assertSee('Review pack state')
->assertSee('Review pack')
->assertSee('Decision trail')
->assertSee('Accepted risk records')
->assertSee('Accepted risks')
->assertSee('Expiring soon')
->assertSee('Expired')
->assertSee('Pending approval')
->assertSee('Needs review')
->assertSee('Operation proof')
->assertSee('Customer-safe follow-ups')
->assertSee('Review package index')
->assertSee('Disclosure rule')
->assertSee('Decision')
->assertSee('Visible')
->assertSee('Diagnostics')
->assertSee('Collapsed')
->assertSee('Raw/support')
->assertSee('Hidden')
->assertSee('Support details stay on authorized diagnostic surfaces')
->assertSee('Customer acceptance checkpoint')
->assertSee('Download review pack')
->assertDontSee('raw payload should stay hidden')
->assertDontSee('stack trace should stay hidden')
->assertDontSee('internal exception should stay hidden')
->assertDontSee('provider secret should stay hidden')
->assertDontSee('debug metadata should stay hidden')
->assertDontSee('source fingerprint should stay hidden')
->assertDontSee('Publish review')
->assertDontSee('Refresh review')
->assertDontSee('Regenerate')
->assertDontSee('Expire snapshot');
});
it('does not show ready to share when accepted-risk follow-up is required', function (): void {
$environment = ManagedEnvironment::factory()->create(['name' => 'Accepted Risk Follow-up ManagedEnvironment']);
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'readonly');
$snapshot = seedEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0);
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
$summary = is_array($review->summary) ? $review->summary : [];
$summary['control_interpretation']['version_key'] = ComplianceEvidenceMappingV1::VERSION_KEY;
$summary['control_interpretation']['controls'] = [
[
'control_key' => 'customer-handoff-readiness',
'title' => 'Customer handoff readiness',
'readiness_bucket' => 'evidence_on_record',
'readiness_label' => 'Evidence on record',
'primary_reason' => 'Evidence path is complete.',
'recommended_next_action' => 'Share the current review pack.',
],
];
$summary['governance_package']['decision_summary'] = [
'status' => 'none',
'total_count' => 0,
'summary' => 'No decisions require customer awareness.',
'next_action' => 'No customer action is needed.',
'entries' => [],
];
$summary['governance_package']['accepted_risks'] = [
[
'title' => 'Accepted risk renewal',
'governance_state' => 'expiring_exception',
'customer_summary' => 'Review the accepted-risk follow-up before customer handoff.',
],
];
$review->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'summary' => $summary,
'published_at' => now(),
'published_by_user_id' => (int) $user->getKey(),
])->save();
$pack = 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(),
]);
$review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
$this->actingAs($user);
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $environment->workspace_id);
Livewire::actingAs($user)
->test(CustomerReviewWorkspace::class)
->assertSee('Is this review ready to share?')
->assertSee('Shareable with follow-up')
->assertSee('accepted-risk follow-up must be called out before handoff')
->assertSee('Review needed')
->assertSee('Follow-up required')
->assertSee('Accepted-risk follow-up is required.')
->assertSee('Download review pack')
->assertSeeInOrder(['Shareable with follow-up', 'Open review', 'Download review pack'])
->assertDontSee('Ready to share');
});
it('customer_review_workspace_does_not_use_tenant_as_platform_context_copy', function (): void {
$environment = ManagedEnvironment::factory()->create(['name' => 'Vocabulary ManagedEnvironment']);
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'readonly');
$snapshot = seedEnvironmentReviewEvidence($environment);
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
$review->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'published_at' => now(),
'published_by_user_id' => (int) $user->getKey(),
])->save();
$this->actingAs($user);
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $environment->workspace_id);
Livewire::actingAs($user)
->test(CustomerReviewWorkspace::class)
->assertSee('Customer-safe review packages')
->assertSee('entitled environments')
->assertDontSee('entitled tenant')
->assertDontSee('entitled tenants')
->assertDontSee('current tenant')
->assertDontSee('tenant filter')
->assertDontSee('for each entitled tenant');
});
it('shows explicit unavailable proof states instead of false share readiness', function (): void {
$tenant = ManagedEnvironment::factory()->create(['name' => 'Needs Evidence ManagedEnvironment']);
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
$snapshot = seedPartialEnvironmentReviewEvidence($tenant);
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
$review->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'published_at' => now(),
'published_by_user_id' => (int) $user->getKey(),
])->save();
$pack = ReviewPack::factory()->ready()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'environment_review_id' => (int) $review->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
]);
$review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
$this->actingAs($user);
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
Livewire::actingAs($user)
->test(CustomerReviewWorkspace::class)
->assertSee('Is this review ready to share?')
->assertSee('Follow-up required before sharing')
->assertSee('Evidence incomplete')
->assertSee('No operation proof linked')
->assertSee('Export not ready')
->assertDontSee('Ready to share')
->assertDontSee('Download review pack');
});
it('shows the current released review using deterministic published review ordering', function (): void {
$publishedAt = now()->subHour();
$tenantA = ManagedEnvironment::factory()->create(['name' => 'Alpha ManagedEnvironment']);
@ -157,12 +402,13 @@
'Latest released review',
'Beta ManagedEnvironment',
$publishedAt->format('M j, Y'),
'No decisions require awareness',
'Decision trail',
'No governance decisions require customer awareness in this released review.',
])
->assertSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $betaReview->fresh()], $tenantB), false);
});
it('excludes entitled tenants without a published review from customer workspace rows', function (): void {
it('excludes entitled environments without a published review from customer workspace rows', function (): void {
$tenantPublished = ManagedEnvironment::factory()->create(['name' => 'Published ManagedEnvironment']);
[$user, $tenantPublished] = createUserWithTenant(tenant: $tenantPublished, role: 'readonly');
@ -242,7 +488,7 @@
->assertDontSee('Publish an environment review before it appears in the customer-safe workspace.');
});
it('uses a page-level empty state when no entitled tenant has a released review', function (): void {
it('uses a page-level empty state when no entitled environment has a released review', function (): void {
$tenant = ManagedEnvironment::factory()->create(['name' => 'Internal Only ManagedEnvironment']);
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
$snapshot = seedEnvironmentReviewEvidence($tenant);
@ -310,7 +556,7 @@
->assertSee('Accepted risks')
->assertSee('Accepted risk')
->assertSee('Included in the released review evidence basis.')
->assertSee('Review required')
->assertSee('Review needed')
->assertSee('Open review')
->assertDontSee('Ready for release')
->assertDontSee('Risk Owner')
@ -344,7 +590,8 @@
Livewire::actingAs($user)
->test(CustomerReviewWorkspace::class)
->assertSee('Decision evidence unavailable')
->assertSee('Decision trail')
->assertSee('Unavailable')
->assertSee('Customer-safe decision evidence is unavailable for this released review.')
->assertDontSee('raw evidence JSON')
->assertDontSee('legacy-fingerprint-abc123')

View File

@ -0,0 +1,67 @@
# Requirements Checklist: Spec 326 Customer Review Workspace v1 Productization
**Purpose**: Validate preparation artifacts before implementation.
**Created**: 2026-05-18
**Feature**: [spec.md](../spec.md)
## Candidate Selection Gate
- [x] CHK001 Candidate is directly user-provided and also aligned with the P1 Customer Review Workspace lane in `docs/product/spec-candidates.md`.
- [x] CHK002 No existing `specs/326-*` package existed before generation.
- [x] CHK003 Related completed specs 312 and 314-325 are treated as historical context and not modified.
- [x] CHK004 Close alternatives are deferred as follow-up specs 327-331.
- [x] CHK005 Scope is a single existing runtime surface and does not create a portal or backend foundation.
- [x] CHK006 Spec Candidate Check is complete with approval class, red flags, score, and decision.
## Spec Readiness Gate
- [x] CHK007 `spec.md` exists.
- [x] CHK008 `plan.md` exists.
- [x] CHK009 `tasks.md` exists.
- [x] CHK010 `repo-truth-map.md` exists because the active spec requires it before runtime changes.
- [x] CHK011 Problem statement, product value, users, goals, non-goals, requirements, acceptance criteria, risks, assumptions, and open questions are present.
- [x] CHK012 Plan identifies likely affected repo surfaces and supporting surfaces.
- [x] CHK013 Tasks are ordered, checklisted, testable, and implementation-ready.
- [x] CHK014 RBAC, workspace/environment isolation, auditability, OperationRun proof semantics, evidence truth, and UX disclosure are addressed.
- [x] CHK015 No open question blocks safe implementation.
- [x] CHK016 Scope is bounded enough for a later implementation loop.
## UI / Productization Coverage
- [x] CHK017 UI Surface Impact is not contradictory and marks the existing page/customer-facing/status/context changes.
- [x] CHK018 UI/Productization Coverage identifies route/page, archetype, design depth, repo-truth level, existing pattern, screenshot need, and coverage decision.
- [x] CHK019 Decision-first role, audience-aware disclosure, UI/UX surface classification, and operator surface contract are present.
- [x] CHK020 Spec preserves Spec 325 visual direction as calibration only, not runtime truth.
- [x] CHK021 Diagnostics are required to be secondary/collapsed and customer-safe defaults are explicit.
## Architecture / Anti-Bloat
- [x] CHK022 Proportionality review says no new source of truth, persisted entity/table/artifact, public abstraction, status family, or cross-domain UI framework.
- [x] CHK023 Plan forbids migrations/packages/env/queues/scheduler/storage/deployment asset changes unless spec/plan are updated first.
- [x] CHK024 Provider boundary check prevents raw provider semantics and Graph payloads from entering default UI.
- [x] CHK025 Shared pattern reuse names existing services/policies/status enums and forbids a new runtime pattern library.
## Filament / Livewire
- [x] CHK026 Livewire v4.0+ compliance is explicit.
- [x] CHK027 Panel provider registration remains `apps/platform/bootstrap/providers.php`.
- [x] CHK028 Related resources keep global search disabled unless a future implementation changes them with View/Edit safety proof.
- [x] CHK029 No destructive action is planned; any unexpected high-impact action has confirmation/auth/audit/test requirements.
- [x] CHK030 Asset strategy is no new assets; `filament:assets` only if implementation unexpectedly registers assets.
## Test Governance
- [x] CHK031 Feature/Livewire and Browser lanes are explicit.
- [x] CHK032 Browser smoke is named and scoped.
- [x] CHK033 Existing helpers/fixtures should be reused; no broad fixture defaults are planned.
- [x] CHK034 Required validation commands are listed.
## Review Outcome
- [x] CHK035 Review outcome class: acceptable-special-case.
- [x] CHK036 Workflow outcome: keep.
- [x] CHK037 Final note location: active feature implementation close-out and final report.
## Notes
Preparation artifacts are ready for implementation. Runtime acceptance remains pending until tasks are implemented, targeted tests pass, browser smoke passes, screenshots are captured where generated, and final no-impact/no-migration/no-package statements are reported.

View File

@ -0,0 +1,266 @@
# Implementation Plan: Spec 326 - Customer Review Workspace v1 Productization
**Branch**: `326-customer-review-workspace-v1-productization` | **Date**: 2026-05-18 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/326-customer-review-workspace-v1-productization/spec.md`
## Summary
Productize the existing Customer Review Workspace into a customer-safe, decision-first review consumption hub. The implementation must refactor the existing Filament page/view around readiness, evidence path, review-pack state, accepted risks, follow-ups, scope, and diagnostics disclosure while preserving existing workspace/environment contracts, policies, audit, and repo-truth boundaries.
No application implementation was performed during preparation. Runtime implementation starts from `tasks.md` in a later implementation loop.
## Technical Context
**Language/Version**: PHP 8.4.15, Laravel 12.52.0
**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Laravel Sail 1.52.0
**Storage**: PostgreSQL via Sail/Dokploy; no schema change expected
**Testing**: Pest 4.3.1, PHPUnit 12.5.4, Pest Browser for named smoke
**Validation Lanes**: confidence plus browser for Spec 326 smoke; no PostgreSQL migration lane expected unless runtime changes unexpectedly touch schema
**Target Platform**: Laravel monolith under `apps/platform`
**Project Type**: web application / Filament admin panel
**Performance Goals**: render from persisted DB state only; no Graph calls during render; avoid broad eager loading or per-row service calls beyond existing patterns
**Constraints**: customer-safe default view, no raw diagnostics, no false green, no new backend foundation, no migrations/packages/env/assets by default
**Scale/Scope**: one existing workspace-scoped page and targeted tests
## UI / Surface Guardrail Plan
- **Guardrail scope**: changed surface.
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**:
- `/admin/reviews/workspace`
- `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`
- `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`
- existing review/pack/evidence/finding links only as repo-supported handoffs.
- **No-impact class, if applicable**: N/A.
- **Native vs custom classification summary**: native Filament page and components plus existing Blade composition; no CSS/JS asset bundle expected.
- **Shared-family relevance**: customer-safe review consumption, status messaging, action links, evidence/report viewer, review-pack delivery, diagnostics disclosure.
- **State layers in scope**: page payload, URL query (`environment_id`), table/session filter state, diagnostic disclosure state if implemented.
- **Audience modes in scope**: customer/read-only, MSP operator, auditor/account manager; support/operator diagnostics only where authorized.
- **Decision/diagnostic/raw hierarchy plan**: decision-first default, evidence/proof second, diagnostics/support/raw third and collapsed/gated.
- **Raw/support gating plan**: hidden by default; only explicit disclosure and existing authorization if implemented.
- **One-primary-action / duplicate-truth control**: main decision card owns status/reason/impact/primary action; panels add proof/source without restating the same blocker in multiple forms.
- **Handling modes by drift class or surface**: review-mandatory for customer-safe disclosure, environment scope, and false-green risk.
- **Repository-signal treatment**: review-mandatory; update `repo-truth-map.md` before runtime edits and document unavailable states.
- **Special surface test profiles**: `global-context-shell`, customer-safe disclosure, browser smoke.
- **Required tests or manual smoke**: Feature/Livewire tests for layout/RBAC/scope/disclosure plus Pest Browser smoke for clean/filtered/clear/reload/diagnostics.
- **Exception path and spread control**: none expected; any CSS-heavy/custom UI or new backend support must update spec/plan before implementation.
- **Active feature PR close-out entry**: Smoke Coverage.
- **UI/Productization coverage decision**: existing page materially changed; implementation must either update coverage registry or document why existing UI-006 report/Spec 325 target artifacts remain sufficient.
- **Coverage artifacts to update**: likely `docs/ui-ux-enterprise-audit/design-coverage-matrix.md` or close-out note; exact need depends on runtime diff.
- **No-impact rationale**: N/A.
- **Navigation / Filament provider-panel handling**: no panel provider or navigation change expected; registration remains in `apps/platform/bootstrap/providers.php`.
- **Screenshot or page-report need**: screenshots required under the spec artifacts; no full new page report unless implementation changes route inventory beyond the existing page.
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes.
- **Systems touched**: CustomerReviewWorkspace, review workspace Blade, WorkspaceHubEnvironmentFilter/FilterStateResetter, EnvironmentReviewRegisterService, ReviewPackService, existing policies and resources for handoff links.
- **Shared abstractions reused**: `EnvironmentReviewRegisterService`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `ArtifactTruthPresenter` where already used, existing `ReviewPackStatus`, `EnvironmentReviewStatus`, `EvidenceSnapshotStatus`, `FindingException` state/validity, existing `WorkspaceAuditLogger`.
- **New abstraction introduced? why?**: none expected. Page-local helpers are permitted only to keep Blade simple.
- **Why the existing abstraction was sufficient or insufficient**: existing services/policies already own truth and access. The gap is productized layout/disclosure, not missing backend foundation.
- **Bounded deviation / spread control**: no cross-surface pattern library; any helper stays private to the page unless a later spec proves broader need.
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: no start/completion semantics; possible secondary proof links only.
- **Central contract reused**: existing operation route/link support if proof links are exposed.
- **Delegated UX behaviors**: N/A for start; proof links must use existing URL/auth helpers.
- **Surface-owned behavior kept local**: display proof availability/unavailability only.
- **Queued DB-notification policy**: N/A.
- **Terminal notification path**: unchanged.
- **Exception path**: none.
## Provider Boundary & Portability Fit
- **Shared provider/platform boundary touched?**: no new provider boundary.
- **Provider-owned seams**: none.
- **Platform-core seams**: review/evidence/review-pack/accepted-risk display from TenantPilot artifacts.
- **Neutral platform terms / contracts preserved**: workspace, environment filter, review, evidence, review pack, accepted risk, decision trail, operation proof.
- **Retained provider-specific semantics and why**: existing review content may contain Microsoft/Intune terms where it is already customer-safe.
- **Bounded extraction or follow-up path**: none for Spec 326.
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- Inventory-first: page consumes persisted review/evidence/pack/finding data only.
- Read/write separation: default page is read-only; no destructive or remote mutation actions expected.
- Graph contract path: no Graph calls in UI render or this feature's default flow.
- Deterministic capabilities: reuse existing capability/policy checks; no raw strings in new action handlers.
- RBAC-UX: non-member workspace/environment access is 404; missing capabilities hide/disable actions according to existing policy semantics.
- Workspace isolation: workspace context remains primary; `environment_id` resolved only within current workspace and actor entitlement.
- Tenant isolation: managed-environment data remains scoped by workspace and entitlement.
- Run observability: no new OperationRun; proof links only when existing and authorized.
- Ops-UX lifecycle: unchanged.
- Test governance: Feature and browser tests are explicitly named; helper/fixture cost must remain feature-local.
- Proportionality: no new persisted truth, status families, abstractions, or cross-domain UI framework.
- No premature abstraction: page-local derived display helpers only.
- Persisted truth: no migrations, seeders, backfills, or packages expected.
- Behavioral state: display states are derived labels, not persisted domain states.
- UI semantics: do not turn badges/explanations into a new semantic layer.
- Shared pattern first: use existing Filament, status enums, `ArtifactTruthPresenter`, and action/link helpers where present.
- Provider boundary: no provider-core drift.
- Filament-native UI: use native Filament sections/cards/badges/actions and existing partials first.
- UI/Productization coverage: material page change must be reflected in active spec and implementation close-out or registry docs.
- Filament v5 / Livewire v4 compliance: required; no v3/v4 legacy APIs.
- Panel provider location: remains `apps/platform/bootstrap/providers.php`.
- Global search: related resources currently keep `protected static bool $isGloballySearchable = false`; do not enable global search in this spec.
- Destructive actions: none expected; if introduced, must be `Action::make(...)->action(...)` plus confirmation, auth, audit, tests.
- Asset strategy: no new registered assets expected; `filament:assets` only needed if implementation unexpectedly registers assets.
## Test Governance Check
- **Test purpose / classification by changed surface**: Feature/Livewire for page payload, RBAC, scope, disclosure; Browser for critical customer-safe workflow smoke.
- **Affected validation lanes**: confidence, browser.
- **Why this lane mix is the narrowest sufficient proof**: page is user-facing and shell/filter-sensitive; Feature tests prove logic, Browser smoke proves rendered shell/filter/disclosure behavior.
- **Narrowest proving command(s)**:
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Reviews tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec326CustomerReviewWorkspaceProductizationSmokeTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail artisan test --filter='CustomerReviewWorkspace|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact`
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
- `git diff --check`
- **Fixture / helper / factory / seed / context cost risks**: reuse existing review/evidence/pack helpers; no broad fixture default changes.
- **Expensive defaults or shared helper growth introduced?**: no.
- **Heavy-family additions, promotions, or visibility changes**: one explicit browser smoke family named for Spec 326.
- **Surface-class relief / special coverage rule**: no standard-native relief because customer-safe disclosure and global-context-shell behavior require explicit tests.
- **Closing validation and reviewer handoff**: verify no raw diagnostics, no false green, correct scope, RBAC action visibility, screenshots, and unchanged panel provider/global search posture.
- **Budget / baseline / trend follow-up**: none expected.
- **Review-stop questions**: Does any visible claim lack repo source? Does any URL alias set scope? Is any action unauthorized? Is diagnostics default-hidden? Are screenshots free of sensitive data?
- **Escalation path**: document-in-feature if browser fixture/setup cost grows.
- **Active feature PR close-out entry**: Smoke Coverage.
- **Why no dedicated follow-up spec is needed**: this is a single strategic surface; broader pattern library/governance inbox/operations/evidence surfaces are separate follow-up specs.
## Project Structure
### Documentation (this feature)
```text
specs/326-customer-review-workspace-v1-productization/
├── spec.md
├── plan.md
├── tasks.md
├── repo-truth-map.md
├── checklists/
│ └── requirements.md
└── artifacts/
└── screenshots/
```
### Source Code (repository root)
Expected implementation touch points:
```text
apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php
apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php
apps/platform/tests/Feature/Reviews/
apps/platform/tests/Feature/Navigation/
apps/platform/tests/Browser/Spec326CustomerReviewWorkspaceProductizationSmokeTest.php
```
Supporting surfaces to inspect but not broadly redesign:
```text
apps/platform/app/Filament/Resources/EnvironmentReviewResource.php
apps/platform/app/Filament/Resources/ReviewPackResource.php
apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php
apps/platform/app/Filament/Resources/FindingExceptionResource.php
apps/platform/app/Filament/Resources/StoredReportResource.php
apps/platform/app/Models/EnvironmentReview.php
apps/platform/app/Models/ReviewPack.php
apps/platform/app/Models/EvidenceSnapshot.php
apps/platform/app/Models/FindingException.php
apps/platform/app/Models/StoredReport.php
apps/platform/app/Models/OperationRun.php
apps/platform/app/Support/Navigation/WorkspaceHubEnvironmentFilter.php
apps/platform/app/Support/Navigation/WorkspaceHubFilterStateResetter.php
apps/platform/app/Filament/Concerns/ClearsWorkspaceHubEnvironmentFilterState.php
```
## Phase 0: Discovery Completed During Preparation
- Current route exists: `GET|HEAD admin/reviews/workspace`.
- Current page class exists: `CustomerReviewWorkspace`.
- Current Blade view exists and already uses Filament sections, badges, environment filter chip, latest released review, decision summary, accepted risks, evidence basis, and table.
- Existing tests cover Customer Review Workspace page, pack access, authorization, navigation context, hub contract, and browser smoke.
- Existing workspace hub filter support uses canonical `environment_id`, cleans legacy keys, and resolves environment within the current workspace/actor entitlement.
- Related resources (`EnvironmentReviewResource`, `ReviewPackResource`, `EvidenceSnapshotResource`, `FindingExceptionResource`, `StoredReportResource`) have global search disabled.
- Panel providers are registered in `apps/platform/bootstrap/providers.php`.
- Spec 312 and Specs 314-325 are historical dependencies; they are not modified.
## Phase 1: Repo Truth Map
Create/update `repo-truth-map.md` before runtime edits and keep it aligned during implementation. Any UI element with no repo-backed source must become unavailable/empty/deferred in the runtime surface.
## Phase 2: Page Skeleton Productization
Refactor the page around:
- Header / scope area.
- Main decision card.
- Readiness summary cards.
- Evidence path panel.
- Review pack panel.
- Accepted risk summary.
- Customer-safe findings/follow-ups.
- Collapsed diagnostics.
Use existing Filament layout primitives. Avoid new asset registration and CSS-heavy custom systems.
## Phase 3: Data Binding
Bind each section to repo-verified sources:
- `EnvironmentReview` for released review and summary.
- `ReviewPack` / `ReviewPackStatus` / `ReviewPackService` for pack state and download.
- `EvidenceSnapshot` and review summary completeness for evidence freshness/path.
- `FindingException` and review package accepted-risk summary for accepted risks.
- `Finding` / decision-summary entries for customer-safe follow-ups where supported.
- `OperationRun` relations only as secondary proof links where already present and authorized.
If data is missing, show explicit unavailable/empty state.
## Phase 4: Actions
Only include repo-real and authorized actions:
- Open review pack / download review pack.
- Open latest review.
- Open evidence snapshot if route/auth exists and is customer-safe.
- Review accepted risks / open queue if existing and authorized.
- Open operation proof if existing and authorized.
- Clear environment filter.
- Open diagnostics only if authorized and collapsed by default.
Do not introduce generation, refresh, publish, expire, revoke, restore, or other customer-impacting mutation actions on the default customer-safe view.
## Phase 5: Scope / Filter Integration
Verify clean workspace entry, filtered `environment_id` entry, visible chip, clear filter, reload safety, legacy alias rejection, and cross-workspace guard. Keep shell Workspace-owned.
## Phase 6: Browser Verification
Run targeted browser verification for clean entry, filtered entry, clear/reload, diagnostics default-hidden, evidence path/review-pack/accepted-risk visibility or explicit unavailable state, and light mode readability where supported.
Screenshots may be saved under:
```text
specs/326-customer-review-workspace-v1-productization/artifacts/screenshots/
```
## Follow-up Plan: Premium Layout Alignment
The follow-up remains inside Spec 326 and narrows to the existing Customer Review Workspace runtime surface.
- Compact the customer-safe header copy and quiet the non-certification disclosure.
- Recompose the Blade view into a main/aside workbench using a responsive `xl:grid-cols-[minmax(0,1fr)_22rem]` layout.
- Keep the main column focused on the decision card, readiness/evidence/review-pack/accepted-risk state cards, customer-safe follow-ups, and the secondary review package table.
- Move evidence path, review-pack state, accepted-risk counts/records, disclosure rules, and collapsed diagnostics into the right aside.
- Preserve existing page-local truth helpers and add only derived display payloads for the aside; no new persisted truth, services, routes, actions, or assets.
- Extend tests for premium layout copy, right-aside sections, collapsed diagnostics, hidden raw diagnostics, and absence of platform-context `tenant` copy.
- Capture `customer-review-workspace-premium-layout.png` in the existing Spec 326 screenshot artifact directory.
## Complexity Tracking
No migrations, no seeders, no packages, no env vars, no queues/scheduler/storage changes, no deployment asset changes, and no backwards compatibility layer are expected.
If implementation proves otherwise, stop, update `spec.md` and this plan, and re-run preparation review before coding the expanded scope.

View File

@ -0,0 +1,83 @@
# Spec 326 Repo Truth Map
Status: implementation aligned
Created: 2026-05-18
Purpose: classify each Customer Review Workspace runtime element before and during implementation. This map is based on repository inspection and the Spec 326 runtime diff.
Implementation update: Spec 326 productizes the existing `CustomerReviewWorkspace` page with page-local derived payloads only. The premium layout follow-up keeps the same scope and recomposes the existing UI into a compact main/aside workbench. No migration, package, env var, queue, scheduler, storage disk, deployment asset, public portal, external auth, review engine, evidence engine, review-pack engine, or legacy query alias support was added.
## Classification Legend
- `repo-verified`: exact runtime source exists and was inspected.
- `foundation-real`: backend model/service/policy exists, but exact page binding still needs implementation verification.
- `derived from existing model`: display value can be derived from existing persisted/domain truth.
- `empty state / unavailable`: no safe source/action exists for v1; show explicit unavailable or omit.
- `deferred future capability`: outside Spec 326 and must not be shown as live runtime truth.
## Data Area Map
Required data areas preserved from preparation review: Tenant Reviews / Environment Reviews, Evidence Snapshots, Review Packs / exports, Accepted Risks / Risk Exceptions, Findings / Finding Exceptions, OperationRuns, Workspace entitlements/capabilities, Audit log.
| UI element | Source model/service/page | Status source | Authorization / capability | Workspace / Environment scope | OperationRun / audit link | Fallback / empty state | Classification |
|---|---|---|---|---|---|---|---|
| Customer Review Workspace route | `CustomerReviewWorkspace`, route `admin/reviews/workspace` | Filament page slug `reviews/workspace` | `EnvironmentReviewRegisterService::canAccessWorkspace()` plus authorized environments | Workspace session via `WorkspaceContext`; optional page filter | `WorkspaceAuditLogger` logs `CustomerReviewWorkspaceOpened` | 404 if no workspace/access/authorized environments | repo-verified |
| Header title and customer-safe mode | `CustomerReviewWorkspace::getTitle()`, Blade view | Localization keys under `localization.review.*` | Page access authorization | Workspace-wide unless `environment_id` filter present | Page-open audit only | Static customer-safe disclosure | repo-verified |
| Environment filter chip | `environmentFilterChip()`, `filament.partials.workspace-hub-environment-filter-chip` | `WorkspaceHubEnvironmentFilter`, table filter state | Environment resolved inside current workspace and actor entitlement | `?environment_id={id}` only | Audit metadata includes `tenant_filter_id` | no chip on clean URL | repo-verified |
| Clear environment filter | `clearWorkspaceFilters()`, `ClearsWorkspaceHubEnvironmentFilterState`, `WorkspaceHubFilterStateResetter` | clean URL via `WorkspaceHubRegistry::cleanUrl()` | Page access auth | clears canonical and session/table filter state | no OperationRun | clean workspace-wide URL | repo-verified |
| Legacy alias rejection | `WorkspaceHubRegistry::forbiddenQueryKeys()` and resetter | forbidden keys include `tenant`, `tenant_id`, `managed_environment_id`, `environment_id`, `environment`, `tenant_scope`, `tableFilters`; canonical `environment_id` is preserved only when explicit | page access plus environment resolver | legacy aliases neutralized; canonical filter scoped | no OperationRun | no filter state or safe 404 | repo-verified |
| Cross-workspace environment guard | `WorkspaceHubEnvironmentFilter::fromRequest()` | environment lookup constrained by `workspace_id` | `User::canAccessTenant()` | current workspace only | no OperationRun | `NotFoundHttpException` | repo-verified |
| Latest released review | `EnvironmentReview`, `EnvironmentReviewRegisterService::latestPublishedQuery()` | `EnvironmentReviewStatus::Published`, `published_at`, `generated_at`, `id` | `EnvironmentReviewRegisterService` authorized tenant query and policies on handoff routes | current workspace and optional environment filter | `EnvironmentReview::operationRun()` relation exists; not default raw | no active/released review empty state | repo-verified |
| Main decision card | `latestReviewConsumptionPayload()`, `reviewReadinessForTenant()` | published review, package availability, evidence/decision summary | page access plus environment entitlement; pack action gated by `Capabilities::REVIEW_PACK_VIEW` | workspace or canonical `environment_id` filter | no new OperationRun | follow-up required/open latest review when pack unavailable | repo-verified |
| Main readiness state | current `latestReviewStateLabel()`, `workspaceReviewNeedsAttention()` and package/evidence helpers | derived from published review, package availability, evidence/decision summary | page access plus environment entitlement | workspace or environment filter | no new OperationRun | no active review / follow-up required | repo-verified |
| Readiness reason and impact | review summary, governance package decision/evidence state, ReviewPack availability | `EnvironmentReview.summary`, `governance_package.decision_summary`, `ReviewPackStatus` | same as review/pack access | workspace/environment scoped | no new OperationRun | customer-safe follow-up copy when source is unavailable | repo-verified |
| Primary next action | `reviewPackDownloadUrl()`, `latestReviewUrl()` | ready downloadable pack vs latest review URL | `Capabilities::REVIEW_PACK_VIEW`, Environment Review view capability via resource route | environment-bound review/pack | ReviewPack service may include source metadata; no run start | open review or unavailable | repo-verified |
| Readiness summary cards | `readinessDimensionPayloads()` | review readiness, evidence state, accepted-risk state, review-pack availability | same as source section | workspace/environment scoped | no new OperationRun | unavailable/not applicable per card | repo-verified |
| Evidence snapshot availability | `EvidenceSnapshot`, `EnvironmentReview::evidenceSnapshot()` | `EvidenceSnapshotStatus`, `completeness_state`, `generated_at`, `expires_at`, review summary | `EvidenceSnapshotPolicy` / `Capabilities::EVIDENCE_VIEW` for detail link | managed environment and workspace | `EvidenceSnapshot::operationRun()` relation exists | evidence unavailable/not generated/stale if unsupported | foundation-real |
| Evidence freshness/staleness | `EvidenceSnapshot` fields and review summary completeness | `generated_at`, `expires_at`, `EvidenceCompletenessState`, review summary | evidence view capability where linking | managed environment and workspace | operation relation exists | explicit unavailable if no reliable freshness | derived from existing model |
| Evidence path panel | `evidencePathForReview()` over existing review/evidence/pack/operation relations | per-item availability states | source-specific policies/capabilities; evidence link checks `Capabilities::EVIDENCE_VIEW` | workspace/environment scoped | existing `OperationRun` relations only | unavailable/not applicable rows | repo-verified |
| Review pack status | `ReviewPack`, `ReviewPackStatus`, `currentExportReviewPack` | queued/generating/ready/failed/expired/file path/expiry | `ReviewPackPolicy`, `Capabilities::REVIEW_PACK_VIEW` for open/download | managed environment/workspace | `ReviewPack::operationRun()` relation exists | not generated/preparing/unavailable/expired | repo-verified |
| Review pack download URL | `ReviewPackService::generateDownloadUrl()` | ready status, file path/disk, not expired | `Capabilities::REVIEW_PACK_VIEW` | managed environment/workspace | source metadata only; no run start | no URL if unauthorized/unavailable | repo-verified |
| Review pack generation action | existing Review Pack resource/job may support generation | `GenerateReviewPackJob`, `ReviewPackResource` | manage capability required | environment-owned resource | OperationRun-backed generation may exist | do not show in default customer-safe surface | empty state / unavailable |
| Accepted risk summary | `FindingException` model and `governance_package.accepted_risks` in review summary | `status`, `current_validity_state`, `review_due_at`, accepted-risk summary entries | page consumes released-review summary without raw internal approval detail | managed environment/workspace | decisions/audit may exist in related workflow | no accepted risks recorded / unavailable | repo-verified |
| Expiring/expired/pending accepted-risk counts | `FindingException` fields | `current_validity_state`, `status`, `expires_at`, `review_due_at` | finding exception view capability or released-review summary | managed environment/workspace | related decisions/audit only if linked | show unavailable if not safely derivable | derived from existing model |
| Customer-safe follow-ups | `customerSafeFollowUpsForReview()` over governance-package decision-summary entries | title, summary, next action where present; proof label from decision trail | released-review customer-safe summary only | managed environment/workspace | no new OperationRun | explicit no-follow-ups state | repo-verified |
| Decision trail | review `governance_package.decision_summary` | decision summary status/entries | released-review safe summary | managed environment/workspace | audit may exist on decisions | unavailable/fallback copy if no decision summary | repo-verified |
| Operation proof | `EnvironmentReview::operationRun`, `EvidenceSnapshot::operationRun`, `ReviewPack::operationRun` | existing run relation presence | existing `OperationRunLinks` handoff only when a run is linked | workspace and managed environment entitlement | existing OperationRun only | proof unavailable if no relation | repo-verified |
| Stored report / export artifact proof | `ReviewPack`, review/export links | review-pack ready/download URL state | review-pack view capability through existing download route | managed environment/workspace | may relate to operation/audit if linked | unavailable unless current pack download is ready | repo-verified |
| Diagnostics disclosure | `diagnosticsDisclosureForReview()` | safe explanatory disclosure only; no raw metadata rendered | no diagnostic payload/action exposed in customer-safe default | workspace/environment scoped | may link to OperationRun/support diagnostics in future specs only | collapsed by default; raw/support details absent | repo-verified |
| Raw payload / provider diagnostics | raw summary payloads, provider errors, Graph data | not safe default source | support-only if ever exposed | N/A for customer default | N/A | never default-visible | deferred future capability |
| Workspace entitlements/capabilities | `CapabilityResolver`, `WorkspaceCapabilityResolver`, policies | capability strings in `Capabilities` | existing policy/capability calls | workspace and managed environment | audit for access/mutations as existing | hidden/unavailable actions | repo-verified |
| Audit page open | `WorkspaceAuditLogger`, `AuditActionId::CustomerReviewWorkspaceOpened` | page-open event metadata | page access auth | workspace resource id | audit log entry | skip only if no user/workspace | repo-verified |
## Required Runtime Element Decisions
| Element | v1 decision |
|---|---|
| New external customer portal | deferred future capability; do not build |
| Public share/invite/email delivery | deferred future capability; do not show |
| Review generation engine | existing backend only; no new engine |
| Evidence refresh action | show only if existing route/action/capability is verified and safe; otherwise unavailable |
| Review pack generation/regeneration | do not show on customer-safe default surface |
| Diagnostics | collapsed/secondary and authorized only; default hidden |
| Green/success state | allowed only when repo-backed proof supports the exact statement |
| Legacy query aliases | rejected/neutralized; do not support |
## Implemented Surface Classification
| Runtime section | Implemented source | Final classification | Notes |
|---|---|---|---|
| Scope and shell context | existing workspace session, canonical `environment_id`, chip partial | repo-verified | Clean entry stays workspace-wide; filtered entry remains Workspace shell with visible chip. |
| Decision-first card | page-local payload from released review, package availability, evidence/follow-up helpers | repo-verified | Shows ready/follow-up state, reason, impact, and one primary repo-real action. |
| Readiness dimensions | released review, evidence state, accepted-risk summary, review-pack state | repo-verified | Uses derived display labels only; no new persisted state family. |
| Evidence path | evidence snapshot, review pack, decision trail, accepted-risk records, OperationRun relation, export artifact | repo-verified | Missing sources render unavailable states instead of success claims. |
| Review pack panel | `ReviewPackStatus`, generated timestamp, evidence snapshot timestamp, download URL, operation relation | repo-verified | Download appears only when existing pack/view capability and ready artifact support it. |
| Right-side evidence path | evidence snapshot, review pack, decision trail, existing OperationRun relation | repo-verified | Aside rows show proof state only; actions stay in the main decision card or existing detail routes. |
| Accepted-risk aside | released review `governance_package.accepted_risks` and `governance_package.governance_decisions` | repo-verified | Counts and records derive from existing review-package arrays; no live metric or new status family is introduced. |
| Disclosure rule aside | customer-safe page disclosure policy from Spec 326 | repo-verified | Decision and evidence are visible, diagnostics are collapsed, and raw/support detail is hidden by default. |
| Customer-safe follow-ups | governance-package decision summary entries | repo-verified | Owner/due fields are not invented; absent data becomes no-follow-ups copy. |
| Diagnostics | collapsed `<details>` with safe disclosure copy | repo-verified | Raw payloads, provider secrets, stack traces, fingerprints, and internal exception text are not rendered by default. |
| Secondary table | existing Filament table over authorized latest published reviews | repo-verified | Kept as secondary context; no Graph calls added. |
## Implementation Update Rule
If implementation discovers that a planned UI element has no safe source, no authorization path, or would require new persisted truth, the element must become `empty state / unavailable` or `deferred future capability`. Do not create backend foundation inside Spec 326 without updating `spec.md`, `plan.md`, and this map first.

View File

@ -0,0 +1,628 @@
# Feature Specification: Spec 326 - Customer Review Workspace v1 Productization
**Feature Branch**: `326-customer-review-workspace-v1-productization`
**Created**: 2026-05-18
**Status**: Implemented
**Type**: Runtime UI productization / customer-safe review consumption / decision-first surface
**Runtime posture**: Narrow runtime UI implementation. Repo-based. No invented backend foundation.
**Input**: User-provided full Spec 326 draft, corrected from an earlier Pattern Library assumption.
## Dependencies And Historical Context
Depends on:
- Spec 312 - Customer Review Workspace v1 Completion, treated as completed/historical runtime foundation and not rewritten.
- Spec 314 - Workspace Hub Navigation Context Contract.
- Spec 315 - Environment CTA Explicit Filter Contract.
- Spec 316 - Workspace Hub Clear Filter Contract.
- Spec 317 - Legacy Tenant / Environment Context Cleanup.
- Spec 318 - Admin Surface Scope & Shell Context Audit.
- Spec 319 - Environment-Owned Surface Routing & Shell Context Contract.
- Spec 320 - Workspace-Owned Analysis Surface Registration & Shell Cutover.
- Spec 321 - Alerts / Audit Log Environment Filter Contract Decision.
- Spec 322 - Browser No-Drift Regression Guard.
- Spec 325 - Screenshot-Anchored Strategic Target Images.
Repo truth adjustment: the user draft intentionally starts from Spec 325 target direction, but the repository already has Spec 312 and runtime Customer Review Workspace work. Spec 326 must build on that existing page and tests instead of restating it as greenfield. Spec 325 premium references are visual calibration only; they are not runtime truth for metrics, actions, states, RBAC, audit, OperationRun, workspace/environment scope, evidence, or review-pack behavior.
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
- **Problem**: Customer Review Workspace is repo-real and already customer-safe in places, but it still needs a v1 productization pass that makes readiness, evidence path, review-pack state, accepted risks, follow-ups, and hidden diagnostics feel like one polished customer-safe review workspace.
- **Today's failure**: A reviewer can see current review and review-pack facts, but the first viewport may still read as an admin list plus panels rather than a decision-first answer to "Is this ready to share, what needs attention, and what proof backs it?" Spec 325 target visuals are not yet implemented as a repo-truth-bounded runtime surface.
- **User-visible improvement**: Customer reviewers, auditors, account managers, and MSP operators get a polished, read-first workspace surface that clearly separates readiness, evidence, accepted risks, review-pack availability, follow-ups, and diagnostics.
- **Smallest enterprise-capable version**: Productize only `CustomerReviewWorkspace` and its existing Blade view, using existing models/services/policies and optional `?environment_id=` filter. Add no portal, migrations, new backend engines, new domain states, new packages, or new external sharing.
- **Explicit non-goals**: No external customer portal, external auth, invitation links, email delivery, PSA handoff, new review engine, new evidence backend, new review-pack backend, AI summarization, billing/entitlement rebuild, broad review-page redesign, Governance Inbox redesign, Evidence Overview redesign, Operations Hub redesign, migrations, packages, env vars, queues, scheduler, storage, or deployment asset changes unless repo analysis proves a narrow exception.
- **Permanent complexity imported**: Feature-local page composition, feature tests, browser smoke coverage, and a repo-truth map. No new persisted truth, public abstraction, enum/status family, cross-domain UI framework, or backend service foundation.
- **Why now**: Spec 325 produced screenshot-anchored target direction for strategic surfaces; Customer Review Workspace is the first high-value runtime productization lane and the clearest sellability surface. Specs 314-322 stabilized workspace/environment context contracts.
- **Why not local**: A label-only patch would not solve the first-read review consumption problem. A portal or framework would overbuild. The narrow correct slice is a repo-truth-bounded productization pass on the existing workspace page.
- **Approval class**: Workflow Compression.
- **Red flags triggered**: Cross-surface customer-safe review presentation and visual productization. Defense: scope is bounded to one existing page, existing truth sources, existing policies, existing routes, and explicit no-new-backend constraints.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 12/12**
- **Decision**: approve.
## Candidate Source And Completed-Spec Guardrail
- **Candidate source**: Direct user-provided manual promotion for Spec 326, aligned with the P1 Customer Review Workspace v1 Completion lane in `docs/product/spec-candidates.md` and the Customer Review Workspace target brief from Spec 325.
- **Completed-spec check**: No `specs/326-*` package existed before generation. Related Specs 312 and 314-325 contain completed/historical preparation or implementation signals and must remain unchanged by this preparation.
- **Close alternatives deferred**: Governance Inbox Decision-First Workbench, Operations Hub Decision-First Workbench, Evidence/Audit disclosure, Environment Dashboard/Baseline Compare, and Restore Safety Workflow productization are follow-up specs 327-331, not hidden scope.
- **Smallest viable implementation slice**: Existing Customer Review Workspace only, including layout, derived display payloads, RBAC-aware actions, environment filter behavior, hidden diagnostics, and targeted tests/browser smoke.
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace canonical-view customer-safe review consumption hub, optionally filtered by canonical `environment_id`.
- **Primary Routes**:
- Existing route: `/admin/reviews/workspace`.
- Existing page class: `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`.
- Existing view: `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`.
- **Data Ownership**:
- Review truth: `EnvironmentReview`, `EnvironmentReviewStatus::Published`, `published_at`, `summary`, `current_export_review_pack_id`.
- Evidence truth: `EvidenceSnapshot`, `EvidenceSnapshotItem`, review summary evidence/completeness fields.
- Review Pack truth: `ReviewPack`, `ReviewPackStatus`, file path/disk, expiry, `ReviewPackService` download URL.
- Accepted risk truth: `FindingException` / review governance-package accepted-risk summary.
- Finding/follow-up truth: `Finding`, `FindingException`, review decision-summary entries where repo-supported.
- Execution proof: existing `OperationRun` relations only as secondary proof links when already present and authorized.
- Audit truth: `AuditLog` and existing `WorkspaceAuditLogger` page-open event.
- **RBAC**:
- Workspace membership required.
- Managed-environment entitlement required where environment data is rendered.
- Existing capabilities and policies remain authoritative for review, evidence, review-pack, accepted-risk, findings, export/download, and diagnostics visibility.
- Non-member or cross-workspace environment access remains deny-as-not-found.
- Member with missing capability receives existing policy semantics; unauthorized actions must be hidden or unavailable without leaking sensitive details.
For canonical-view specs:
- **Default filter behavior when tenant-context is active**: Clean `/admin/reviews/workspace` remains workspace-wide and must not inherit remembered environment context, Filament tenant context, session table filters, or legacy query aliases.
- **Explicit entitlement checks preventing cross-tenant leakage**: `?environment_id=` must resolve through the current workspace and actor entitlement; cross-workspace or inaccessible IDs return 404/no-access.
## 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
- [x] Customer-facing surface changed
- [ ] Dangerous action changed
- [x] Status/evidence/review presentation changed
- [x] Workspace/environment context presentation changed
## UI/Productization Coverage *(mandatory when UI Surface Impact is not "No UI surface impact"; otherwise write `N/A - no reachable UI surface impact` plus rationale)*
- **Route/page/surface**: `/admin/reviews/workspace`, `CustomerReviewWorkspace`, customer review workspace Blade view.
- **Current or new page archetype**: Customer Workspace / Strategic Surface, matching `docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md`.
- **Design depth**: Strategic Surface.
- **Repo-truth level**: repo-verified page and foundational models; individual target visual elements must be classified in `repo-truth-map.md`.
- **Existing pattern reused**: Filament Page, Filament Sections, Filament table, badges, existing workspace hub environment filter chip, existing review-pack download route, existing page report and target brief.
- **New pattern required**: no new runtime pattern framework; page-local composition only.
- **Screenshot required**: yes, browser smoke screenshots under `specs/326-customer-review-workspace-v1-productization/artifacts/screenshots/`.
- **Page audit required**: no new full audit unless implementation materially changes the route inventory; update coverage registry or record checked no-impact for docs if the implementation remains on the same route.
- **Customer-safe review required**: yes. Raw diagnostics and internal metadata are hidden by default.
- **Dangerous-action review required**: no new dangerous action; verify no destructive/generation/regeneration/publish actions are introduced on the customer-safe default surface.
- **Coverage files updated or explicitly not needed**:
- [ ] `docs/ui-ux-enterprise-audit/route-inventory.md`
- [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`
- [ ] `docs/ui-ux-enterprise-audit/page-reports/...`
- [ ] `docs/ui-ux-enterprise-audit/strategic-surfaces.md`
- [ ] `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md`
- [ ] `docs/ui-ux-enterprise-audit/unresolved-pages.md`
- [ ] `N/A - no reachable UI surface impact`
- **Coverage decision for implementation**: implementation must either update the UI coverage registry for the material surface change or document in this spec's close-out why the existing UI-006 report and Spec 325 target artifacts remain sufficient.
- **Implementation close-out coverage decision**: no separate UI coverage registry update is required for Spec 326. The reachable route, navigation entry, panel/provider registration, page archetype, and strategic-surface identity remain the existing UI-006 Customer Review Workspace surface. Spec 326 changes the runtime composition on that route and records proof in this active spec package, `repo-truth-map.md`, tests, and screenshot artifacts; Spec 325 remains visual calibration, not runtime truth.
## Cross-Cutting / Shared Pattern Reuse *(mandatory)*
- **Cross-cutting feature?**: yes.
- **Interaction class(es)**: customer-safe review consumption, readiness/status messaging, action links, evidence/report viewers, review-pack state, accepted-risk summaries, diagnostics disclosure.
- **Systems touched**: `CustomerReviewWorkspace`, `customer-review-workspace.blade.php`, `EnvironmentReviewRegisterService`, `ReviewPackService`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, existing review/evidence/pack/exception resources and policies.
- **Existing pattern(s) to extend**: existing Customer Review Workspace payload helpers, workspace hub filter chip partial, `ArtifactTruthPresenter` where already used, `ReviewPackStatus`, `EnvironmentReviewStatus`, `EvidenceSnapshotStatus`, `FindingException` status/validity truth, existing localization structure, existing audit logger.
- **Shared contract / presenter / builder / renderer to reuse**: reuse existing services and enums; do not introduce a new customer-review presenter framework unless implementation proves the current page-local helpers cannot stay bounded.
- **Why the existing shared path is sufficient or insufficient**: Existing paths provide truth and authorization, but current composition still needs decision-first hierarchy and customer-safe disclosure polish. A page-local derived payload is sufficient for v1.
- **Allowed deviation and why**: bounded page-local layout/view helper changes are allowed. New shared UI/status frameworks are not allowed.
- **Consistency impact**: Labels, badges, actions, and links must stay aligned with existing review, evidence, review-pack, and operation vocabulary. No alternate status taxonomy may become product truth.
- **Review focus**: Verify no fake metrics, no false green, no raw diagnostics by default, no unauthorized actions, no shell-scope regression, and no duplicate local semantics replacing existing truth.
## OperationRun UX Impact *(mandatory)*
- **Touches OperationRun start/completion/link UX?**: secondary link semantics only if repo-supported; no new OperationRun creation, queueing, completion, dedupe, or lifecycle behavior.
- **Shared OperationRun UX contract/layer reused**: existing operation links/support surfaces only; if an operation proof link is shown, it must use existing `OperationRunLinks` or existing resource routes and authorization.
- **Delegated start/completion UX behaviors**: N/A - no operation start.
- **Local surface-owned behavior that remains**: show proof availability/unavailability in the evidence path only.
- **Queued DB-notification policy**: N/A.
- **Terminal notification path**: unchanged.
- **Exception required?**: none.
## Provider Boundary / Platform Core Check *(mandatory)*
- **Shared provider/platform boundary touched?**: no new provider seam; page consumes existing TenantPilot review/evidence truth.
- **Boundary classification**: platform-core customer review consumption over existing tenant-managed environment artifacts.
- **Seams affected**: display of review, evidence, review-pack, accepted-risk, finding/follow-up, and operation proof links.
- **Neutral platform terms preserved or introduced**: workspace, environment filter, customer review, readiness, evidence path, review pack, accepted risk, decision trail, operation proof.
- **Provider-specific semantics retained and why**: existing Intune/Microsoft terms may appear only inside already customer-safe review/evidence content. Do not surface raw provider IDs, Graph payloads, tenant aliases, or provider diagnostics by default.
- **Why this does not deepen provider coupling accidentally**: no Graph calls, no provider connection changes, no provider taxonomy changes, and no new persisted provider-shaped terms.
- **Follow-up path**: provider-readiness and environment-dashboard productization remain separate target lanes.
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed)*
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|---|---:|---|---|---|---:|---|
| Customer Review Workspace page | yes | Native Filament page plus existing Blade composition | customer-safe review, evidence/report, review-pack, status messaging | page, URL-query, table state | no | Existing route only |
| Header / scope area | yes | Filament section / Blade | workspace/environment context presentation | page, URL-query | no | Must keep Workspace shell ownership |
| Main decision card | yes | Filament section/cards/badges | status/readiness messaging | page payload | no | Derived labels only, no new persisted state |
| Evidence path / review-pack / accepted-risk panels | yes | Filament sections/cards/badges | evidence/report viewer, artifact delivery | page payload | no | Raw diagnostics hidden |
| Customer-safe findings/follow-ups | yes | Filament table/list/card pattern | decision/finding follow-up | page payload | no | Use existing decision summary/finding exception truth where repo-supported |
| Diagnostics disclosure | yes | collapsed/progressive disclosure only | support/raw detail | page payload/action visibility | no | Authorized and collapsed by default |
## 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 |
|---|---|---|---|---|---|---|---|
| Customer Review Workspace | Primary Decision Surface for customer-safe review consumption | Reviewer decides whether a review can be shared or needs follow-up | readiness state, reason, impact, evidence freshness, accepted-risk state, review-pack availability, one next action | review detail, evidence snapshot, review pack, decision trail, operation proof, diagnostics | Primary because it is the customer-safe hub | Review consumption, not raw admin operations | Removes cross-page reconstruction |
| Evidence path panel | Tertiary Evidence / Diagnostics | Reviewer checks proof path after the primary decision | evidence snapshot, review pack, decision trail, accepted-risk records, operation proof availability | raw/support details only behind disclosure and authorization | Secondary proof area, not primary decision | Evidence-first review trust | Separates proof from payloads |
| Review-pack panel | Secondary Context / Artifact delivery | Reviewer decides whether export/share artifact is usable | pack state, last generated, snapshot used, export availability | operation proof if already linked | Artifact owner remains existing pack truth | Export-aware review consumption | Avoids guessing artifact readiness |
## 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 |
|---|---|---|---|---|---|---|---|
| Customer Review Workspace | customer-read-only, MSP operator, auditor/account manager | readiness, reason, impact, evidence freshness, review-pack state, accepted-risk summary, safe follow-ups | collapsed and secondary if present | raw payloads, provider diagnostics, fingerprints, internal exception traces, raw OperationRun payloads | open/download review pack when ready; otherwise open review or follow the repo-real next action | raw diagnostics, generation/publish/destructive actions, internal metadata | main card states status once; panels add proof/source |
| Diagnostics disclosure | operator/support only where authorized | unavailable/collapsed indicator only | run metadata, export artifact detail, raw metadata if authorized | raw/support details behind explicit disclosure | view proof or close disclosure | default hidden | no diagnostic text in default decision card |
## 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 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Customer Review Workspace | Workbench / Review Workspace | Customer-safe decision-first hub | Open/download review pack or open review | Explicit primary CTA | no fake row-click | right-side/contextual panels and neutral links | none | `/admin/reviews/workspace` | existing review/evidence/pack routes | workspace plus optional environment chip | Customer reviews | readiness, evidence, review pack, accepted risks, follow-ups | none |
| Evidence path panel | Detail / Evidence | Inspect proof path | contextual proof links only when authorized | explicit links | N/A | panel body | none | same page | existing evidence/operation routes if available | source/proof labels | Evidence path | available/unavailable/stale/requires refresh | none |
| Diagnostics disclosure | Diagnostics / Support Raw | Inspect internal proof after explicit open | disclosure component | N/A | collapsed area | none | same page | existing operation/report routes if available | authorized-only label | Diagnostics | collapsed status only | 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 |
|---|---|---|---|---|---|---|---|---|---|---|
| Customer Review Workspace | Customer reviewer / auditor / MSP operator | Consume review and decide share/follow-up readiness | Workspace review hub | Is this review ready to share, what needs attention, and what evidence backs it? | readiness state, reason, impact, evidence freshness, accepted risks, review-pack state, follow-ups, scope | raw metadata, raw payloads, internal exception traces, operation diagnostics | review readiness, evidence freshness, accepted-risk review, export/share readiness, follow-up priority | read-only by default | open/download pack, open review, review accepted risks/evidence where existing and authorized, clear filter | none introduced |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no.
- **New persisted entity/table/artifact?**: no. `repo-truth-map.md` is a preparation artifact, not runtime truth.
- **New abstraction?**: no public abstraction. Page-local private helpers are allowed only when they reduce Blade complexity.
- **New enum/state/reason family?**: no domain state. Display states must be derived from existing `EnvironmentReviewStatus`, `EnvironmentReviewCompletenessState`, `ReviewPackStatus`, `EvidenceSnapshotStatus`, `FindingException` state/validity, and existing summary payloads.
- **New cross-domain UI framework/taxonomy?**: no.
- **Current operator problem**: The customer-safe workspace needs a decision-first runtime surface that consumes existing truth without making false readiness or export claims.
- **Existing structure is insufficient because**: Current page foundations do not fully match Spec 325's premium, right-side proof/detail direction and must classify each visible element as repo-backed or unavailable.
- **Narrowest correct implementation**: Refactor the existing page layout and derived payloads, bind to existing sources, hide diagnostics, and add targeted tests/browser smoke.
- **Ownership cost**: Feature-local layout/payload tests and browser smoke. No durable backend model or new framework cost.
- **Alternative intentionally rejected**: new portal, new review-pack engine, new evidence backend, new readiness status machine, broad design-system implementation, or cross-surface pattern library.
- **Release truth**: current-release runtime UI productization over existing review/evidence/review-pack foundations.
### Compatibility posture
This feature assumes pre-production runtime posture. Backward compatibility, historical aliases, migration shims, dual-write logic, and legacy customer portal routes are out of scope. Existing legacy query aliases (`tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, `tableFilters`) must not be supported for Customer Review Workspace filtering.
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
- **Test purpose / classification**: Feature, Filament/Livewire, Browser. Existing related Unit coverage may remain unchanged unless implementation touches pure helpers.
- **Validation lane(s)**: confidence plus browser for critical customer-safe UI/scope smoke.
- **Why this classification and these lanes are sufficient**: The change is a user-facing Filament/Livewire page productization with RBAC, scope, and disclosure behavior. Feature tests prove data/scope/action rules; browser smoke proves shell/filter/reload/disclosure layout behavior.
- **New or expanded test families**: `tests/Feature/Reviews/*` additions and `tests/Browser/Spec326CustomerReviewWorkspaceProductizationSmokeTest.php`.
- **Fixture / helper cost impact**: reuse existing review/evidence/pack/finding helpers in `tests/Pest.php`; do not widen expensive defaults.
- **Heavy-family visibility / justification**: browser addition is explicit and named for Spec 326.
- **Special surface test profile**: `global-context-shell` plus customer-safe disclosure.
- **Standard-native relief or required special coverage**: special coverage required for environment filter, clear/reload, diagnostics default-hidden, and browser screenshot artifacts.
- **Reviewer handoff**: confirm hidden raw diagnostics, RBAC action hiding, no false green, workspace-wide clean entry, canonical filter, clear filter, and cross-workspace guard.
- **Budget / baseline / trend impact**: no expected material lane-cost shift beyond one targeted browser smoke.
- **Escalation needed**: document-in-feature if browser coverage becomes too expensive or requires fixture broadening.
- **Active feature PR close-out entry**: Smoke Coverage.
- **Planned validation commands**:
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Reviews tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec326CustomerReviewWorkspaceProductizationSmokeTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail artisan test --filter='CustomerReviewWorkspace|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact`
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
- `git diff --check`
## Summary
Productize the existing Customer Review Workspace into a customer-safe, decision-first review consumption surface.
The page must answer:
```text
Is this review ready to share, what needs attention, and what evidence backs it?
```
It must be customer-safe, read-only by default, decision-first, evidence-backed, review-pack aware, accepted-risk aware, scope-clear, export-aware, and diagnostics-secondary.
This spec must not create a public/external customer portal. It improves the existing workspace-scoped Customer Review Workspace surface.
## Product Context
TenantPilot is a governance-of-record platform. Its core value is turning tenant state, drift, reviews, evidence, accepted risks, and operation outcomes into understandable decisions and proof. Customer Review Workspace is a key sellability surface because it lets MSPs and customers consume review outcomes without navigating raw OperationRuns, logs, technical tables, or internal operator pages.
Spec 325 target direction calibrates the experience toward a premium enterprise shell, decision card first, visible evidence path, right-side detail/decision panel, customer-safe copy, raw diagnostics hidden by default, and a dense but calm product surface. Spec 325 images are not proof of real metrics/actions/states.
## Problem Statement
The current Customer Review Workspace is repo-real and already has strong foundations, but v1 productization remains incomplete until the default experience clearly frames readiness, evidence freshness, review-pack state, accepted risks, follow-ups, scope, and diagnostics disclosure.
Likely current gaps to verify during implementation:
- Review status may be visible but not framed as readiness with reason and impact.
- Evidence freshness/path may exist but not be first-class.
- Review Pack state may be present but not productized as export/share readiness.
- Accepted risks may be summarized but not with expiring/expired/pending/customer-safe follow-up clarity where repo-supported.
- Findings/follow-ups may still require interpretation from decision-summary or finding exception sources.
- Default content may not clearly separate customer-safe content from internal diagnostics.
- Scope between workspace-wide and environment-filtered view must remain explicit.
- Raw diagnostics must stay collapsed/secondary by default.
- No generic "Healthy", "Ready", "Complete", or green success state may be shown unless repo-backed proof supports it.
## Product Decision
Customer Review Workspace v1 is a workspace-scoped customer-safe review consumption hub.
It may be filtered by Environment only through:
```text
?environment_id={id}
```
When filtered:
- shell remains Workspace-only
- a visible Environment filter chip appears
- clear filter returns to the clean workspace-wide Customer Review Workspace
It is not an Environment-owned page, not a public customer portal, not a raw review admin table, and not a full GRC tool.
## Follow-up Premium Layout Alignment *(2026-05-18)*
Spec 326 remains the active implementation scope. The follow-up alignment sharpens the existing runtime UI against the Spec 325 premium target direction without creating a new spec or backend surface.
The implementation must:
- keep `/admin/reviews/workspace` as the only route in scope;
- replace the verbose intro with a compact customer-safe review package header;
- remove platform-context `tenant` wording from the Customer Review Workspace runtime copy;
- render a dense main/aside workbench layout, with the main column owning the review decision and the aside owning evidence, review-pack, accepted-risk, and disclosure proof;
- keep one dominant primary action in the main decision card;
- keep diagnostics collapsed and raw/support detail hidden by default;
- keep the existing table as secondary `Review package index` context;
- avoid migrations, models, packages, assets, auth/portal expansion, shell/sidebar changes, new metrics, false green claims, or new product features.
This follow-up does not change backend truth, RBAC semantics, OperationRun semantics, audit semantics, storage, queues, env vars, or deployment assets.
## Hard Rules
### Repo Truth
Every visible runtime element must be classified in `repo-truth-map.md` as one of:
- repo-verified
- foundation-real
- derived from existing model
- empty state / unavailable
- deferred future capability
Do not show a live metric/action unless a real repo source exists.
### Customer Safety
Default view must not expose raw diagnostic payloads, provider secrets, internal exception traces, raw OperationRun payloads, internal-only remediation details, debug metadata, or unfiltered tenant/provider IDs.
### Evidence First, But Not Raw First
Evidence must be visible as proof path:
- Evidence snapshot
- Review pack
- Decision trail
- Accepted risk record
- OperationRun proof
- Export artifact
Raw internals stay secondary.
### No False Green
Do not show generic "Healthy", "Ready", "Complete", or green success states unless repo-backed state proves it. Preferred labels include "Ready to share", "Needs evidence refresh", "Review pack unavailable", "Accepted risk review needed", "No active review", "Evidence unavailable", "Export not ready", and "Follow-up required".
### Workspace/Environment Contract
Preserve Specs 314-322:
- clean sidebar/global entry is workspace-wide
- Environment CTA entry uses `?environment_id=`
- visible chip when filtered
- clear filter is reload/session/table-safe
- no tenant aliases
- no remembered Environment shell
- no active Environment shell ownership
## User Scenarios & Testing *(mandatory)*
### User Story 1 - See review readiness first (Priority: P1)
As a customer reviewer or MSP operator, I want the first viewport to answer whether the review is ready to share so I can decide what needs attention without reading a raw table.
**Why this priority**: This is the core productization value.
**Independent Test**: Render Customer Review Workspace with repo-backed published review states and assert the main decision card shows readiness, reason, impact, and one next action without raw diagnostics.
**Acceptance Scenarios**:
1. **Given** a published review with a usable review pack and complete evidence basis, **When** the workspace renders, **Then** the main decision card states readiness only if repo-backed proof supports it.
2. **Given** stale/incomplete/missing evidence or missing pack truth, **When** the workspace renders, **Then** the main decision card shows follow-up or unavailable state instead of false success.
3. **Given** no active/published review, **When** the workspace renders, **Then** the page shows no active review/no released review state and no fake readiness.
### User Story 2 - Follow the evidence path without raw internals (Priority: P1)
As an auditor or customer reviewer, I want to see which evidence, review pack, decision trail, accepted risk records, and operation proof support the review so I can trust the conclusion.
**Why this priority**: Evidence-backed trust is TenantPilot's governance-of-record value.
**Independent Test**: Seed reviews with evidence snapshots, review packs, accepted risks, and operation relations where possible; assert proof path labels and links are shown only when authorized and raw payloads stay hidden.
**Acceptance Scenarios**:
1. **Given** evidence snapshot and review pack relations exist, **When** the page renders, **Then** evidence path and review pack sections show availability, freshness/staleness where repo-supported, and source links where authorized.
2. **Given** proof is unavailable or unsupported, **When** the page renders, **Then** the section shows explicit unavailable/not applicable state.
3. **Given** raw metadata or diagnostic payload exists in stored summaries, **When** the default page renders, **Then** raw internals are absent.
### User Story 3 - Understand accepted risks and follow-ups customer-safely (Priority: P1)
As a customer reviewer, I want accepted-risk and follow-up summaries in plain language so I know which decisions require attention without internal approval details.
**Why this priority**: Accepted risks are customer accountability records and must not be buried or overshared.
**Independent Test**: Seed current, expiring, expired, pending, and missing accepted-risk/follow-up situations where repo-supported; assert counts/states/customer-safe rows and absence of internal owner/debug details by default.
**Acceptance Scenarios**:
1. **Given** accepted risks in the released review scope, **When** the workspace renders, **Then** it shows count/state/summary/customer-safe review need.
2. **Given** expiring/expired/pending accepted risks where repo-supported, **When** the workspace renders, **Then** they are called out without internal debug details.
3. **Given** findings/follow-ups exist, **When** the workspace renders, **Then** rows show title, priority/severity if available, owner/due if available, proof state, and next action.
### User Story 4 - Preserve workspace and environment scope contracts (Priority: P1)
As an MSP operator, I want clean workspace entry and explicit environment-filtered entry to behave predictably so I do not misread the review scope.
**Why this priority**: Context leakage would make the customer-safe page unsafe.
**Independent Test**: Use clean URL, `?environment_id=`, clear filter, reload, legacy alias, and cross-workspace environment scenarios.
**Acceptance Scenarios**:
1. **Given** clean `/admin/reviews/workspace`, **When** the page loads, **Then** it is workspace-wide with no environment chip and no remembered environment shell.
2. **Given** `/admin/reviews/workspace?environment_id={id}`, **When** the page loads, **Then** shell remains workspace-owned, data is filtered where supported, and chip/clear filter are visible.
3. **Given** legacy alias query keys or `tableFilters`, **When** the page loads, **Then** they do not create filter state.
4. **Given** a cross-workspace environment ID, **When** the page loads, **Then** it returns safe no-access/404.
### User Story 5 - Keep diagnostics secondary (Priority: P2)
As a support-capable operator, I want diagnostics available only after explicit disclosure so customer reviewers are not exposed to raw operational internals by default.
**Why this priority**: Customer safety and support utility both matter, but default view must remain safe.
**Independent Test**: Render default page and diagnostics disclosure paths; assert raw terms are absent by default and capability-gated/collapsed if implemented.
**Acceptance Scenarios**:
1. **Given** diagnostics exist, **When** the page renders, **Then** diagnostics are collapsed or secondary by default.
2. **Given** user lacks diagnostic capability, **When** the page renders, **Then** diagnostic links/content are hidden or unavailable without sensitive leakage.
3. **Given** authorized operator opens diagnostics, **When** details appear, **Then** they remain secondary and avoid secrets/raw provider payloads.
## Functional Requirements *(mandatory)*
- **FR-001**: The page MUST keep the existing canonical route `/admin/reviews/workspace`.
- **FR-002**: The page MUST show header/scope context: Customer Review Workspace, workspace, workspace-wide vs environment filter, and review mode/customer-safe.
- **FR-003**: The page MUST show a main decision card asking "Is this review ready to share?" or equivalent stable copy.
- **FR-004**: The main decision card MUST show status, reason, impact, and one primary next action.
- **FR-005**: The page MUST show readiness dimensions for readiness, evidence, and accepted risk or repo-verified equivalent dimensions.
- **FR-006**: The page MUST show an evidence path panel/area covering evidence snapshot, review pack, decision trail, accepted risk records, OperationRun proof, and export artifact where repo-supported.
- **FR-007**: The page MUST show review pack status, last generated where available, evidence snapshot used where available, export availability, staleness/freshness where repo-supported, and authorized open/download action where available.
- **FR-008**: The page MUST show accepted-risk counts and states where repo-supported: total, expiring soon, expired, pending approval, and requiring review.
- **FR-009**: The page MUST show customer-safe findings/follow-ups where repo-supported, including title, priority/severity, owner/due if available, evidence/proof state, and next action.
- **FR-010**: The page MUST hide raw diagnostics by default.
- **FR-011**: The page MUST not expose provider secrets, raw provider payloads, raw OperationRun payloads, internal exception traces, debug metadata, or unfiltered provider/tenant IDs by default.
- **FR-012**: The page MUST classify each visible element in `repo-truth-map.md` before runtime implementation changes.
- **FR-013**: The page MUST use existing models/services/policies and derived labels instead of inventing backend truth.
- **FR-014**: The page MUST not add migrations unless implementation proves a minimal UI-supporting field is absolutely required and the spec is updated first.
- **FR-015**: The page MUST use `environment_id` as the only canonical environment filter query key.
- **FR-016**: The page MUST reject or neutralize legacy filter aliases: `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and `tableFilters`.
- **FR-017**: The page MUST return safe no-access/404 for cross-workspace or unauthorized environment filters.
- **FR-018**: The clear-filter action MUST return to the clean workspace-wide URL and remain safe across reload/back/forward behavior.
- **FR-019**: The page MUST preserve existing RBAC/capability checks for review, evidence, review pack/export, accepted risks, findings, operation proof, and diagnostics.
- **FR-020**: Unauthorized actions MUST be hidden or unavailable according to product convention and must not leak sensitive existence details.
- **FR-021**: The page MUST not introduce destructive or customer-impacting mutation actions into the customer-safe default view.
- **FR-022**: Any destructive/high-impact action later added by implementation MUST use `Action::make(...)->action(...)`, `->requiresConfirmation()`, server-side authorization, audit, and tests; current expected implementation adds none.
- **FR-023**: The page MUST remain Filament v5 and Livewire v4.0+ compliant.
- **FR-024**: Panel provider registration MUST remain in `apps/platform/bootstrap/providers.php`; this spec must not modify panel provider registration unless implementation proves a route registration defect.
- **FR-025**: Globally searchable resources touched by this spec MUST either have View/Edit pages and safe search contracts or keep global search disabled; current related resources are disabled.
- **FR-026**: The page MUST not register new heavy assets. If any Filament asset registration is introduced, deployment must include `cd apps/platform && php artisan filament:assets`; expected outcome is no new assets.
## Non-Functional Requirements
- **NFR-001**: Default view must be readable in Filament light mode and dark mode.
- **NFR-002**: No Graph calls may occur during UI render.
- **NFR-003**: Query cost must remain bounded to existing persisted data and scoped workspace/environment queries.
- **NFR-004**: The implementation must avoid CSS-heavy one-off design systems and prefer native Filament sections/cards/badges/grids.
- **NFR-005**: The page must remain responsive enough for the Filament admin shell.
- **NFR-006**: Empty/unavailable states must be honest and explicit.
- **NFR-007**: No raw diagnostics/screenshots/test artifacts may contain secrets.
## UX Requirements
- Header/scope area shows workspace-wide vs filtered context.
- Main decision card comes before tables and diagnostics.
- Right-side or secondary evidence/detail panel exists.
- Readiness, evidence, accepted risk, review pack, and follow-up content are scan-first.
- One dominant next action is visible.
- Diagnostic/internal details are collapsed/secondary.
- Tables are not the only default experience.
- Copy is customer-safe and avoids internal admin jargon where possible.
- No green/success cue is used without repo-backed proof.
## Capability / RBAC Requirements
Implementation must verify existing capabilities or policy methods for:
- view customer reviews
- view reviews
- view evidence
- view review packs / exports
- view accepted risks
- view findings
- generate review pack if action exists
- refresh evidence if action exists
- export/download review pack if action exists
- view operation run proof if link exists
- view internal diagnostics
No unauthorized action may be shown as executable.
## Data Source Discovery Requirements
Before runtime changes, create and maintain:
```text
specs/326-customer-review-workspace-v1-productization/repo-truth-map.md
```
The map must cover:
- Tenant Reviews / Environment Reviews
- Customer Review Workspace page state
- Evidence Snapshots
- Review Packs / exports
- Accepted Risks / Risk Exceptions
- Findings / Finding Exceptions
- Stored Reports / Export Artifacts
- OperationRuns
- Workspace entitlements/capabilities
- Audit log
Each entry must include UI element, source model/service/page, status source, authorization/capability, workspace/environment scope, OperationRun/audit link, fallback/empty state, and classification.
## Assumptions
- Spec 312 work is present and can be refined; it must not be rewritten as if it never happened.
- Existing helpers in `tests/Pest.php` can create review/evidence/pack scenarios for targeted tests.
- Existing `ReviewPack`, `EnvironmentReview`, `EvidenceSnapshot`, and `FindingException` models are sufficient for v1.
- No migration is expected.
- No package or env var is expected.
## Risks
- The page may already contain customer-safe summary logic, so implementation must avoid broad rewrites and focus on gaps against Spec 325 target direction.
- Accepted-risk/follow-up counts may not all be available as direct repo truth; unavailable/empty states must be honest.
- Diagnostics disclosure could leak raw payloads if implemented too eagerly.
- Browser smoke may require stable fixtures and must not broaden browser lane cost beyond the named Spec 326 test.
## Open Questions
No product decision blocks implementation. Implementation must resolve repo-truth details in `repo-truth-map.md` and update this spec/plan before continuing if a requested visible element cannot be sourced safely.
## Acceptance Criteria
### Productization
- [ ] Customer Review Workspace has decision-first layout.
- [ ] Main readiness question is visible.
- [ ] Review readiness is visible.
- [ ] Evidence freshness/path is visible.
- [ ] Review Pack status is visible.
- [ ] Accepted Risk summary is visible.
- [ ] Customer-safe findings/follow-ups are visible or explicitly unavailable.
- [ ] One primary next action is clear.
- [ ] Diagnostics are secondary/collapsed.
### Customer Safety
- [ ] Raw diagnostics are hidden by default.
- [ ] Provider secrets are not visible.
- [ ] Internal exception/debug text is not visible.
- [ ] Customer-facing copy avoids internal admin jargon.
- [ ] Empty/unavailable states are honest.
- [ ] No false green success state.
### Evidence / Review Pack
- [ ] Evidence path is visible.
- [ ] Evidence snapshot availability is shown.
- [ ] Review pack availability is shown.
- [ ] Review pack freshness/staleness is shown where repo-supported.
- [ ] Export/open actions respect RBAC.
- [ ] OperationRun proof is linked where repo-supported and authorized.
### Scope
- [ ] Clean URL is workspace-wide.
- [ ] Shell is Workspace-only.
- [ ] Environment filter uses `environment_id`.
- [ ] Visible Environment chip appears when filtered.
- [ ] Clear filter works.
- [ ] Reload after clear is safe.
- [ ] Legacy aliases do not create filter state.
- [ ] Cross-workspace Environment is rejected.
### RBAC
- [ ] Unauthorized user cannot access protected data.
- [ ] Unauthorized actions are hidden/disabled.
- [ ] Diagnostics require appropriate capability.
- [ ] Export/open review pack respects capability.
- [ ] Evidence access respects capability.
### UI / Visual
- [ ] Layout uses premium target direction from Spec 325 without treating target images as truth.
- [ ] Filament light mode remains readable.
- [ ] No one-off heavy CSS unless justified.
- [ ] Right-side panel or equivalent evidence/detail area exists.
- [ ] Tables are not the only default experience.
- [ ] Page remains responsive enough for Filament admin shell.
### Tests / Validation
- [ ] Repo truth map exists.
- [ ] Required Feature tests pass.
- [ ] Required Browser smoke passes.
- [ ] Relevant Spec 314-322 guards still pass.
- [ ] `pint --dirty` passes.
- [ ] `git diff --check` passes.
- [ ] No broad rebaseline.
- [ ] Full suite status is honestly reported if run/not run.
## Definition Of Done
Spec 326 is done when:
1. Customer Review Workspace is productized into a decision-first surface.
2. Customer-safe review readiness is visible.
3. Review Pack state is visible.
4. Evidence freshness/path is visible.
5. Accepted risks are summarized safely.
6. Customer-safe findings/follow-ups are visible or honestly unavailable.
7. Raw diagnostics are hidden by default.
8. Primary next action is clear.
9. Workspace/Environment filter contract remains correct.
10. RBAC/capabilities are respected.
11. No false green states are introduced.
12. No invented backend claims are shown as runtime truth.
13. Browser smoke confirms clean, filtered, clear, reload, and disclosure behavior.
14. Spec 314-322 context guards remain green or known unrelated failures are documented.
15. No migrations/packages/env/queues/scheduler/storage/deployment assets are introduced unless explicitly justified.
## Follow-Up Specs
- Spec 327 - Governance Inbox Decision-First Workbench Productization.
- Spec 328 - Operations Hub Decision-First Workbench Productization.
- Spec 329 - Evidence / Audit Log Disclosure Productization.
- Spec 330 - Environment Dashboard / Baseline Compare Productization.
- Spec 331 - Restore Safety Workflow Productization.
Do not start these inside Spec 326.

View File

@ -0,0 +1,168 @@
# Tasks: Spec 326 - Customer Review Workspace v1 Productization
**Input**: Design documents from `/specs/326-customer-review-workspace-v1-productization/`
**Prerequisites**: `spec.md`, `plan.md`, `repo-truth-map.md`
**Tests**: Required. This is a runtime UI/customer-safe Filament/Livewire page productization with browser smoke.
## Test Governance Checklist
- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
- [x] New or changed tests stay in the smallest honest family, and the browser addition is explicit.
- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default.
- [x] Planned validation commands cover the change without pulling in unrelated lane cost.
- [x] The declared surface test profile (`global-context-shell` plus customer-safe disclosure) is explicit.
- [x] Any material budget, baseline, trend, or escalation note is recorded in the active spec or PR.
## Phase 1: Preparation And Repo Truth
**Purpose**: Confirm runtime truth and prevent invented claims before page edits.
- [x] T001 Re-read `specs/326-customer-review-workspace-v1-productization/spec.md`, `plan.md`, and `repo-truth-map.md`.
- [x] T002 Re-read related completed context only: Specs 312 and 314-325. Do not modify their artifacts.
- [x] T003 Verify current `CustomerReviewWorkspace` route/class/view and existing tests before editing.
- [x] T004 Update `repo-truth-map.md` with any newly discovered source, capability, fallback, or classification before runtime changes.
- [x] T005 Confirm no migration/package/env/queue/storage/deployment asset change is required; if one appears necessary, stop and update spec/plan first.
- [x] T006 Confirm Filament v5 / Livewire v4.0+ compliance and no Livewire v3/Filament legacy API use.
- [x] T007 Confirm panel provider registration remains `apps/platform/bootstrap/providers.php`.
- [x] T008 Confirm related globally searchable resources stay disabled or have safe View/Edit pages; expected related resources remain `protected static bool $isGloballySearchable = false`.
## Phase 2: Feature Tests First
**Purpose**: Lock customer-safe behavior, scope, RBAC, and no-false-green before the UI refactor.
- [x] T009 Add or update a feature test asserting `repo-truth-map.md` exists and lists required data areas.
- [x] T010 Add or update a Feature/Livewire test for the decision-first layout text: `Customer Review Workspace`, `Is this review ready to share?`, `Readiness`, `Evidence`, `Accepted risk`, `Evidence path`, `Review pack`, and `Decision trail`.
- [x] T011 Add or update a Feature/Livewire test that raw diagnostics are hidden by default: `raw payload`, `stack trace`, `provider secret`, `debug metadata`, and `internal exception` must not appear.
- [x] T012 Add or update review-pack readiness tests for available, unavailable, and stale/needs-refresh or explicit unsupported/unavailable state.
- [x] T013 Add or update evidence freshness tests proving evidence state is visible and no generic green/success state appears without repo-backed proof.
- [x] T014 Add or update accepted-risk summary tests for total/current state and expiring/expired/pending where repo-supported; assert internal approval/debug details are absent by default.
- [x] T015 Add or update customer-safe follow-up tests for title, priority/severity if available, owner/due if available, proof state, next action, and no raw diagnostics by default.
- [x] T016 Add or update RBAC tests covering view review, view evidence, export/open review pack, and diagnostics action visibility/unavailability.
- [x] T017 Add or update canonical environment filter tests for `?environment_id=`, visible chip, workspace shell only, clear filter, and provable filtered data.
- [x] T018 Add or update legacy alias rejection tests for `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and `tableFilters`.
- [x] T019 Add or update cross-workspace environment filter guard test returning safe 404/no-access.
## Phase 3: Page Skeleton Productization
**Purpose**: Refactor existing page layout without new backend foundation.
- [x] T020 Update `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` to expose a repo-truth-bounded payload for header/scope, main decision card, readiness dimensions, evidence path, review pack, accepted risks, follow-ups, and diagnostics disclosure.
- [x] T021 Update `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php` to render the decision-first structure before the table.
- [x] T022 Ensure the header/scope area shows workspace-wide vs environment-filtered context, visible environment chip when filtered, and customer-safe review mode.
- [x] T023 Ensure the main decision card shows status, reason, impact, and one primary next action.
- [x] T024 Ensure readiness summary cards show readiness, evidence, and accepted-risk dimensions or repo-verified equivalents.
- [x] T025 Ensure the evidence path panel shows evidence snapshot, review pack, decision trail, accepted-risk records, OperationRun proof, and export artifact as available/unavailable/stale/requires-refresh/not-applicable.
- [x] T026 Ensure the review-pack panel shows status, last generated where available, evidence snapshot where available, export/open availability, staleness/freshness where repo-supported, and operation proof where repo-supported.
- [x] T027 Ensure accepted-risk summary shows counts/states only where repo-supported and uses customer-safe language.
- [x] T028 Ensure customer-safe findings/follow-ups show supported fields and honest unavailable state where source truth is absent.
- [x] T029 Ensure diagnostics/internal details are collapsed or secondary by default and authorized before visibility.
- [x] T030 Keep the existing table as secondary context; it must not be the only default experience, must render from persisted data only, and must not make Graph calls during page render.
## Phase 4: Actions, RBAC, And Safety
**Purpose**: Show only real, authorized actions and preserve read-only default behavior.
- [x] T031 Keep primary action repo-real and authorized: open/download review pack when ready and permitted, otherwise open latest review or show unavailable/follow-up state.
- [x] T032 Add/open evidence, accepted-risk, finding, operation-proof, or diagnostics links only when route and authorization are repo-real.
- [x] T033 Ensure unauthorized actions are hidden or unavailable without leaking sensitive details.
- [x] T034 Verify no customer-safe default action publishes, generates, refreshes, regenerates, expires, revokes, deletes, restores, or mutates tenant/provider state.
- [x] T035 If any high-impact action is unexpectedly required, implement it with `Action::make(...)->action(...)`, `->requiresConfirmation()`, server-side authorization, audit, notification, and tests after updating spec/plan first.
- [x] T036 Ensure existing page-open audit logging remains safe and does not include secrets/raw payloads.
## Phase 5: Workspace / Environment Scope Contract
**Purpose**: Preserve Specs 314-322.
- [x] T037 Verify clean `/admin/reviews/workspace` does not read remembered environment shell state or persisted table filters.
- [x] T038 Verify `/admin/reviews/workspace?environment_id={id}` filters only page data, shows visible chip, and keeps Workspace shell ownership.
- [x] T039 Verify clear filter redirects to clean workspace URL and remains safe after reload.
- [x] T040 Verify legacy aliases are removed/neutralized and do not set filter state.
- [x] T041 Verify cross-workspace or unauthorized `environment_id` returns safe no-access/404.
- [x] T042 Verify back/forward/reload behavior does not resurrect cleared environment filter state.
## Phase 6: Browser Smoke And Screenshots
**Purpose**: Prove the user-facing contract in the integrated browser lane.
- [x] T043 Create `apps/platform/tests/Browser/Spec326CustomerReviewWorkspaceProductizationSmokeTest.php` using existing Pest Browser conventions.
- [x] T044 Browser Flow A: clean workspace entry; assert Workspace shell only, no Environment chip, main decision card, evidence path, diagnostics collapsed, screenshot.
- [x] T045 Browser Flow B: filtered environment entry; assert Workspace shell only, visible chip, filter copy, clear filter, screenshot.
- [x] T046 Browser Flow C: clear filter and reload; assert clean URL, chip does not return, no active Environment shell.
- [x] T047 Browser Flow D: customer-safe disclosure; assert raw diagnostics absent by default, open diagnostics if available/authorized, verify secondary placement.
- [x] T048 Browser Flow E: light mode readability check if supported; capture optional screenshot.
- [x] T049 Save screenshots under `specs/326-customer-review-workspace-v1-productization/artifacts/screenshots/` when generated and ensure they contain no secrets.
## Phase 7: UI Coverage And Documentation Artifacts
**Purpose**: Satisfy UI-COV without unrelated docs churn.
- [x] T050 Decide after runtime diff whether `docs/ui-ux-enterprise-audit/route-inventory.md` or `design-coverage-matrix.md` needs an update.
- [x] T051 If coverage docs are not changed, add a close-out note explaining why existing UI-006 report plus Spec 325 target artifacts remain sufficient for the unchanged route.
- [x] T052 Update `repo-truth-map.md` final classifications for implemented/empty/deferred elements.
- [x] T053 Do not create general documentation files outside required Spec Kit/UI coverage artifacts.
## Phase 8: Validation
**Purpose**: Run narrow proof and report honestly.
- [x] T054 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Reviews tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php --compact`.
- [x] T055 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec326CustomerReviewWorkspaceProductizationSmokeTest.php --compact`.
- [ ] T056 Run `cd apps/platform && ./vendor/bin/sail artisan test --filter='CustomerReviewWorkspace|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact`. Attempted after T054/T055; the process was killed with signal 9 after the feature/navigation portion completed, so this exact all-matching filtered command is not marked complete.
- [x] T057 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`; `artisan pint` is not registered, so validation used `./vendor/bin/sail pint --dirty` plus explicit touched PHP/localization paths.
- [x] T058 Run `git diff --check`.
- [x] T059 Report full-suite status honestly if not run.
- [x] T060 Confirm no migrations, seeders, packages, env vars, queues, scheduler, storage, deployment assets, backwards compatibility layer, or legacy tenant alias support were added.
## Follow-up Phase: Premium Layout Alignment
**Purpose**: Continue Spec 326 without new numbering and align the Customer Review Workspace runtime UI more closely with the premium Spec 325 target direction.
- [x] T061 Compact the Customer Review Workspace intro and quiet the non-certification disclosure.
- [x] T062 Remove platform-context `tenant` wording from Customer Review Workspace runtime copy and tests.
- [x] T063 Recompose the Blade view into a dense main/aside layout with a decision-first main column and evidence/review-pack/disclosure aside.
- [x] T064 Keep the table as secondary `Review package index` context after the decision and evidence workbench.
- [x] T065 Extend Feature/Browser assertions for main question, evidence path, review-pack panel, accepted-risk panel, disclosure rule, collapsed diagnostics, hidden raw diagnostics, and no platform-context tenant copy.
- [x] T066 Capture `artifacts/screenshots/customer-review-workspace-premium-layout.png`.
- [x] T067 Re-run the requested Spec 326 validation commands and record results honestly. Feature/navigation tests passed, `pint --dirty` passed, and `git diff --check` passed. The requested Pest Browser smoke command was attempted twice in the follow-up and hung without output until manually stopped; manual browser smoke plus screenshot capture passed with no console warnings/errors.
## Follow-up Phase: Accepted-Risk Readiness Alignment
**Purpose**: Keep the premium layout truthful when accepted-risk follow-up is required and avoid a false ready-to-share primary state.
- [x] T068 Align the main decision card with accepted-risk follow-up truth so it shows `Shareable with follow-up` or `Review needed` instead of `Ready to share` when follow-up is required.
- [x] T069 Keep the right-side Evidence / Review Pack / Accepted Risk / Disclosure aside visible and test-addressable at desktop width.
- [x] T070 Shorten the readiness, evidence, review-pack, and accepted-risk state-card copy.
- [x] T071 Add a regression test proving accepted-risk follow-up suppresses the `Ready to share` state.
- [x] T072 Re-run the requested final validation. Feature Reviews passed, Spec 326 Pest Browser smoke passed, `pint --dirty` passed, and `git diff --check` passed. Direct in-app browser verification passed at medium desktop width and the premium-layout screenshot artifact was refreshed.
- [x] T073 Demote `Download review pack` to a secondary action when the main state is `Shareable with follow-up`.
- [x] T074 Move the premium Evidence / Review Pack / Accepted Risk / Disclosure aside to the medium desktop breakpoint and compact the aside panels so it is visible earlier in the first viewport.
## Non-Goals Checklist
- [x] NT001 Do not build an external customer portal.
- [x] NT002 Do not implement external authentication, invitation links, email delivery, or PSA handoff.
- [x] NT003 Do not implement a new review/evidence/review-pack backend.
- [x] NT004 Do not redesign Governance Inbox, Operations Hub, Evidence Overview, Environment Dashboard, Baseline Compare, or Restore Safety Workflow.
- [x] NT005 Do not add migrations unless spec/plan are updated first with proof.
- [x] NT006 Do not rewrite completed Specs 312 or 314-325.
- [x] NT007 Do not add legacy tenant query alias support.
## Required Final Report Content
When implementation later completes, report:
- Changed behavior.
- Customer-safe review surface.
- Evidence / Review Pack / Accepted Risk coverage.
- Files changed.
- Repo truth map status.
- Tests run and results.
- Browser verification and screenshots path.
- Known gaps.
- Remaining follow-ups.
- Diagnostics default state.
- RBAC-visible/hidden actions.
- Repo-verified vs unavailable states.
- Full suite run/not run.
- Explicit no migrations/seeders/packages/env/queues/scheduler/storage/deployment assets/backcompat/legacy aliases statement.