feat: implement management report layout branded report themes (#437)
Implemented management report layout branded report themes as requested. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #437
@ -387,7 +387,7 @@ private function attachPublishedReviewPack(
|
||||
array $scenarioConfig,
|
||||
): void {
|
||||
$filePath = (string) ($scenarioConfig['published_review_pack_path'] ?? 'review-packs/spec351-browser-ready-published.zip');
|
||||
$fileContents = (string) ($scenarioConfig['published_review_pack_contents'] ?? 'PK-spec351-browser-ready-published');
|
||||
$fileContents = $this->publishedReviewPackZipContents($environment, $review, $snapshot, $scenarioConfig);
|
||||
|
||||
Storage::disk('exports')->put($filePath, $fileContents);
|
||||
|
||||
@ -430,6 +430,62 @@ private function attachPublishedReviewPack(
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $scenarioConfig
|
||||
*/
|
||||
private function publishedReviewPackZipContents(
|
||||
ManagedEnvironment $environment,
|
||||
EnvironmentReview $review,
|
||||
EvidenceSnapshot $snapshot,
|
||||
array $scenarioConfig,
|
||||
): string {
|
||||
$executiveSummary = (string) ($scenarioConfig['published_review_pack_contents'] ?? 'Spec 351 browser-ready published review pack.');
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'review-output-fixture-');
|
||||
|
||||
if ($tempFile === false) {
|
||||
throw new \RuntimeException('Failed to allocate a temporary review pack archive.');
|
||||
}
|
||||
|
||||
try {
|
||||
$zip = new \ZipArchive();
|
||||
$result = $zip->open($tempFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
|
||||
|
||||
if ($result !== true) {
|
||||
throw new \RuntimeException("Failed to create fixture ZIP archive: error code {$result}");
|
||||
}
|
||||
|
||||
$zip->addFromString('executive-summary.md', $executiveSummary);
|
||||
$zip->addFromString('metadata.json', json_encode([
|
||||
'fixture' => 'spec-351-review-output-browser-smoke',
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'managed_environment_slug' => (string) $environment->slug,
|
||||
'environment_review_id' => (int) $review->getKey(),
|
||||
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
||||
], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
||||
$zip->addFromString('summary.json', json_encode([
|
||||
'status' => 'ready',
|
||||
'source' => 'browser-fixture',
|
||||
'executive_summary_file' => 'executive-summary.md',
|
||||
], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
||||
|
||||
if ($zip->close() !== true) {
|
||||
throw new \RuntimeException('Failed to finalize fixture ZIP archive.');
|
||||
}
|
||||
|
||||
$contents = file_get_contents($tempFile);
|
||||
|
||||
if (! is_string($contents) || $contents === '') {
|
||||
throw new \RuntimeException('Failed to read fixture ZIP archive contents.');
|
||||
}
|
||||
|
||||
return $contents;
|
||||
} finally {
|
||||
if (file_exists($tempFile)) {
|
||||
unlink($tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function markReadyReview(EnvironmentReview $review): EnvironmentReview
|
||||
{
|
||||
$review->loadMissing(['sections', 'evidenceSnapshot.items']);
|
||||
|
||||
@ -950,9 +950,12 @@ private function resumeCaptureAction(): Action
|
||||
$viewAction = Action::make('view_run')
|
||||
->label(OperationRunLinks::openLabel())
|
||||
->url(OperationRunLinks::tenantlessView($run));
|
||||
$runTenantId = is_numeric($run->managed_environment_id)
|
||||
? (int) $run->managed_environment_id
|
||||
: null;
|
||||
|
||||
if (! $run->wasRecentlyCreated && in_array((string) $run->status, ['queued', 'running'], true)) {
|
||||
OpsUxBrowserEvents::dispatchRunEnqueued($this);
|
||||
OpsUxBrowserEvents::dispatchRunEnqueued($this, $runTenantId);
|
||||
|
||||
OperationUxPresenter::alreadyQueuedToast((string) $run->type)
|
||||
->actions([$viewAction])
|
||||
@ -961,7 +964,7 @@ private function resumeCaptureAction(): Action
|
||||
return;
|
||||
}
|
||||
|
||||
OpsUxBrowserEvents::dispatchRunEnqueued($this);
|
||||
OpsUxBrowserEvents::dispatchRunEnqueued($this, $runTenantId);
|
||||
|
||||
OperationUxPresenter::queuedToast((string) $run->type)
|
||||
->actions([$viewAction])
|
||||
|
||||
@ -138,7 +138,7 @@ private function captureAction(): Action
|
||||
->url(OperationRunLinks::view($run, $sourceTenant));
|
||||
|
||||
if (! $run->wasRecentlyCreated && in_array((string) $run->status, ['queued', 'running'], true)) {
|
||||
OpsUxBrowserEvents::dispatchRunEnqueued($this);
|
||||
OpsUxBrowserEvents::dispatchRunEnqueued($this, $sourceTenant);
|
||||
|
||||
OperationUxPresenter::alreadyQueuedToast((string) $run->type)
|
||||
->actions([$viewAction])
|
||||
@ -147,7 +147,7 @@ private function captureAction(): Action
|
||||
return;
|
||||
}
|
||||
|
||||
OpsUxBrowserEvents::dispatchRunEnqueued($this);
|
||||
OpsUxBrowserEvents::dispatchRunEnqueued($this, $sourceTenant);
|
||||
|
||||
OperationUxPresenter::queuedToast((string) $run->type)
|
||||
->actions([$viewAction])
|
||||
@ -291,7 +291,7 @@ private function compareNowAction(): Action
|
||||
->url(OperationRunLinks::view($run, $targetTenant));
|
||||
|
||||
if (! $run->wasRecentlyCreated && in_array((string) $run->status, ['queued', 'running'], true)) {
|
||||
OpsUxBrowserEvents::dispatchRunEnqueued($this);
|
||||
OpsUxBrowserEvents::dispatchRunEnqueued($this, $targetTenant);
|
||||
|
||||
OperationUxPresenter::alreadyQueuedToast((string) $run->type)
|
||||
->actions([$viewAction])
|
||||
@ -300,7 +300,7 @@ private function compareNowAction(): Action
|
||||
return;
|
||||
}
|
||||
|
||||
OpsUxBrowserEvents::dispatchRunEnqueued($this);
|
||||
OpsUxBrowserEvents::dispatchRunEnqueued($this, $targetTenant);
|
||||
|
||||
OperationUxPresenter::queuedToast((string) $run->type)
|
||||
->actions([$viewAction])
|
||||
|
||||
@ -12,11 +12,11 @@
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\ReviewPackService;
|
||||
use App\Support\Auth\Capabilities;
|
||||
use App\Support\ReviewPacks\ReportDisclosurePolicy;
|
||||
use App\Support\ReviewPacks\ReportProfileRegistry;
|
||||
use App\Support\ReviewPacks\ReportThemeResolver;
|
||||
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
|
||||
use App\Support\ReviewPackStatus;
|
||||
use Illuminate\Http\Request;
|
||||
@ -112,13 +112,33 @@ private function reportState(
|
||||
$technicalDetails = (bool) ($disclosurePolicy['show_technical_details'] ?? false)
|
||||
? (is_array($guidance['technical_details'] ?? null) ? $guidance['technical_details'] : [])
|
||||
: [];
|
||||
$topFindings = is_array($governancePackage['top_findings'] ?? null) ? $governancePackage['top_findings'] : [];
|
||||
$acceptedRisks = $this->acceptedRiskItems(
|
||||
is_array($governancePackage['accepted_risks'] ?? null) ? $governancePackage['accepted_risks'] : [],
|
||||
$state !== ReviewPackOutputResolutionGuidance::STATE_INTERNAL_ONLY,
|
||||
);
|
||||
$governanceDecisions = is_array($decisionSummary['entries'] ?? null)
|
||||
? $decisionSummary['entries']
|
||||
: (is_array($governancePackage['governance_decisions'] ?? null) ? $governancePackage['governance_decisions'] : []);
|
||||
$theme = ReportThemeResolver::resolve($reviewPack, $tenant, $profile, $guidance, $evidenceBasis, [
|
||||
'top_findings' => $topFindings,
|
||||
'top_findings_measured' => array_key_exists('top_findings', $governancePackage),
|
||||
'accepted_risks' => $acceptedRisks,
|
||||
'accepted_risks_measured' => array_key_exists('accepted_risks', $governancePackage),
|
||||
'governance_decisions' => $governanceDecisions,
|
||||
'governance_decisions_measured' => array_key_exists('entries', $decisionSummary)
|
||||
|| array_key_exists('governance_decisions', $governancePackage),
|
||||
]);
|
||||
|
||||
return [
|
||||
'title' => __('localization.review.rendered_report'),
|
||||
'guidance' => $guidance,
|
||||
'state' => $state,
|
||||
'hero' => $this->heroState($state, $guidance),
|
||||
'branding' => $this->brandingState($tenant),
|
||||
'theme' => $theme,
|
||||
'layout' => $theme['layout'],
|
||||
'branding' => $theme['identity'],
|
||||
'kpi_strip' => $theme['kpi_strip'],
|
||||
'profile' => $profile,
|
||||
'disclosure_policy' => $disclosurePolicy,
|
||||
'source_metadata' => $this->sourceMetadata($request, $review),
|
||||
@ -149,15 +169,10 @@ private function reportState(
|
||||
'evidence_basis' => $evidenceBasis,
|
||||
'evidence_basis_summary' => $evidenceBasis['description'],
|
||||
'limitations' => $limitations,
|
||||
'top_findings' => is_array($governancePackage['top_findings'] ?? null) ? $governancePackage['top_findings'] : [],
|
||||
'accepted_risks' => $this->acceptedRiskItems(
|
||||
is_array($governancePackage['accepted_risks'] ?? null) ? $governancePackage['accepted_risks'] : [],
|
||||
$state !== ReviewPackOutputResolutionGuidance::STATE_INTERNAL_ONLY,
|
||||
),
|
||||
'top_findings' => $topFindings,
|
||||
'accepted_risks' => $acceptedRisks,
|
||||
'decision_summary' => $decisionSummary,
|
||||
'governance_decisions' => is_array($decisionSummary['entries'] ?? null)
|
||||
? $decisionSummary['entries']
|
||||
: (is_array($governancePackage['governance_decisions'] ?? null) ? $governancePackage['governance_decisions'] : []),
|
||||
'governance_decisions' => $governanceDecisions,
|
||||
'next_actions' => is_array($summary['recommended_next_actions'] ?? null) ? $summary['recommended_next_actions'] : [],
|
||||
'non_certification_disclosure' => $nonCertificationDisclosure,
|
||||
'section_appendix' => $sectionAppendix,
|
||||
@ -167,22 +182,6 @@ private function reportState(
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{prepared_by:string,prepared_for:string,generated_by:string}
|
||||
*/
|
||||
private function brandingState(ManagedEnvironment $tenant): array
|
||||
{
|
||||
$workspace = $tenant->workspace;
|
||||
|
||||
return [
|
||||
'prepared_by' => $workspace instanceof Workspace && filled($workspace->name)
|
||||
? (string) $workspace->name
|
||||
: 'TenantPilot',
|
||||
'prepared_for' => filled($tenant->name) ? (string) $tenant->name : __('localization.review.tenant'),
|
||||
'generated_by' => 'TenantPilot',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $guidance
|
||||
* @return array{title:string,badge:string,summary:string,warning:?string,color:string,customer_safe:bool}
|
||||
|
||||
@ -111,7 +111,11 @@ public function startCapture(
|
||||
);
|
||||
|
||||
if ($run->wasRecentlyCreated) {
|
||||
CaptureBaselineSnapshotJob::dispatch($run);
|
||||
$this->runs->dispatchOrFail(
|
||||
$run,
|
||||
fn () => CaptureBaselineSnapshotJob::dispatch($run),
|
||||
emitQueuedNotification: true,
|
||||
);
|
||||
}
|
||||
|
||||
return ['ok' => true, 'run' => $run];
|
||||
|
||||
@ -118,11 +118,12 @@ public function resume(OperationRun $priorRun, User $initiator): array
|
||||
);
|
||||
|
||||
if ($run->wasRecentlyCreated) {
|
||||
match ($runType) {
|
||||
OperationRunType::BaselineCapture->value => CaptureBaselineSnapshotJob::dispatch($run),
|
||||
OperationRunType::BaselineCompare->value => CompareBaselineToTenantJob::dispatch($run),
|
||||
default => null,
|
||||
$dispatcher = match ($runType) {
|
||||
OperationRunType::BaselineCapture->value => fn () => CaptureBaselineSnapshotJob::dispatch($run),
|
||||
OperationRunType::BaselineCompare->value => fn () => CompareBaselineToTenantJob::dispatch($run),
|
||||
};
|
||||
|
||||
$this->runs->dispatchOrFail($run, $dispatcher, emitQueuedNotification: true);
|
||||
}
|
||||
|
||||
$this->auditLogger->log(
|
||||
|
||||
@ -6,13 +6,14 @@
|
||||
|
||||
use App\Filament\Widgets\Inventory\InventoryKpiHeader;
|
||||
use App\Livewire\BulkOperationProgress;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use Filament\Facades\Filament;
|
||||
|
||||
final class OpsUxBrowserEvents
|
||||
{
|
||||
public const RunEnqueued = 'ops-ux:run-enqueued';
|
||||
|
||||
public static function dispatchRunEnqueued(mixed $livewire): void
|
||||
public static function dispatchRunEnqueued(mixed $livewire, ManagedEnvironment|int|null $tenant = null): void
|
||||
{
|
||||
if (! is_object($livewire)) {
|
||||
return;
|
||||
@ -22,12 +23,33 @@ public static function dispatchRunEnqueued(mixed $livewire): void
|
||||
return;
|
||||
}
|
||||
|
||||
$tenantId = Filament::getTenant()?->getKey();
|
||||
$tenantId = self::resolveTenantId($tenant);
|
||||
|
||||
// In Livewire v3, dispatch() emits a DOM event that bubbles.
|
||||
// In Livewire v4, dispatch() emits a DOM event that bubbles.
|
||||
// Our progress widget is mounted outside the initiating component's DOM tree,
|
||||
// so we target it explicitly to ensure it receives the event immediately.
|
||||
$livewire->dispatch(self::RunEnqueued, tenantId: $tenantId)->to(BulkOperationProgress::class);
|
||||
$livewire->dispatch(self::RunEnqueued, tenantId: $tenantId)->to(InventoryKpiHeader::class);
|
||||
}
|
||||
|
||||
private static function resolveTenantId(ManagedEnvironment|int|null $tenant): ?int
|
||||
{
|
||||
if ($tenant instanceof ManagedEnvironment) {
|
||||
return (int) $tenant->getKey();
|
||||
}
|
||||
|
||||
if (is_int($tenant) && $tenant > 0) {
|
||||
return $tenant;
|
||||
}
|
||||
|
||||
$currentTenantId = Filament::getTenant()?->getKey();
|
||||
|
||||
if (! is_numeric($currentTenantId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentTenantId = (int) $currentTenantId;
|
||||
|
||||
return $currentTenantId > 0 ? $currentTenantId : null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,7 +198,6 @@ private static function candidatePrimaryAction(
|
||||
if (! $review->isPublished() || ! in_array($state, [
|
||||
ReviewPackOutputResolutionGuidance::STATE_PUBLICATION_BLOCKED,
|
||||
ReviewPackOutputResolutionGuidance::STATE_PUBLISHED_WITH_LIMITATIONS,
|
||||
ReviewPackOutputResolutionGuidance::STATE_EXPORT_NOT_READY,
|
||||
ReviewPackOutputResolutionGuidance::STATE_INTERNAL_ONLY,
|
||||
], true)) {
|
||||
return null;
|
||||
|
||||
221
apps/platform/app/Support/ReviewPacks/ReportThemeResolver.php
Normal file
@ -0,0 +1,221 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support\ReviewPacks;
|
||||
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Models\Workspace;
|
||||
|
||||
final class ReportThemeResolver
|
||||
{
|
||||
public const string DEFAULT_ACCENT = 'teal';
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $profile
|
||||
* @param array<string, mixed> $guidance
|
||||
* @param array<string, mixed> $evidenceBasis
|
||||
* @param array<string, mixed> $metrics
|
||||
* @return array{
|
||||
* identity:array{prepared_by:string,prepared_for:string,generated_by:string,generated_at:mixed,accent:string,logo:null},
|
||||
* layout:array{mode:string,appendix_prominence:string,section_order:list<string>,section_ranks:array<string,int>},
|
||||
* kpi_strip:list<array{key:string,label:string,value:string,description:string}>
|
||||
* }
|
||||
*/
|
||||
public static function resolve(
|
||||
ReviewPack $reviewPack,
|
||||
ManagedEnvironment $tenant,
|
||||
array $profile,
|
||||
array $guidance,
|
||||
array $evidenceBasis,
|
||||
array $metrics,
|
||||
): array {
|
||||
$workspace = $tenant->workspace;
|
||||
|
||||
return [
|
||||
'identity' => self::identityFor(
|
||||
$workspace instanceof Workspace ? $workspace->name : null,
|
||||
$tenant->name,
|
||||
$reviewPack->generated_at,
|
||||
),
|
||||
'layout' => self::layoutForProfile($profile),
|
||||
'kpi_strip' => self::kpiStrip($guidance, $evidenceBasis, $metrics),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{prepared_by:string,prepared_for:string,generated_by:string,generated_at:mixed,accent:string,logo:null}
|
||||
*/
|
||||
public static function identityFor(?string $workspaceName, ?string $environmentName, mixed $generatedAt = null): array
|
||||
{
|
||||
return [
|
||||
'prepared_by' => self::displayText($workspaceName, 'TenantPilot'),
|
||||
'prepared_for' => self::displayText($environmentName, __('localization.review.tenant')),
|
||||
'generated_by' => 'TenantPilot',
|
||||
'generated_at' => $generatedAt,
|
||||
'accent' => self::DEFAULT_ACCENT,
|
||||
'logo' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $profile
|
||||
* @return array{mode:string,appendix_prominence:string,section_order:list<string>,section_ranks:array<string,int>}
|
||||
*/
|
||||
public static function layoutForProfile(array $profile): array
|
||||
{
|
||||
$profileKey = (string) ($profile['effective_key'] ?? ($profile['profile_key'] ?? ReportProfileRegistry::DEFAULT_PROFILE));
|
||||
|
||||
[$mode, $appendixProminence, $sectionOrder] = match ($profileKey) {
|
||||
ReportProfileRegistry::CUSTOMER_EXECUTIVE => [
|
||||
'executive',
|
||||
'minimal',
|
||||
[
|
||||
'executive_summary',
|
||||
'profile',
|
||||
'limitations',
|
||||
'findings',
|
||||
'governance_decisions',
|
||||
'accepted_risks',
|
||||
'evidence_basis',
|
||||
'next_actions',
|
||||
'disclosure',
|
||||
'appendix',
|
||||
'technical_details',
|
||||
],
|
||||
],
|
||||
ReportProfileRegistry::CUSTOMER_TECHNICAL => [
|
||||
'technical',
|
||||
'standard',
|
||||
[
|
||||
'executive_summary',
|
||||
'profile',
|
||||
'evidence_basis',
|
||||
'limitations',
|
||||
'findings',
|
||||
'governance_decisions',
|
||||
'accepted_risks',
|
||||
'next_actions',
|
||||
'disclosure',
|
||||
'appendix',
|
||||
'technical_details',
|
||||
],
|
||||
],
|
||||
ReportProfileRegistry::AUDITOR_APPENDIX => [
|
||||
'auditor_appendix',
|
||||
'high',
|
||||
[
|
||||
'profile',
|
||||
'evidence_basis',
|
||||
'disclosure',
|
||||
'limitations',
|
||||
'appendix',
|
||||
'technical_details',
|
||||
'executive_summary',
|
||||
'findings',
|
||||
'governance_decisions',
|
||||
'accepted_risks',
|
||||
'next_actions',
|
||||
],
|
||||
],
|
||||
default => [
|
||||
'internal',
|
||||
'standard',
|
||||
[
|
||||
'executive_summary',
|
||||
'profile',
|
||||
'limitations',
|
||||
'evidence_basis',
|
||||
'findings',
|
||||
'governance_decisions',
|
||||
'accepted_risks',
|
||||
'next_actions',
|
||||
'disclosure',
|
||||
'appendix',
|
||||
'technical_details',
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
$sectionRanks = [];
|
||||
|
||||
foreach ($sectionOrder as $rank => $sectionKey) {
|
||||
$sectionRanks[$sectionKey] = ($rank + 1) * 10;
|
||||
}
|
||||
|
||||
return [
|
||||
'mode' => $mode,
|
||||
'appendix_prominence' => $appendixProminence,
|
||||
'section_order' => $sectionOrder,
|
||||
'section_ranks' => $sectionRanks,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $guidance
|
||||
* @param array<string, mixed> $evidenceBasis
|
||||
* @param array<string, mixed> $metrics
|
||||
* @return list<array{key:string,label:string,value:string,description:string}>
|
||||
*/
|
||||
public static function kpiStrip(array $guidance, array $evidenceBasis, array $metrics): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'key' => 'governance_status',
|
||||
'label' => __('localization.review.report_kpi_governance_status'),
|
||||
'value' => self::displayText($guidance['boundary_label'] ?? null, __('localization.review.unavailable')),
|
||||
'description' => self::displayText(
|
||||
$guidance['primary_reason'] ?? null,
|
||||
__('localization.review.report_summary_default_impact'),
|
||||
),
|
||||
],
|
||||
[
|
||||
'key' => 'evidence_coverage',
|
||||
'label' => __('localization.review.report_kpi_evidence_coverage'),
|
||||
'value' => self::displayText($evidenceBasis['label'] ?? null, __('localization.review.unavailable')),
|
||||
'description' => self::displayText($evidenceBasis['description'] ?? null, __('localization.review.report_evidence_missing_description')),
|
||||
],
|
||||
[
|
||||
'key' => 'key_risks',
|
||||
'label' => __('localization.review.report_kpi_key_risks'),
|
||||
'value' => self::countValue($metrics['top_findings'] ?? null, (bool) ($metrics['top_findings_measured'] ?? false)),
|
||||
'description' => __('localization.review.report_kpi_key_risks_description'),
|
||||
],
|
||||
[
|
||||
'key' => 'open_decisions',
|
||||
'label' => __('localization.review.report_kpi_open_decisions'),
|
||||
'value' => self::countValue($metrics['governance_decisions'] ?? null, (bool) ($metrics['governance_decisions_measured'] ?? false)),
|
||||
'description' => __('localization.review.report_kpi_open_decisions_description'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private static function countValue(mixed $items, bool $measured): string
|
||||
{
|
||||
if (! $measured) {
|
||||
return __('localization.review.report_kpi_not_measured');
|
||||
}
|
||||
|
||||
if (! is_array($items)) {
|
||||
return __('localization.review.unavailable');
|
||||
}
|
||||
|
||||
return (string) count($items);
|
||||
}
|
||||
|
||||
private static function displayText(mixed $value, string $fallback): string
|
||||
{
|
||||
if (! is_scalar($value) && $value !== null) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$text = preg_replace('/\s+/', ' ', trim((string) $value));
|
||||
|
||||
if (! is_string($text) || $text === '') {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
return str_starts_with($text, 'localization.') ? $fallback : $text;
|
||||
}
|
||||
}
|
||||
@ -179,7 +179,7 @@
|
||||
'operation_initiator_name' => 'Spec 351 Browser Operator',
|
||||
'publish_reason' => 'Seed published predecessor for Spec 351 browser verification.',
|
||||
'published_review_pack_path' => 'review-packs/spec351-browser-ready-published.zip',
|
||||
'published_review_pack_contents' => 'PK-spec351-browser-ready-published',
|
||||
'published_review_pack_contents' => 'Spec 351 browser-ready published review pack.',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@ -797,6 +797,7 @@
|
||||
'rendered_report_summary_fallback' => 'Für dieses veröffentlichte Review ist keine Executive-Zusammenfassung verfügbar.',
|
||||
'rendered_report_html_only' => 'Dieser v1-Bericht bleibt HTML-first. Verwenden Sie bei Bedarf den Browser-Druckdialog für eine PDF-Übergabe.',
|
||||
'rendered_report_appendix_note' => 'Dieser gerenderte Bericht ist aus dem aktuellen Review-Pack-Vertrag abgeleitet. Das ZIP-Paket bleibt der strukturierte Anhang und das herunterladbare Artefakt.',
|
||||
'rendered_report_toolbar' => 'Aktionen für den gerenderten Bericht',
|
||||
'print_rendered_report' => 'Bericht drucken',
|
||||
'return_to_review_detail' => 'Review-Detail öffnen',
|
||||
'return_to_review_pack_detail' => 'Review-Pack-Detail öffnen',
|
||||
@ -820,6 +821,14 @@
|
||||
'governance_review_report' => 'Governance-Review-Bericht',
|
||||
'prepared_by_for' => 'Erstellt von :prepared_by für :prepared_for',
|
||||
'generated_by' => 'Erzeugt durch :generated_by',
|
||||
'report_kpi_strip' => 'Entscheidungsleiste des Berichts',
|
||||
'report_kpi_governance_status' => 'Governance-Status',
|
||||
'report_kpi_evidence_coverage' => 'Evidence-Abdeckung',
|
||||
'report_kpi_key_risks' => 'Schlüsselrisiken',
|
||||
'report_kpi_key_risks_description' => 'Repo-gestützte Risiken aus dem aktuellen Governance-Paket.',
|
||||
'report_kpi_open_decisions' => 'Offene Entscheidungen',
|
||||
'report_kpi_open_decisions_description' => 'Governance-Entscheidungen, die im Bericht Aufmerksamkeit erfordern.',
|
||||
'report_kpi_not_measured' => 'Nicht gemessen',
|
||||
'disclosure_policy' => 'Disclosure-Policy',
|
||||
'proof_state_verified' => 'Verifiziert',
|
||||
'proof_state_assumed' => 'Angenommen',
|
||||
|
||||
@ -797,6 +797,7 @@
|
||||
'rendered_report_summary_fallback' => 'No executive summary is available for this released review.',
|
||||
'rendered_report_html_only' => 'This v1 report is HTML-first. Use your browser print dialog when you need a PDF handoff.',
|
||||
'rendered_report_appendix_note' => 'This rendered report is derived from the current review-pack contract. The ZIP package remains the structured appendix and downloadable artifact.',
|
||||
'rendered_report_toolbar' => 'Rendered report actions',
|
||||
'print_rendered_report' => 'Print report',
|
||||
'return_to_review_detail' => 'Open review detail',
|
||||
'return_to_review_pack_detail' => 'Open review pack detail',
|
||||
@ -820,6 +821,14 @@
|
||||
'governance_review_report' => 'Governance review report',
|
||||
'prepared_by_for' => 'Prepared by :prepared_by for :prepared_for',
|
||||
'generated_by' => 'Generated by :generated_by',
|
||||
'report_kpi_strip' => 'Report decision strip',
|
||||
'report_kpi_governance_status' => 'Governance status',
|
||||
'report_kpi_evidence_coverage' => 'Evidence coverage',
|
||||
'report_kpi_key_risks' => 'Key risks',
|
||||
'report_kpi_key_risks_description' => 'Repo-backed risks summarized in the current governance package.',
|
||||
'report_kpi_open_decisions' => 'Open decisions',
|
||||
'report_kpi_open_decisions_description' => 'Governance decisions requiring reader awareness in this report.',
|
||||
'report_kpi_not_measured' => 'Not measured',
|
||||
'disclosure_policy' => 'Disclosure policy',
|
||||
'proof_state_verified' => 'Verified',
|
||||
'proof_state_assumed' => 'Assumed',
|
||||
|
||||
@ -7,7 +7,11 @@
|
||||
'gray' => 'background:#f3f4f6;color:#374151;border-color:#d1d5db;',
|
||||
];
|
||||
$hero = is_array($report['hero'] ?? null) ? $report['hero'] : [];
|
||||
$branding = is_array($report['branding'] ?? null) ? $report['branding'] : [];
|
||||
$theme = is_array($report['theme'] ?? null) ? $report['theme'] : [];
|
||||
$layout = is_array($report['layout'] ?? null) ? $report['layout'] : (is_array($theme['layout'] ?? null) ? $theme['layout'] : []);
|
||||
$themeIdentity = is_array($theme['identity'] ?? null) ? $theme['identity'] : [];
|
||||
$branding = $themeIdentity !== [] ? $themeIdentity : (is_array($report['branding'] ?? null) ? $report['branding'] : []);
|
||||
$kpiStrip = is_array($report['kpi_strip'] ?? null) ? $report['kpi_strip'] : [];
|
||||
$managementSummary = is_array($report['management_summary'] ?? null) ? $report['management_summary'] : [];
|
||||
$evidenceBasis = is_array($report['evidence_basis'] ?? null) ? $report['evidence_basis'] : [];
|
||||
$profile = is_array($report['profile'] ?? null) ? $report['profile'] : [];
|
||||
@ -25,6 +29,9 @@
|
||||
$requestedProfile = $sourceMetadata['requested_profile'] ?? ($profile['requested_key'] ?? null);
|
||||
$generatedAt = $report['generated_at'] ?? null;
|
||||
$publishedAt = $report['published_at'] ?? null;
|
||||
$sectionRanks = is_array($layout['section_ranks'] ?? null) ? $layout['section_ranks'] : [];
|
||||
$sectionRank = static fn (string $sectionKey): int => (int) ($sectionRanks[$sectionKey] ?? 90);
|
||||
$sectionStyle = static fn (string $sectionKey): string => 'order: '.$sectionRank($sectionKey).';';
|
||||
@endphp
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
@ -48,13 +55,21 @@
|
||||
body {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(circle at 8% 8%, rgba(20, 83, 45, 0.08), transparent 34%),
|
||||
radial-gradient(circle at 90% 4%, rgba(15, 118, 110, 0.12), transparent 28%),
|
||||
linear-gradient(180deg, #eef7f4 0%, #f8fafc 24%, #e5e7eb 100%);
|
||||
background: linear-gradient(180deg, #eef7f4 0%, #f8fafc 24%, #e5e7eb 100%);
|
||||
font-family: Avenir, "Avenir Next", "Segoe UI", sans-serif;
|
||||
}
|
||||
a { color: inherit; }
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
.page {
|
||||
width: min(1080px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
@ -88,6 +103,10 @@
|
||||
font: 700 12px/1.2 Avenir, "Avenir Next", "Segoe UI", sans-serif;
|
||||
cursor: pointer;
|
||||
}
|
||||
.app-action:focus-visible {
|
||||
outline: 3px solid rgba(15, 118, 110, 0.38);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
.app-action--primary {
|
||||
background: #0f172a;
|
||||
border-color: #0f172a;
|
||||
@ -130,7 +149,7 @@
|
||||
font-family: Georgia, Cambria, "Times New Roman", serif;
|
||||
font-size: clamp(2.6rem, 6vw, 5.2rem);
|
||||
line-height: 0.93;
|
||||
letter-spacing: -0.05em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.hero-summary {
|
||||
max-width: 76ch;
|
||||
@ -154,6 +173,25 @@
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.kpi-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.kpi-card {
|
||||
min-width: 0;
|
||||
border: 1px solid rgba(15, 118, 110, 0.22);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
padding: 15px;
|
||||
}
|
||||
.kpi-value {
|
||||
margin: 8px 0 0;
|
||||
color: var(--ink);
|
||||
font: 800 22px/1.05 Avenir, "Avenir Next", "Segoe UI", sans-serif;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.share-warning {
|
||||
margin-top: 22px;
|
||||
border: 1px solid #fbbf24;
|
||||
@ -204,7 +242,7 @@
|
||||
font-family: Georgia, Cambria, "Times New Roman", serif;
|
||||
font-size: clamp(1.55rem, 3vw, 2.15rem);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.025em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
@ -271,6 +309,10 @@
|
||||
border-radius: 24px;
|
||||
padding: 22px;
|
||||
}
|
||||
.supporting[data-appendix-prominence="high"] {
|
||||
border: 1px solid rgba(15, 118, 110, 0.24);
|
||||
background: #f0fdfa;
|
||||
}
|
||||
.appendix-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
margin-top: 14px;
|
||||
@ -300,11 +342,28 @@
|
||||
break-inside: avoid;
|
||||
}
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.page {
|
||||
width: min(100% - 20px, 1080px);
|
||||
padding-top: 14px;
|
||||
}
|
||||
.cover,
|
||||
.content {
|
||||
padding: 22px;
|
||||
}
|
||||
.report-toolbar {
|
||||
align-items: stretch;
|
||||
}
|
||||
.toolbar-actions,
|
||||
.app-action {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="report-toolbar screen-only" data-testid="rendered-report-toolbar">
|
||||
<div class="report-toolbar screen-only" data-testid="rendered-report-toolbar" aria-label="{{ __('localization.review.rendered_report_toolbar') }}">
|
||||
<div>{{ __('localization.review.rendered_report_html_only') }}</div>
|
||||
|
||||
<div class="toolbar-actions">
|
||||
@ -326,13 +385,18 @@
|
||||
</a>
|
||||
@endif
|
||||
|
||||
<button class="app-action app-action--primary" type="button" onclick="window.print()">
|
||||
<button class="app-action app-action--primary" type="button" onclick="window.print()" data-testid="rendered-report-print-action">
|
||||
{{ __('localization.review.print_rendered_report') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="report-canvas" data-testid="rendered-report-canvas">
|
||||
<main
|
||||
class="report-canvas"
|
||||
data-testid="rendered-report-canvas"
|
||||
data-layout-mode="{{ $layout['mode'] ?? 'internal' }}"
|
||||
data-appendix-prominence="{{ $layout['appendix_prominence'] ?? 'standard' }}"
|
||||
>
|
||||
<section class="cover" data-testid="rendered-report-hero">
|
||||
<div class="co-brand">
|
||||
<span>{{ __('localization.review.prepared_by_for', [
|
||||
@ -351,6 +415,20 @@
|
||||
<span class="badge" style="{{ $boundaryBadgeStyle }}">{{ $report['guidance']['boundary_label'] ?? __('localization.review.requires_review') }}</span>
|
||||
</div>
|
||||
|
||||
@if ($kpiStrip !== [])
|
||||
<section class="kpi-strip" data-testid="rendered-report-kpi-strip" aria-labelledby="rendered-report-kpi-strip-heading">
|
||||
<h2 class="sr-only" id="rendered-report-kpi-strip-heading">{{ __('localization.review.report_kpi_strip') }}</h2>
|
||||
|
||||
@foreach ($kpiStrip as $kpi)
|
||||
<article class="kpi-card" data-kpi-key="{{ $kpi['key'] ?? 'unknown' }}">
|
||||
<p class="field-label">{{ $kpi['label'] ?? __('localization.review.unavailable') }}</p>
|
||||
<p class="kpi-value">{{ $kpi['value'] ?? __('localization.review.unavailable') }}</p>
|
||||
<p class="note">{{ $kpi['description'] ?? '' }}</p>
|
||||
</article>
|
||||
@endforeach
|
||||
</section>
|
||||
@endif
|
||||
|
||||
@if (filled($hero['warning'] ?? null))
|
||||
<div class="share-warning" data-testid="rendered-report-sharing-warning">{{ $hero['warning'] }}</div>
|
||||
@endif
|
||||
@ -384,8 +462,8 @@
|
||||
</section>
|
||||
|
||||
<div class="content">
|
||||
<section class="section" data-testid="rendered-report-executive-summary">
|
||||
<h2>{{ __('localization.review.executive_summary') }}</h2>
|
||||
<section class="section" data-testid="rendered-report-executive-summary" data-section-rank="{{ $sectionRank('executive_summary') }}" style="{{ $sectionStyle('executive_summary') }}" aria-labelledby="rendered-report-executive-summary-heading">
|
||||
<h2 id="rendered-report-executive-summary-heading">{{ __('localization.review.executive_summary') }}</h2>
|
||||
<p class="copy">{{ $report['executive_summary'] }}</p>
|
||||
|
||||
<div class="summary-grid">
|
||||
@ -416,8 +494,8 @@
|
||||
@endif
|
||||
</section>
|
||||
|
||||
<section class="section" data-testid="rendered-report-profile">
|
||||
<h2>{{ __('localization.review.report_profile') }}</h2>
|
||||
<section class="section" data-testid="rendered-report-profile" data-section-rank="{{ $sectionRank('profile') }}" style="{{ $sectionStyle('profile') }}" aria-labelledby="rendered-report-profile-heading">
|
||||
<h2 id="rendered-report-profile-heading">{{ __('localization.review.report_profile') }}</h2>
|
||||
|
||||
<div class="summary-grid">
|
||||
<div class="summary-field">
|
||||
@ -449,8 +527,8 @@
|
||||
</section>
|
||||
|
||||
@if (($report['limitations'] ?? []) !== [])
|
||||
<section class="section" data-testid="rendered-report-output-limitations">
|
||||
<h2>{{ __('localization.review.output_limitations') }}</h2>
|
||||
<section class="section" data-testid="rendered-report-output-limitations" data-section-rank="{{ $sectionRank('limitations') }}" style="{{ $sectionStyle('limitations') }}" aria-labelledby="rendered-report-output-limitations-heading">
|
||||
<h2 id="rendered-report-output-limitations-heading">{{ __('localization.review.output_limitations') }}</h2>
|
||||
<div class="limitations">
|
||||
@foreach (($report['limitations'] ?? []) as $limitation)
|
||||
<article class="limitation">
|
||||
@ -463,8 +541,8 @@
|
||||
</section>
|
||||
@endif
|
||||
|
||||
<section class="section" data-testid="rendered-report-findings">
|
||||
<h2>{{ __('localization.review.findings_and_open_risks') }}</h2>
|
||||
<section class="section" data-testid="rendered-report-findings" data-section-rank="{{ $sectionRank('findings') }}" style="{{ $sectionStyle('findings') }}" aria-labelledby="rendered-report-findings-heading">
|
||||
<h2 id="rendered-report-findings-heading">{{ __('localization.review.findings_and_open_risks') }}</h2>
|
||||
@if (($report['top_findings'] ?? []) === [])
|
||||
<p class="empty-state">{{ __('localization.review.no_open_risks_listed') }}</p>
|
||||
@else
|
||||
@ -481,8 +559,8 @@
|
||||
@endif
|
||||
</section>
|
||||
|
||||
<section class="section" data-testid="rendered-report-accepted-risks">
|
||||
<h2>{{ __('localization.review.accepted_risks') }}</h2>
|
||||
<section class="section" data-testid="rendered-report-accepted-risks" data-section-rank="{{ $sectionRank('accepted_risks') }}" style="{{ $sectionStyle('accepted_risks') }}" aria-labelledby="rendered-report-accepted-risks-heading">
|
||||
<h2 id="rendered-report-accepted-risks-heading">{{ __('localization.review.accepted_risks') }}</h2>
|
||||
@if (($report['accepted_risks'] ?? []) === [])
|
||||
<p class="empty-state">{{ __('localization.review.no_accepted_risks_listed_for_review') }}</p>
|
||||
@else
|
||||
@ -505,8 +583,8 @@
|
||||
@endif
|
||||
</section>
|
||||
|
||||
<section class="section" data-testid="rendered-report-governance-decisions">
|
||||
<h2>{{ __('localization.review.governance_decisions_requiring_awareness') }}</h2>
|
||||
<section class="section" data-testid="rendered-report-governance-decisions" data-section-rank="{{ $sectionRank('governance_decisions') }}" style="{{ $sectionStyle('governance_decisions') }}" aria-labelledby="rendered-report-governance-decisions-heading">
|
||||
<h2 id="rendered-report-governance-decisions-heading">{{ __('localization.review.governance_decisions_requiring_awareness') }}</h2>
|
||||
|
||||
@if (filled($report['decision_summary']['summary'] ?? null))
|
||||
<p class="copy">{{ $report['decision_summary']['summary'] }}</p>
|
||||
@ -531,14 +609,14 @@
|
||||
@endif
|
||||
</section>
|
||||
|
||||
<section class="section" data-testid="rendered-report-evidence-basis">
|
||||
<h2>{{ __('localization.review.evidence_basis') }}</h2>
|
||||
<section class="section" data-testid="rendered-report-evidence-basis" data-section-rank="{{ $sectionRank('evidence_basis') }}" style="{{ $sectionStyle('evidence_basis') }}" aria-labelledby="rendered-report-evidence-basis-heading">
|
||||
<h2 id="rendered-report-evidence-basis-heading">{{ __('localization.review.evidence_basis') }}</h2>
|
||||
<p class="copy">{{ $evidenceBasis['description'] ?? $report['evidence_basis_summary'] }}</p>
|
||||
<p class="note">{{ $evidenceBasis['operator_action'] ?? '' }}</p>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>{{ __('localization.review.next_actions') }}</h2>
|
||||
<section class="section" data-section-rank="{{ $sectionRank('next_actions') }}" style="{{ $sectionStyle('next_actions') }}" aria-labelledby="rendered-report-next-actions-heading">
|
||||
<h2 id="rendered-report-next-actions-heading">{{ __('localization.review.next_actions') }}</h2>
|
||||
@if (($report['next_actions'] ?? []) === [])
|
||||
<p class="empty-state">{{ __('localization.review.no_next_action_listed') }}</p>
|
||||
@else
|
||||
@ -550,8 +628,8 @@
|
||||
@endif
|
||||
</section>
|
||||
|
||||
<section class="section" data-testid="rendered-report-disclosure">
|
||||
<h2>{{ __('localization.review.disclosure_policy') }}</h2>
|
||||
<section class="section" data-testid="rendered-report-disclosure" data-section-rank="{{ $sectionRank('disclosure') }}" style="{{ $sectionStyle('disclosure') }}" aria-labelledby="rendered-report-disclosure-heading">
|
||||
<h2 id="rendered-report-disclosure-heading">{{ __('localization.review.disclosure_policy') }}</h2>
|
||||
|
||||
@if (($disclosurePolicy['blocking_reasons'] ?? []) !== [])
|
||||
<div class="limitations">
|
||||
@ -590,8 +668,8 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section supporting" data-testid="rendered-report-supporting-appendix">
|
||||
<h2>{{ __('localization.review.supporting_appendix') }}</h2>
|
||||
<section class="section supporting" data-testid="rendered-report-supporting-appendix" data-section-rank="{{ $sectionRank('appendix') }}" style="{{ $sectionStyle('appendix') }}" data-appendix-prominence="{{ $layout['appendix_prominence'] ?? 'standard' }}" aria-labelledby="rendered-report-supporting-appendix-heading">
|
||||
<h2 id="rendered-report-supporting-appendix-heading">{{ __('localization.review.supporting_appendix') }}</h2>
|
||||
<p class="copy">{{ __('localization.review.rendered_report_appendix_note') }}</p>
|
||||
|
||||
@if (($disclosurePolicy['show_section_appendix'] ?? false) !== true)
|
||||
|
||||
@ -193,6 +193,7 @@ function spec347BrowserCreatePublishedReviewWithPack(
|
||||
[
|
||||
'control_interpretation' => [
|
||||
'version_key' => ComplianceEvidenceMappingV1::VERSION_KEY,
|
||||
'non_certification_disclosure' => 'TenantPilot summarizes available service-delivery evidence for governance review. This report is not a certification, legal attestation, audit opinion, or compliance guarantee.',
|
||||
'controls' => [
|
||||
[
|
||||
'control_key' => 'customer-output',
|
||||
@ -244,6 +245,7 @@ function spec347BrowserCreatePublishedReviewWithPack(
|
||||
'include_pii' => false,
|
||||
'include_operations' => true,
|
||||
], $packOptions),
|
||||
'summary' => is_array($review->summary) ? $review->summary : $summary,
|
||||
'file_path' => $filePath,
|
||||
'file_disk' => 'exports',
|
||||
'generated_at' => now()->subMinutes(4),
|
||||
|
||||
@ -0,0 +1,332 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\EnvironmentReview;
|
||||
use App\Models\EnvironmentReviewSection;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Models\User;
|
||||
use App\Services\ReviewPackService;
|
||||
use App\Support\Evidence\EvidenceCompletenessState;
|
||||
use App\Support\ReviewPacks\ReportProfileRegistry;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
pest()->browser()->timeout(90_000);
|
||||
|
||||
beforeEach(function (): void {
|
||||
Storage::fake('exports');
|
||||
});
|
||||
|
||||
it('Spec366 smokes management report layout variants and print behavior', function (): void {
|
||||
[$user, $customerEnvironment, $customerReview, $customerPack] = spec366BrowserCreateLayoutPack(
|
||||
environmentName: 'Spec366 Browser Customer',
|
||||
customerSafeReady: true,
|
||||
);
|
||||
[$limitedUser, $limitedEnvironment, $limitedReview, $limitedPack] = spec366BrowserCreateLayoutPack(
|
||||
user: $user,
|
||||
workspaceId: (int) $customerEnvironment->workspace_id,
|
||||
environmentName: 'Spec366 Browser Limited',
|
||||
customerSafeReady: true,
|
||||
partialEvidence: true,
|
||||
);
|
||||
[$internalUser, $internalEnvironment, $internalReview, $internalPack] = spec366BrowserCreateLayoutPack(
|
||||
user: $user,
|
||||
workspaceId: (int) $customerEnvironment->workspace_id,
|
||||
environmentName: 'Spec366 Browser Internal',
|
||||
customerSafeReady: true,
|
||||
packOverrides: [
|
||||
'options' => [
|
||||
'include_pii' => true,
|
||||
'include_operations' => true,
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
spec366BrowserAuthenticate($this, $user, $customerEnvironment);
|
||||
|
||||
$customerExecutivePage = visit(spec366BrowserRenderedReportUrl($customerPack, $customerReview, ReportProfileRegistry::CUSTOMER_EXECUTIVE))
|
||||
->resize(1280, 1440)
|
||||
->waitForText(__('localization.review.report_profile_customer_executive'))
|
||||
->assertSee(__('localization.review.report_kpi_key_risks'))
|
||||
->assertSee(__('localization.review.report_kpi_open_decisions'))
|
||||
->assertSee('Spec366 Browser Control')
|
||||
->assertSee(__('localization.review.report_appendix_hidden_for_profile'))
|
||||
->assertDontSee('Spec366 Browser Technical Appendix')
|
||||
->assertScript('document.querySelector("[data-testid=\"rendered-report-canvas\"]").dataset.layoutMode === "executive"', true)
|
||||
->assertScript('Boolean(document.querySelector("[data-testid=\"rendered-report-toolbar\"]").compareDocumentPosition(document.querySelector("[data-testid=\"rendered-report-canvas\"]")) & Node.DOCUMENT_POSITION_FOLLOWING)', true)
|
||||
->assertScript('(() => { const control = document.querySelector("[data-testid=\"rendered-report-toolbar\"] a, [data-testid=\"rendered-report-print-action\"]"); control?.focus(); return document.activeElement === control; })()', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
$customerExecutivePage->screenshot(true, spec366BrowserScreenshotName('01-customer-executive-report'));
|
||||
spec366CopyBrowserScreenshot('01-customer-executive-report');
|
||||
|
||||
$customerExecutivePage->script('document.body.classList.add("print-preview-smoke"); window.scrollTo(0, 0);');
|
||||
$customerExecutivePage
|
||||
->assertScript('window.getComputedStyle(document.querySelector("[data-testid=\"rendered-report-toolbar\"]")).display === "none"', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"rendered-report-canvas\"]").offsetParent !== null', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"rendered-report-disclosure\"]") instanceof HTMLElement', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
$customerExecutivePage->screenshot(true, spec366BrowserScreenshotName('06-print-view'));
|
||||
spec366CopyBrowserScreenshot('06-print-view');
|
||||
$customerExecutivePage->screenshot(false, spec366BrowserScreenshotName('07-report-toolbar-hidden-print'));
|
||||
spec366CopyBrowserScreenshot('07-report-toolbar-hidden-print');
|
||||
$customerExecutivePage->script('document.body.classList.remove("print-preview-smoke");');
|
||||
|
||||
visit(spec366BrowserRenderedReportUrl($limitedPack, $limitedReview, ReportProfileRegistry::CUSTOMER_EXECUTIVE))
|
||||
->resize(1280, 1440)
|
||||
->waitForText(__('localization.review.report_state_with_limitations'))
|
||||
->assertSee(__('localization.review.report_external_sharing_warning'))
|
||||
->assertSee(__('localization.review.output_limitations'))
|
||||
->assertScript('document.querySelector("[data-testid=\"rendered-report-canvas\"]").dataset.layoutMode === "executive"', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->screenshot(true, spec366BrowserScreenshotName('02-customer-executive-limited-report'));
|
||||
spec366CopyBrowserScreenshot('02-customer-executive-limited-report');
|
||||
|
||||
visit(spec366BrowserRenderedReportUrl($internalPack, $internalReview, ReportProfileRegistry::INTERNAL_MSP_REVIEW))
|
||||
->resize(1280, 1440)
|
||||
->waitForText(__('localization.review.report_profile_internal_msp_review'))
|
||||
->assertSee(__('localization.review.report_state_internal_with_limitations'))
|
||||
->assertSee('Spec366 Browser Technical Appendix')
|
||||
->assertScript('document.querySelector("[data-testid=\"rendered-report-canvas\"]").dataset.layoutMode === "internal"', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->screenshot(true, spec366BrowserScreenshotName('03-internal-msp-report'));
|
||||
spec366CopyBrowserScreenshot('03-internal-msp-report');
|
||||
|
||||
visit(spec366BrowserRenderedReportUrl($customerPack, $customerReview, ReportProfileRegistry::CUSTOMER_TECHNICAL))
|
||||
->resize(390, 1100)
|
||||
->waitForText(__('localization.review.report_profile_customer_technical'))
|
||||
->assertSee(__('localization.review.report_kpi_key_risks'))
|
||||
->assertSee('Spec366 Browser Technical Appendix')
|
||||
->assertScript('document.querySelector("[data-testid=\"rendered-report-canvas\"]").dataset.layoutMode === "technical"', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->screenshot(true, spec366BrowserScreenshotName('04-customer-technical-report'));
|
||||
spec366CopyBrowserScreenshot('04-customer-technical-report');
|
||||
|
||||
visit(spec366BrowserRenderedReportUrl($customerPack, $customerReview, ReportProfileRegistry::AUDITOR_APPENDIX))
|
||||
->resize(1280, 1440)
|
||||
->waitForText(__('localization.review.report_profile_auditor_appendix'))
|
||||
->assertSee(__('localization.review.report_audience_controlled_auditor'))
|
||||
->assertSee('Spec366 Browser Technical Appendix')
|
||||
->assertScript('document.querySelector("[data-testid=\"rendered-report-canvas\"]").dataset.layoutMode === "auditor_appendix"', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"rendered-report-canvas\"]").dataset.appendixProminence === "high"', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->screenshot(true, spec366BrowserScreenshotName('05-auditor-appendix-report'));
|
||||
spec366CopyBrowserScreenshot('05-auditor-appendix-report');
|
||||
|
||||
expect($limitedUser)->toBeInstanceOf(User::class)
|
||||
->and($limitedEnvironment)->toBeInstanceOf(ManagedEnvironment::class)
|
||||
->and($internalUser)->toBeInstanceOf(User::class)
|
||||
->and($internalEnvironment)->toBeInstanceOf(ManagedEnvironment::class);
|
||||
});
|
||||
|
||||
function spec366BrowserScreenshotName(string $name): string
|
||||
{
|
||||
return $name;
|
||||
}
|
||||
|
||||
function spec366CopyBrowserScreenshot(string $name): void
|
||||
{
|
||||
$filename = spec366BrowserScreenshotName($name).'.png';
|
||||
$primarySource = base_path('tests/Browser/Screenshots/'.$filename);
|
||||
$fallbackSource = \Pest\Browser\Support\Screenshot::path($filename);
|
||||
$targetDirectories = array_values(array_unique([
|
||||
repo_path('specs/366-management-report-layout-branded-report-themes-v1/artifacts/screenshots'),
|
||||
base_path('specs/366-management-report-layout-branded-report-themes-v1/artifacts/screenshots'),
|
||||
]));
|
||||
|
||||
foreach ($targetDirectories as $targetDirectory) {
|
||||
if (! is_dir($targetDirectory)) {
|
||||
@mkdir($targetDirectory, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
$source = null;
|
||||
|
||||
for ($attempt = 0; $attempt < 50 && $source === null; $attempt++) {
|
||||
foreach ([$primarySource, $fallbackSource] as $candidate) {
|
||||
if (is_file($candidate)) {
|
||||
$source = $candidate;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($source !== null) {
|
||||
break;
|
||||
}
|
||||
|
||||
usleep(100_000);
|
||||
clearstatcache(true, $primarySource);
|
||||
clearstatcache(true, $fallbackSource);
|
||||
}
|
||||
|
||||
if (is_string($source) && is_file($source)) {
|
||||
foreach ($targetDirectories as $targetDirectory) {
|
||||
if (is_dir($targetDirectory) && is_writable($targetDirectory)) {
|
||||
@copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$name.'.png');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function spec366BrowserAuthenticate(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(),
|
||||
]);
|
||||
|
||||
setAdminPanelContext($environment);
|
||||
}
|
||||
|
||||
function spec366BrowserRenderedReportUrl(ReviewPack $pack, EnvironmentReview $review, string $profile): string
|
||||
{
|
||||
return app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
|
||||
'source_surface' => 'review_pack',
|
||||
'review_id' => (int) $review->getKey(),
|
||||
'interpretation_version' => $review->controlInterpretationVersion(),
|
||||
ReportProfileRegistry::QUERY_PARAMETER => $profile,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $packOverrides
|
||||
* @return array{0:User,1:ManagedEnvironment,2:EnvironmentReview,3:ReviewPack}
|
||||
*/
|
||||
function spec366BrowserCreateLayoutPack(
|
||||
?User $user = null,
|
||||
?int $workspaceId = null,
|
||||
string $environmentName = 'Spec366 Browser Environment',
|
||||
bool $customerSafeReady = false,
|
||||
bool $partialEvidence = false,
|
||||
?array $packOverrides = [],
|
||||
): array {
|
||||
$environment = ManagedEnvironment::factory()->active()->create([
|
||||
'workspace_id' => $workspaceId,
|
||||
'name' => $environmentName,
|
||||
]);
|
||||
|
||||
if ($user === null) {
|
||||
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'manager');
|
||||
} else {
|
||||
createUserWithTenant(tenant: $environment, user: $user, role: 'owner', workspaceRole: 'manager');
|
||||
}
|
||||
|
||||
$environment->workspace?->forceFill(['name' => 'Spec366 Browser MSP'])->save();
|
||||
$environment = $environment->fresh('workspace');
|
||||
$snapshot = seedEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0);
|
||||
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
|
||||
$review->forceFill([
|
||||
'status' => 'published',
|
||||
'published_at' => now(),
|
||||
'published_by_user_id' => (int) $user->getKey(),
|
||||
])->save();
|
||||
|
||||
if ($customerSafeReady) {
|
||||
$review = markEnvironmentReviewCustomerSafeReady($review);
|
||||
}
|
||||
|
||||
if ($partialEvidence) {
|
||||
restateEnvironmentReviewEvidenceSnapshot($review->evidenceSnapshot, EvidenceCompletenessState::Partial);
|
||||
$review = $review->fresh(['sections', 'evidenceSnapshot']);
|
||||
}
|
||||
|
||||
$review->loadMissing('sections');
|
||||
$appendixSection = $review->sections->first();
|
||||
|
||||
if ($appendixSection instanceof EnvironmentReviewSection) {
|
||||
$appendixSection->forceFill([
|
||||
'render_payload' => array_replace_recursive(
|
||||
is_array($appendixSection->render_payload) ? $appendixSection->render_payload : [],
|
||||
[
|
||||
'entries' => [
|
||||
[
|
||||
'title' => 'Spec366 Browser Technical Appendix',
|
||||
'summary' => 'Visible in technical, internal, and auditor layouts.',
|
||||
],
|
||||
],
|
||||
'highlights' => ['Spec366 browser appendix highlight.'],
|
||||
],
|
||||
),
|
||||
])->save();
|
||||
}
|
||||
|
||||
$filePath = 'review-packs/'.($environment->external_id ?: 'spec366').'/browser-report.zip';
|
||||
Storage::disk('exports')->put($filePath, 'PK-spec366-browser-report');
|
||||
|
||||
$summary = array_replace_recursive([
|
||||
'governance_package' => [
|
||||
'executive_summary' => 'Spec 366 browser management report.',
|
||||
'top_findings' => [
|
||||
[
|
||||
'title' => 'Spec366 Browser Control',
|
||||
'summary' => 'A management-visible control requires owner awareness.',
|
||||
],
|
||||
],
|
||||
'accepted_risks' => [],
|
||||
'decision_summary' => [
|
||||
'status' => 'open',
|
||||
'summary' => 'A governance decision still requires stakeholder awareness.',
|
||||
'next_action' => 'Confirm the owner review date.',
|
||||
'entries' => [
|
||||
[
|
||||
'title' => 'Confirm owner review',
|
||||
'summary' => 'Owner review remains the next governance step.',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'control_interpretation' => [
|
||||
'non_certification_disclosure' => 'Spec 366 browser non-certification disclosure.',
|
||||
],
|
||||
'recommended_next_actions' => [
|
||||
'Confirm the owner review date.',
|
||||
],
|
||||
'delivery_bundle' => [
|
||||
'executive_entrypoint_file' => 'executive-summary.md',
|
||||
'appendix_files' => ['metadata.json', 'summary.json', 'sections.json'],
|
||||
],
|
||||
], is_array($packOverrides['summary'] ?? null) ? $packOverrides['summary'] : []);
|
||||
|
||||
$pack = ReviewPack::factory()->ready()->create(array_merge([
|
||||
'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(),
|
||||
'options' => [
|
||||
'include_pii' => false,
|
||||
'include_operations' => true,
|
||||
],
|
||||
'summary' => $summary,
|
||||
'file_path' => $filePath,
|
||||
'file_disk' => 'exports',
|
||||
'generated_at' => now()->subMinutes(3),
|
||||
'expires_at' => now()->addDay(),
|
||||
], $packOverrides ?? []));
|
||||
|
||||
$review->forceFill([
|
||||
'current_export_review_pack_id' => (int) $pack->getKey(),
|
||||
])->save();
|
||||
|
||||
return [$user, $environment, $review->fresh(['sections', 'evidenceSnapshot', 'currentExportReviewPack']), $pack->fresh()];
|
||||
}
|
||||
@ -67,6 +67,7 @@
|
||||
->and($reviewPack?->environment_review_id)->toBe((int) $publishedReview->getKey());
|
||||
|
||||
Storage::disk('exports')->assertExists((string) $reviewPack->file_path);
|
||||
assertSeededReviewPackIsValidZip((string) Storage::disk('exports')->path((string) $reviewPack->file_path));
|
||||
|
||||
$this->actingAs($user)->withSession([
|
||||
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
|
||||
@ -148,3 +149,15 @@ function tenantpilotReviewOutputFixtureRelativeAdminRedirect(string $url): strin
|
||||
|
||||
return $path.$query.$fragment;
|
||||
}
|
||||
|
||||
function assertSeededReviewPackIsValidZip(string $path): void
|
||||
{
|
||||
$zip = new \ZipArchive();
|
||||
|
||||
expect($zip->open($path))->toBeTrue()
|
||||
->and($zip->locateName('executive-summary.md'))->not->toBeFalse()
|
||||
->and($zip->locateName('metadata.json'))->not->toBeFalse()
|
||||
->and($zip->locateName('summary.json'))->not->toBeFalse();
|
||||
|
||||
$zip->close();
|
||||
}
|
||||
|
||||
@ -5,11 +5,15 @@
|
||||
use App\Filament\Resources\BaselineProfileResource;
|
||||
use App\Filament\Resources\BaselineProfileResource\Pages\ViewBaselineProfile;
|
||||
use App\Jobs\CaptureBaselineSnapshotJob;
|
||||
use App\Livewire\BulkOperationProgress;
|
||||
use App\Models\BaselineProfile;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Notifications\OperationRunQueued;
|
||||
use App\Support\Baselines\BaselineCaptureMode;
|
||||
use App\Support\OperationRunLinks;
|
||||
use App\Support\OperationRunType;
|
||||
use App\Support\OpsUx\OpsUxBrowserEvents;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
@ -106,6 +110,7 @@ function seedCaptureProfileForTenant(
|
||||
->assertActionHasLabel('capture', 'Capture baseline (full content)')
|
||||
->assertActionEnabled('capture')
|
||||
->callAction('capture', data: ['source_environment_id' => (int) $tenant->getKey()])
|
||||
->assertDispatchedTo(BulkOperationProgress::class, OpsUxBrowserEvents::RunEnqueued, tenantId: (int) $tenant->getKey())
|
||||
->assertStatus(200);
|
||||
|
||||
$topLevelActionNames = collect(baselineProfileCaptureHeaderActions($component))
|
||||
@ -129,6 +134,19 @@ function seedCaptureProfileForTenant(
|
||||
expect($run)->not->toBeNull();
|
||||
expect($run?->status)->toBe('queued');
|
||||
expect(data_get($run?->context, 'baseline_capture.inventory_sync_run_id'))->toBe((int) $inventorySyncRun->getKey());
|
||||
|
||||
$this->assertDatabaseHas('notifications', [
|
||||
'notifiable_id' => $user->getKey(),
|
||||
'notifiable_type' => $user->getMorphClass(),
|
||||
'type' => OperationRunQueued::class,
|
||||
'data->format' => 'filament',
|
||||
'data->title' => 'Baseline capture queued',
|
||||
]);
|
||||
|
||||
$notification = $user->notifications()->latest('id')->first();
|
||||
|
||||
expect(data_get($notification?->data, 'actions.0.url'))->toBe(OperationRunLinks::view($run, $tenant))
|
||||
->and(data_get($notification?->data, 'actions.0.target'))->toBe('admin_operation_run');
|
||||
});
|
||||
|
||||
it('shows the shared capture block on the start surface when no credible inventory basis exists', function (): void {
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
|
||||
use App\Jobs\CaptureBaselineSnapshotJob;
|
||||
use App\Jobs\CompareBaselineToTenantJob;
|
||||
use App\Livewire\BulkOperationProgress;
|
||||
use App\Models\BaselineProfile;
|
||||
use App\Models\BaselineSnapshot;
|
||||
use App\Models\OperationRun;
|
||||
use App\Notifications\OperationRunQueued;
|
||||
use App\Support\Baselines\BaselineCaptureMode;
|
||||
use App\Support\Baselines\BaselineEvidenceResumeToken;
|
||||
use App\Support\OperationRunLinks;
|
||||
use App\Support\OperationRunOutcome;
|
||||
use App\Support\OperationRunStatus;
|
||||
use App\Support\OperationRunType;
|
||||
use App\Support\OpsUx\OpsUxBrowserEvents;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Livewire\Livewire;
|
||||
|
||||
@ -52,6 +57,7 @@
|
||||
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
||||
->assertActionVisible('resumeCapture')
|
||||
->callAction('resumeCapture')
|
||||
->assertDispatchedTo(BulkOperationProgress::class, OpsUxBrowserEvents::RunEnqueued, tenantId: (int) $tenant->getKey())
|
||||
->assertStatus(200);
|
||||
|
||||
Queue::assertPushed(CompareBaselineToTenantJob::class);
|
||||
@ -66,4 +72,72 @@
|
||||
expect($resumed)->not->toBeNull();
|
||||
$context = is_array($resumed?->context) ? $resumed->context : [];
|
||||
expect($context['baseline_compare']['resume_token'] ?? null)->toBe($token);
|
||||
|
||||
$this->assertDatabaseHas('notifications', [
|
||||
'notifiable_id' => $user->getKey(),
|
||||
'notifiable_type' => $user->getMorphClass(),
|
||||
'type' => OperationRunQueued::class,
|
||||
'data->format' => 'filament',
|
||||
'data->title' => 'Baseline compare queued',
|
||||
]);
|
||||
});
|
||||
|
||||
it('notifies and refreshes operation activity after resuming a baseline capture run', function (): void {
|
||||
Queue::fake();
|
||||
config()->set('tenantpilot.baselines.full_content_capture.enabled', true);
|
||||
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$profile = BaselineProfile::factory()->active()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'capture_mode' => BaselineCaptureMode::FullContent->value,
|
||||
]);
|
||||
|
||||
$token = BaselineEvidenceResumeToken::encode(['offset' => 1]);
|
||||
|
||||
$run = OperationRun::factory()->for($tenant)->create([
|
||||
'type' => OperationRunType::BaselineCapture->value,
|
||||
'status' => OperationRunStatus::Completed->value,
|
||||
'outcome' => OperationRunOutcome::PartiallySucceeded->value,
|
||||
'user_id' => (int) $user->getKey(),
|
||||
'context' => [
|
||||
'baseline_profile_id' => (int) $profile->getKey(),
|
||||
'source_environment_id' => (int) $tenant->getKey(),
|
||||
'effective_scope' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
|
||||
'capture_mode' => BaselineCaptureMode::FullContent->value,
|
||||
'baseline_capture' => [
|
||||
'resume_token' => $token,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
||||
->assertActionVisible('resumeCapture')
|
||||
->callAction('resumeCapture')
|
||||
->assertDispatchedTo(BulkOperationProgress::class, OpsUxBrowserEvents::RunEnqueued, tenantId: (int) $tenant->getKey())
|
||||
->assertStatus(200);
|
||||
|
||||
Queue::assertPushed(CaptureBaselineSnapshotJob::class);
|
||||
|
||||
$resumed = OperationRun::query()
|
||||
->where('managed_environment_id', (int) $tenant->getKey())
|
||||
->where('type', OperationRunType::BaselineCapture->value)
|
||||
->where('status', OperationRunStatus::Queued->value)
|
||||
->latest('id')
|
||||
->first();
|
||||
|
||||
expect($resumed)->not->toBeNull();
|
||||
$context = is_array($resumed?->context) ? $resumed->context : [];
|
||||
expect($context['baseline_capture']['resume_token'] ?? null)->toBe($token);
|
||||
|
||||
$notification = $user->notifications()
|
||||
->where('type', OperationRunQueued::class)
|
||||
->latest('id')
|
||||
->first();
|
||||
|
||||
expect($notification)->not->toBeNull()
|
||||
->and(data_get($notification?->data, 'title'))->toBe('Baseline capture queued')
|
||||
->and(data_get($notification?->data, 'actions.0.url'))->toBe(OperationRunLinks::view($resumed, $tenant))
|
||||
->and(data_get($notification?->data, 'actions.0.target'))->toBe('admin_operation_run');
|
||||
});
|
||||
|
||||
@ -32,7 +32,10 @@ function createReadyPackWithFile(?array $packOverrides = []): array
|
||||
[$user, $tenant] = createUserWithTenant();
|
||||
|
||||
$filePath = 'review-packs/'.$tenant->external_id.'/test.zip';
|
||||
Storage::disk('exports')->put($filePath, 'PK-fake-zip-content');
|
||||
$zipContents = reviewPackDownloadTestZipContents([
|
||||
'executive-summary.md' => 'Ready review pack download fixture.',
|
||||
]);
|
||||
Storage::disk('exports')->put($filePath, $zipContents);
|
||||
|
||||
$pack = ReviewPack::factory()->ready()->create(array_merge([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
@ -40,12 +43,57 @@ function createReadyPackWithFile(?array $packOverrides = []): array
|
||||
'initiated_by_user_id' => (int) $user->getKey(),
|
||||
'file_path' => $filePath,
|
||||
'file_disk' => 'exports',
|
||||
'sha256' => hash('sha256', 'PK-fake-zip-content'),
|
||||
'file_size' => strlen($zipContents),
|
||||
'sha256' => hash('sha256', $zipContents),
|
||||
], $packOverrides));
|
||||
|
||||
return [$user, $tenant, $pack];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $files
|
||||
*/
|
||||
function reviewPackDownloadTestZipContents(array $files = []): string
|
||||
{
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'review-pack-download-test-');
|
||||
|
||||
if ($tempFile === false) {
|
||||
throw new RuntimeException('Failed to allocate a temporary review pack test archive.');
|
||||
}
|
||||
|
||||
try {
|
||||
$zip = new \ZipArchive();
|
||||
$result = $zip->open($tempFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
|
||||
|
||||
if ($result !== true) {
|
||||
throw new RuntimeException("Failed to create review pack test archive: error code {$result}");
|
||||
}
|
||||
|
||||
foreach (array_replace([
|
||||
'metadata.json' => json_encode(['fixture' => 'review-pack-download-test'], JSON_THROW_ON_ERROR),
|
||||
'summary.json' => json_encode(['status' => 'ready'], JSON_THROW_ON_ERROR),
|
||||
], $files) as $filename => $contents) {
|
||||
$zip->addFromString($filename, $contents);
|
||||
}
|
||||
|
||||
if ($zip->close() !== true) {
|
||||
throw new RuntimeException('Failed to finalize review pack test archive.');
|
||||
}
|
||||
|
||||
$contents = file_get_contents($tempFile);
|
||||
|
||||
if (! is_string($contents) || $contents === '') {
|
||||
throw new RuntimeException('Failed to read review pack test archive contents.');
|
||||
}
|
||||
|
||||
return $contents;
|
||||
} finally {
|
||||
if (file_exists($tempFile)) {
|
||||
unlink($tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createCurrentReviewPackForRenderedReport(
|
||||
?array $packOverrides = [],
|
||||
bool $customerSafeReady = false,
|
||||
@ -68,7 +116,10 @@ function createCurrentReviewPackForRenderedReport(
|
||||
}
|
||||
|
||||
$filePath = 'review-packs/'.$tenant->external_id.'/rendered-report.zip';
|
||||
Storage::disk('exports')->put($filePath, 'PK-rendered-report-content');
|
||||
$zipContents = reviewPackDownloadTestZipContents([
|
||||
'executive-summary.md' => 'Rendered report download fixture.',
|
||||
]);
|
||||
Storage::disk('exports')->put($filePath, $zipContents);
|
||||
|
||||
$summary = array_replace_recursive([
|
||||
'governance_package' => [
|
||||
@ -106,7 +157,8 @@ function createCurrentReviewPackForRenderedReport(
|
||||
'summary' => $summary,
|
||||
'file_path' => $filePath,
|
||||
'file_disk' => 'exports',
|
||||
'sha256' => hash('sha256', 'PK-rendered-report-content'),
|
||||
'file_size' => strlen($zipContents),
|
||||
'sha256' => hash('sha256', $zipContents),
|
||||
'expires_at' => now()->addDay(),
|
||||
], $packOverrides);
|
||||
$packAttributes['summary'] = $summary;
|
||||
|
||||
@ -0,0 +1,397 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\EnvironmentReview;
|
||||
use App\Models\EnvironmentReviewSection;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Services\ReviewPackService;
|
||||
use App\Support\Audit\AuditActionId;
|
||||
use App\Support\Evidence\EvidenceCompletenessState;
|
||||
use App\Support\ReviewPacks\ReportProfileRegistry;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
Storage::fake('exports');
|
||||
});
|
||||
|
||||
it('renders a management-ready first screen with repo-backed identity, KPIs, and safe copy', function (): void {
|
||||
[$user, $tenant, $review, $pack] = spec366CreateRenderedReportPack(
|
||||
packOverrides: [
|
||||
'summary' => [
|
||||
'governance_package' => [
|
||||
'executive_summary' => 'Spec 366 executive narrative for management readers.',
|
||||
'top_findings' => [
|
||||
[
|
||||
'title' => 'Conditional access drift',
|
||||
'summary' => 'One policy drift requires owner awareness.',
|
||||
],
|
||||
],
|
||||
'accepted_risks' => [
|
||||
[
|
||||
'title' => 'Migration exception',
|
||||
'governance_state' => 'valid_exception',
|
||||
'customer_safe_summary' => 'The exception is time-bound and governance-tracked.',
|
||||
'owner_label' => 'Service Delivery',
|
||||
'expires_at' => '2026-07-15',
|
||||
],
|
||||
],
|
||||
'decision_summary' => [
|
||||
'status' => 'open',
|
||||
'summary' => 'One governance decision requires owner awareness.',
|
||||
'next_action' => 'Brief named stakeholders and retain the structured appendix.',
|
||||
'entries' => [
|
||||
[
|
||||
'title' => 'Retain monitoring exception',
|
||||
'summary' => 'Owner accepted limited monitoring until the next review.',
|
||||
'next_action' => 'Confirm the next review date.',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'recommended_next_actions' => [
|
||||
'Brief named stakeholders and retain the ZIP package as the structured appendix.',
|
||||
],
|
||||
],
|
||||
],
|
||||
customerSafeReady: true,
|
||||
);
|
||||
|
||||
$signedUrl = spec366RenderedReportUrl($pack, $review, ReportProfileRegistry::CUSTOMER_EXECUTIVE);
|
||||
bindFailHardGraphClient();
|
||||
$packCount = ReviewPack::query()->count();
|
||||
$operationRunCount = OperationRun::query()->count();
|
||||
|
||||
$response = $this->actingAs($user)->get($signedUrl);
|
||||
$content = (string) $response->getContent();
|
||||
$toolbarPosition = strpos($content, 'data-testid="rendered-report-toolbar"');
|
||||
$canvasPosition = strpos($content, 'data-testid="rendered-report-canvas"');
|
||||
|
||||
$response->assertOk()
|
||||
->assertSee('Spec 366 executive narrative for management readers.')
|
||||
->assertSee('Prepared by Spec366 MSP for Spec366 Production')
|
||||
->assertSee('Generated by TenantPilot')
|
||||
->assertSee('Customer-safe report ready')
|
||||
->assertSee('Report decision strip')
|
||||
->assertSee('Governance status')
|
||||
->assertSee('Evidence coverage')
|
||||
->assertSee('Key risks')
|
||||
->assertSee('Open decisions')
|
||||
->assertSee('Conditional access drift')
|
||||
->assertSee('Retain monitoring exception')
|
||||
->assertSee('Migration exception')
|
||||
->assertSee('Audience')
|
||||
->assertSee('Non-certification disclosure')
|
||||
->assertSee('Supporting appendix')
|
||||
->assertSee('data-layout-mode="executive"', false)
|
||||
->assertSee('data-appendix-prominence="minimal"', false)
|
||||
->assertSee('data-testid="rendered-report-kpi-strip"', false)
|
||||
->assertSee('body.print-preview-smoke .report-toolbar', false)
|
||||
->assertSee('@media print', false)
|
||||
->assertDontSee('localization.')
|
||||
->assertDontSee('Certified report')
|
||||
->assertDontSee('Approved compliance report')
|
||||
->assertDontSee('Share with customer')
|
||||
->assertDontSee((string) $review->fingerprint);
|
||||
|
||||
expect($toolbarPosition)->not->toBeFalse()
|
||||
->and($canvasPosition)->not->toBeFalse()
|
||||
->and($toolbarPosition)->toBeLessThan($canvasPosition)
|
||||
->and(AuditLog::query()->where('action', AuditActionId::ReviewPackDownloaded->value)->count())->toBe(0)
|
||||
->and(ReviewPack::query()->count())->toBe($packCount)
|
||||
->and(OperationRun::query()->count())->toBe($operationRunCount);
|
||||
});
|
||||
|
||||
it('keeps limited and PII-bearing report states visibly bounded', function (): void {
|
||||
[$limitedUser, $limitedTenant, $limitedReview, $limitedPack] = spec366CreateRenderedReportPack(customerSafeReady: true);
|
||||
restateEnvironmentReviewEvidenceSnapshot($limitedReview->evidenceSnapshot, EvidenceCompletenessState::Partial);
|
||||
|
||||
$limitedResponse = $this->actingAs($limitedUser)->get(
|
||||
spec366RenderedReportUrl(
|
||||
$limitedPack->fresh(['environmentReview']),
|
||||
$limitedReview->fresh(['sections', 'evidenceSnapshot', 'currentExportReviewPack']),
|
||||
ReportProfileRegistry::CUSTOMER_EXECUTIVE,
|
||||
),
|
||||
);
|
||||
|
||||
$limitedResponse->assertOk()
|
||||
->assertSee('Report with limitations')
|
||||
->assertSee('Do not share externally before review.')
|
||||
->assertSee('Output limitations')
|
||||
->assertSee('Review or refresh the evidence basis before external sharing.')
|
||||
->assertSee('data-layout-mode="executive"', false)
|
||||
->assertSee('data-section-rank="30"', false)
|
||||
->assertDontSee('Customer-ready report')
|
||||
->assertDontSee('Certified report')
|
||||
->assertDontSee('Approved compliance report')
|
||||
->assertDontSee('Share with customer')
|
||||
->assertDontSee('localization.');
|
||||
|
||||
expect($limitedTenant)->toBeInstanceOf(ManagedEnvironment::class);
|
||||
|
||||
[$piiUser, $piiTenant, $piiReview, $piiPack] = spec366CreateRenderedReportPack(
|
||||
packOverrides: [
|
||||
'options' => [
|
||||
'include_pii' => true,
|
||||
'include_operations' => true,
|
||||
],
|
||||
],
|
||||
customerSafeReady: true,
|
||||
environmentName: 'Spec366 PII Production',
|
||||
);
|
||||
|
||||
$this->actingAs($piiUser)
|
||||
->get(spec366RenderedReportUrl($piiPack, $piiReview, ReportProfileRegistry::CUSTOMER_EXECUTIVE))
|
||||
->assertOk()
|
||||
->assertSee('Internal report with limitations')
|
||||
->assertSee('Do not share externally before review.')
|
||||
->assertSee('Customer-facing profile blocked by internal-only detail')
|
||||
->assertSee('Protected values boundary')
|
||||
->assertSee('data-layout-mode="executive"', false)
|
||||
->assertDontSee('Customer-safe report ready')
|
||||
->assertDontSee('Certified report')
|
||||
->assertDontSee('Approved compliance report')
|
||||
->assertDontSee('Share with customer')
|
||||
->assertDontSee('localization.');
|
||||
|
||||
expect($piiTenant)->toBeInstanceOf(ManagedEnvironment::class);
|
||||
});
|
||||
|
||||
it('renders profile-aware hierarchy and keeps fallback requests visible', function (): void {
|
||||
[$user, $tenant, $review, $pack] = spec366CreateRenderedReportPack(customerSafeReady: true);
|
||||
|
||||
$profiles = [
|
||||
ReportProfileRegistry::CUSTOMER_EXECUTIVE => [
|
||||
'mode' => 'executive',
|
||||
'prominence' => 'minimal',
|
||||
'label' => 'Customer executive',
|
||||
'shows_appendix' => false,
|
||||
'fallback' => false,
|
||||
],
|
||||
ReportProfileRegistry::CUSTOMER_TECHNICAL => [
|
||||
'mode' => 'technical',
|
||||
'prominence' => 'standard',
|
||||
'label' => 'Customer technical',
|
||||
'shows_appendix' => true,
|
||||
'fallback' => false,
|
||||
],
|
||||
ReportProfileRegistry::INTERNAL_MSP_REVIEW => [
|
||||
'mode' => 'internal',
|
||||
'prominence' => 'standard',
|
||||
'label' => 'Internal MSP review',
|
||||
'shows_appendix' => true,
|
||||
'fallback' => false,
|
||||
],
|
||||
ReportProfileRegistry::AUDITOR_APPENDIX => [
|
||||
'mode' => 'auditor_appendix',
|
||||
'prominence' => 'high',
|
||||
'label' => 'Auditor appendix',
|
||||
'shows_appendix' => true,
|
||||
'fallback' => false,
|
||||
],
|
||||
ReportProfileRegistry::FRAMEWORK_READINESS => [
|
||||
'mode' => 'internal',
|
||||
'prominence' => 'standard',
|
||||
'label' => 'Internal MSP review',
|
||||
'shows_appendix' => true,
|
||||
'fallback' => true,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($profiles as $requestedProfile => $expectation) {
|
||||
$response = $this->actingAs($user)->get(spec366RenderedReportUrl($pack, $review, $requestedProfile));
|
||||
|
||||
$response->assertOk()
|
||||
->assertSee($expectation['label'])
|
||||
->assertSee('data-layout-mode="'.$expectation['mode'].'"', false)
|
||||
->assertSee('data-appendix-prominence="'.$expectation['prominence'].'"', false)
|
||||
->assertDontSee('localization.');
|
||||
|
||||
if ($expectation['shows_appendix']) {
|
||||
$response->assertSee('Spec366 Technical Control')
|
||||
->assertDontSee(__('localization.review.report_appendix_hidden_for_profile'));
|
||||
} else {
|
||||
$response->assertSee(__('localization.review.report_appendix_hidden_for_profile'))
|
||||
->assertDontSee('Spec366 Technical Control');
|
||||
}
|
||||
|
||||
if ($expectation['fallback']) {
|
||||
$response->assertSee(__('localization.review.report_profile_fallback_notice'))
|
||||
->assertSee(ReportProfileRegistry::FRAMEWORK_READINESS)
|
||||
->assertSee(ReportProfileRegistry::INTERNAL_MSP_REVIEW);
|
||||
}
|
||||
}
|
||||
|
||||
expect($tenant)->toBeInstanceOf(ManagedEnvironment::class);
|
||||
});
|
||||
|
||||
it('preserves rendered route guards and the Review Pack ZIP download contract', function (): void {
|
||||
[$owner, $tenant, $review, $pack] = spec366CreateRenderedReportPack(customerSafeReady: true);
|
||||
$renderedUrl = spec366RenderedReportUrl($pack, $review, ReportProfileRegistry::CUSTOMER_EXECUTIVE);
|
||||
$downloadUrl = app(ReviewPackService::class)->generateDownloadUrl($pack, [
|
||||
'source_surface' => 'review_pack',
|
||||
'review_id' => (int) $review->getKey(),
|
||||
'interpretation_version' => $review->controlInterpretationVersion(),
|
||||
]);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->get($renderedUrl)
|
||||
->assertOk();
|
||||
|
||||
$this->actingAs($owner)
|
||||
->get($downloadUrl)
|
||||
->assertOk()
|
||||
->assertDownload();
|
||||
|
||||
[$outsider] = createUserWithTenant();
|
||||
|
||||
$this->actingAs($outsider)
|
||||
->get($renderedUrl)
|
||||
->assertNotFound();
|
||||
|
||||
[$guardUser, $guardTenant, $guardReview, $guardPack] = spec366CreateRenderedReportPack(
|
||||
customerSafeReady: true,
|
||||
environmentName: 'Spec366 Guard Production',
|
||||
);
|
||||
$guardUrl = spec366RenderedReportUrl($guardPack, $guardReview, ReportProfileRegistry::CUSTOMER_EXECUTIVE);
|
||||
$guardReview->forceFill(['current_export_review_pack_id' => null])->save();
|
||||
|
||||
$this->actingAs($guardUser)
|
||||
->get($guardUrl)
|
||||
->assertNotFound();
|
||||
|
||||
[$expiredUser, $expiredTenant, $expiredReview, $expiredPack] = spec366CreateRenderedReportPack(
|
||||
customerSafeReady: true,
|
||||
environmentName: 'Spec366 Expired Production',
|
||||
);
|
||||
$expiredUrl = spec366RenderedReportUrl($expiredPack, $expiredReview, ReportProfileRegistry::CUSTOMER_EXECUTIVE);
|
||||
$expiredPack->forceFill(['expires_at' => now()->subMinute()])->save();
|
||||
|
||||
$this->actingAs($expiredUser)
|
||||
->get($expiredUrl)
|
||||
->assertNotFound();
|
||||
|
||||
expect($tenant)->toBeInstanceOf(ManagedEnvironment::class)
|
||||
->and($guardTenant)->toBeInstanceOf(ManagedEnvironment::class)
|
||||
->and($expiredTenant)->toBeInstanceOf(ManagedEnvironment::class);
|
||||
});
|
||||
|
||||
function spec366RenderedReportUrl(ReviewPack $pack, EnvironmentReview $review, string $profile): string
|
||||
{
|
||||
return app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
|
||||
'source_surface' => 'review_pack',
|
||||
'review_id' => (int) $review->getKey(),
|
||||
'interpretation_version' => $review->controlInterpretationVersion(),
|
||||
ReportProfileRegistry::QUERY_PARAMETER => $profile,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $packOverrides
|
||||
* @return array{0:\App\Models\User,1:ManagedEnvironment,2:EnvironmentReview,3:ReviewPack}
|
||||
*/
|
||||
function spec366CreateRenderedReportPack(
|
||||
?array $packOverrides = [],
|
||||
bool $customerSafeReady = false,
|
||||
?EvidenceSnapshot $snapshot = null,
|
||||
string $environmentName = 'Spec366 Production',
|
||||
): array {
|
||||
$packOverrides ??= [];
|
||||
$tenant = ManagedEnvironment::factory()->active()->create([
|
||||
'name' => $environmentName,
|
||||
]);
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner', workspaceRole: 'manager');
|
||||
$tenant->workspace?->forceFill(['name' => 'Spec366 MSP'])->save();
|
||||
$tenant = $tenant->fresh('workspace');
|
||||
$snapshot ??= seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
|
||||
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
|
||||
$review->forceFill([
|
||||
'status' => 'published',
|
||||
'published_at' => now(),
|
||||
'published_by_user_id' => (int) $user->getKey(),
|
||||
])->save();
|
||||
|
||||
if ($customerSafeReady) {
|
||||
$review = markEnvironmentReviewCustomerSafeReady($review);
|
||||
}
|
||||
|
||||
$review->loadMissing('sections');
|
||||
$appendixSection = $review->sections->first();
|
||||
|
||||
if ($appendixSection instanceof EnvironmentReviewSection) {
|
||||
$appendixSection->forceFill([
|
||||
'render_payload' => array_replace_recursive(
|
||||
is_array($appendixSection->render_payload) ? $appendixSection->render_payload : [],
|
||||
[
|
||||
'entries' => [
|
||||
[
|
||||
'title' => 'Spec366 Technical Control',
|
||||
'summary' => 'Visible only when the profile allows detailed appendix content.',
|
||||
],
|
||||
],
|
||||
'highlights' => ['Spec366 appendix highlight.'],
|
||||
],
|
||||
),
|
||||
])->save();
|
||||
}
|
||||
|
||||
$filePath = 'review-packs/'.$tenant->external_id.'/spec366-rendered-report.zip';
|
||||
Storage::disk('exports')->put($filePath, 'PK-spec366-rendered-report-content');
|
||||
|
||||
$summary = array_replace_recursive([
|
||||
'governance_package' => [
|
||||
'executive_summary' => 'Spec 366 management report summary.',
|
||||
'evidence_basis_summary' => 'The report is anchored to the current released evidence snapshot.',
|
||||
'top_findings' => [],
|
||||
'accepted_risks' => [],
|
||||
'decision_summary' => [
|
||||
'status' => 'none',
|
||||
'summary' => '',
|
||||
'next_action' => '',
|
||||
'entries' => [],
|
||||
],
|
||||
],
|
||||
'control_interpretation' => [
|
||||
'non_certification_disclosure' => 'TenantPilot summarizes available service-delivery evidence for governance review. This report is not a certification, legal attestation, audit opinion, or compliance guarantee.',
|
||||
],
|
||||
'recommended_next_actions' => [],
|
||||
'delivery_bundle' => [
|
||||
'executive_entrypoint_file' => 'executive-summary.md',
|
||||
'appendix_files' => ['metadata.json', 'summary.json', 'sections.json'],
|
||||
],
|
||||
], is_array($packOverrides['summary'] ?? null) ? $packOverrides['summary'] : []);
|
||||
|
||||
$packAttributes = array_merge([
|
||||
'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(),
|
||||
'options' => [
|
||||
'include_pii' => false,
|
||||
'include_operations' => true,
|
||||
],
|
||||
'summary' => $summary,
|
||||
'file_path' => $filePath,
|
||||
'file_disk' => 'exports',
|
||||
'sha256' => hash('sha256', 'PK-spec366-rendered-report-content'),
|
||||
'generated_at' => now()->subMinutes(5),
|
||||
'expires_at' => now()->addDay(),
|
||||
], $packOverrides);
|
||||
$packAttributes['summary'] = $summary;
|
||||
|
||||
$pack = ReviewPack::factory()->ready()->create($packAttributes);
|
||||
|
||||
$review->forceFill([
|
||||
'current_export_review_pack_id' => (int) $pack->getKey(),
|
||||
])->save();
|
||||
|
||||
return [$user, $tenant, $review->fresh(['sections', 'evidenceSnapshot', 'currentExportReviewPack']), $pack->fresh()];
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Support\ReviewPacks\ReportProfileRegistry;
|
||||
use App\Support\ReviewPacks\ReportThemeResolver;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
it('derives text-only report identity from existing names and safe defaults', function (): void {
|
||||
$generatedAt = Carbon::parse('2026-06-08 09:30:00', 'UTC');
|
||||
|
||||
$identity = ReportThemeResolver::identityFor(
|
||||
workspaceName: 'Contoso MSP',
|
||||
environmentName: 'Fabrikam Production',
|
||||
generatedAt: $generatedAt,
|
||||
);
|
||||
|
||||
expect($identity['prepared_by'])->toBe('Contoso MSP')
|
||||
->and($identity['prepared_for'])->toBe('Fabrikam Production')
|
||||
->and($identity['generated_by'])->toBe('TenantPilot')
|
||||
->and($identity['generated_at'])->toBe($generatedAt)
|
||||
->and($identity['accent'])->toBe(ReportThemeResolver::DEFAULT_ACCENT)
|
||||
->and($identity['logo'])->toBeNull();
|
||||
});
|
||||
|
||||
it('falls back without creating persisted theme or logo truth', function (): void {
|
||||
$identity = ReportThemeResolver::identityFor(
|
||||
workspaceName: ' ',
|
||||
environmentName: null,
|
||||
);
|
||||
|
||||
expect($identity['prepared_by'])->toBe('TenantPilot')
|
||||
->and($identity['prepared_for'])->toBe(__('localization.review.tenant'))
|
||||
->and($identity['generated_by'])->toBe('TenantPilot')
|
||||
->and($identity['generated_at'])->toBeNull()
|
||||
->and($identity['accent'])->toBe('teal')
|
||||
->and($identity['logo'])->toBeNull();
|
||||
});
|
||||
|
||||
it('maps implemented report profiles to bounded layout modes and section ranks', function (): void {
|
||||
$executive = ReportThemeResolver::layoutForProfile([
|
||||
'effective_key' => ReportProfileRegistry::CUSTOMER_EXECUTIVE,
|
||||
]);
|
||||
$technical = ReportThemeResolver::layoutForProfile([
|
||||
'effective_key' => ReportProfileRegistry::CUSTOMER_TECHNICAL,
|
||||
]);
|
||||
$internal = ReportThemeResolver::layoutForProfile([
|
||||
'effective_key' => ReportProfileRegistry::INTERNAL_MSP_REVIEW,
|
||||
]);
|
||||
$auditor = ReportThemeResolver::layoutForProfile([
|
||||
'effective_key' => ReportProfileRegistry::AUDITOR_APPENDIX,
|
||||
]);
|
||||
|
||||
expect($executive['mode'])->toBe('executive')
|
||||
->and($executive['appendix_prominence'])->toBe('minimal')
|
||||
->and($executive['section_ranks']['executive_summary'])->toBeLessThan($executive['section_ranks']['appendix'])
|
||||
->and($technical['mode'])->toBe('technical')
|
||||
->and($technical['section_ranks']['evidence_basis'])->toBeLessThan($technical['section_ranks']['appendix'])
|
||||
->and($internal['mode'])->toBe('internal')
|
||||
->and($internal['section_ranks']['limitations'])->toBeLessThan($internal['section_ranks']['appendix'])
|
||||
->and($auditor['mode'])->toBe('auditor_appendix')
|
||||
->and($auditor['appendix_prominence'])->toBe('high')
|
||||
->and($auditor['section_ranks']['appendix'])->toBeLessThan($auditor['section_ranks']['executive_summary']);
|
||||
});
|
||||
|
||||
it('builds a KPI strip from measured repo truth and marks missing metrics as not measured', function (): void {
|
||||
$kpis = ReportThemeResolver::kpiStrip(
|
||||
guidance: [
|
||||
'boundary_label' => 'Customer safe',
|
||||
'primary_reason' => 'Evidence and disclosure proof are available.',
|
||||
],
|
||||
evidenceBasis: [
|
||||
'label' => 'Complete',
|
||||
'description' => 'Evidence snapshot #12 is complete.',
|
||||
],
|
||||
metrics: [
|
||||
'top_findings' => [
|
||||
['title' => 'Risk one'],
|
||||
['title' => 'Risk two'],
|
||||
],
|
||||
'top_findings_measured' => true,
|
||||
'governance_decisions_measured' => false,
|
||||
],
|
||||
);
|
||||
|
||||
expect($kpis)->toHaveCount(4)
|
||||
->and($kpis[0]['key'])->toBe('governance_status')
|
||||
->and($kpis[0]['value'])->toBe('Customer safe')
|
||||
->and($kpis[1]['key'])->toBe('evidence_coverage')
|
||||
->and($kpis[1]['value'])->toBe('Complete')
|
||||
->and($kpis[2]['key'])->toBe('key_risks')
|
||||
->and($kpis[2]['value'])->toBe('2')
|
||||
->and($kpis[3]['key'])->toBe('open_decisions')
|
||||
->and($kpis[3]['value'])->toBe(__('localization.review.report_kpi_not_measured'));
|
||||
});
|
||||
@ -8,9 +8,9 @@ ## Summary
|
||||
| --- | ---: | --- |
|
||||
| UI route/page inventory rows | 99 | Includes dynamic route families and utility/auth endpoints. |
|
||||
| Unique page reports | 20 | `page-reports/*.md`; some inventory rows intentionally share existing reports where routes resolve to the same surface. |
|
||||
| Desktop screenshots | 15 | Route-inventory-linked desktop evidence, including strategic runtime captures and blocker evidence screenshots. |
|
||||
| Desktop screenshots | 16 | Route-inventory-linked desktop evidence, including strategic runtime captures, blocker evidence screenshots, and the Spec 366 rendered-report capture. |
|
||||
| Tablet screenshots | 0 | Deferred to later strategic mockup/implementation specs. |
|
||||
| Mobile screenshots | 0 | Deferred to later strategic mockup/implementation specs. |
|
||||
| Mobile screenshots | 1 | Spec 366 adds mobile-ish rendered-report evidence for the customer technical profile; broader mobile coverage remains deferred. |
|
||||
| Strategic Surface rows | 45 | Individual target treatment or explicit product decision required. |
|
||||
| Domain Pattern Surface rows | 45 | Can be handled through grouped pattern specs unless later evidence raises risk. |
|
||||
| Design-System Cleanup Surface rows | 7 | Tables/forms/states/copy cleanup, no individual target mockup expected by default. |
|
||||
@ -53,7 +53,7 @@ ## Coverage By Area
|
||||
| Monitoring | 9 | Operations hub and alert delivery landing captured; record details and config forms remain pattern/manual review. |
|
||||
| Inventory | 8 | Route-discovered only; coverage, policy version detail, and raw-data exposure need later review. |
|
||||
| Evidence / audit | 8 | Audit log captured; evidence/report detail routes need customer-safe progressive-disclosure review. |
|
||||
| Reviews | 7 | Review register, customer workspace, review pack detail, and the rendered-report route now have bounded browser evidence; deeper evidence/report surfaces still remain open elsewhere. |
|
||||
| Reviews | 7 | Review register, customer workspace, review pack detail, and the rendered-report route now have bounded browser evidence; Spec 366 adds rendered-report profile, print, and mobile-ish captures while deeper evidence/report surfaces still remain open elsewhere. |
|
||||
| Backup / restore | 6 | High-risk area; backup sets and restore runs were blocked by fixture capability. |
|
||||
| Settings / admin | 5 | Workspace and environment access are RBAC-sensitive and need later review. |
|
||||
| Provider / integration | 5 | Provider connections and required permissions are captured; create/edit/onboarding remain high-risk unresolved surfaces. |
|
||||
|
||||
@ -8,8 +8,8 @@ # UI-099 Rendered Review Report
|
||||
| Archetype | Reviews |
|
||||
| Design depth | Strategic Surface |
|
||||
| Repo truth | repo-verified |
|
||||
| Screenshot | `specs/357-report-profiles-disclosure-policy-v1/artifacts/screenshots/spec357-in-app-browser-customer-executive.png` |
|
||||
| Browser status | Reached in the live in-app browser on 2026-06-05 via the Spec 351 review-output fixture plus a signed rendered-report URL; verified HTML-first chrome, effective profile metadata, disclosure-proof badges, and appendix hiding for `customer_executive`. |
|
||||
| Screenshot | `specs/366-management-report-layout-branded-report-themes-v1/artifacts/screenshots/01-customer-executive-report.png` |
|
||||
| Browser status | Reached in the Pest browser on 2026-06-08 via Spec 366 fixtures and signed rendered-report URLs; verified management-first cover, text co-branding, KPI strip, profile-aware hierarchy, disclosure-proof badges, appendix hiding for `customer_executive`, technical/auditor appendix visibility, mobile-ish stacking, and print-toolbar hiding. |
|
||||
|
||||
## First Five Seconds
|
||||
|
||||
@ -23,6 +23,7 @@ ## First Five Seconds
|
||||
## Productization Review
|
||||
|
||||
- Decision-first: the hero and guidance badges summarize stakeholder-safe posture before appendix detail.
|
||||
- Management-first: the cover includes prepared-by/prepared-for identity, generated-by metadata, governance status, evidence coverage, key-risk count, and open-decision count before detail sections.
|
||||
- Evidence-first: evidence basis, governance decisions, accepted risks, and technical details stay visible in bounded sections.
|
||||
- Audience-first: the route now states effective profile, requested profile, audience boundary, and source surface before appendix detail.
|
||||
- Truth-over-presentation: profile-specific filtering may hide appendix detail, but it may not hide readiness, evidence state, disclosure state, or non-certification copy.
|
||||
@ -58,6 +59,15 @@ ## Spec 357 Follow-up
|
||||
- disclosure proof is split into `verified`, `assumed`, `missing`, `unknown`, and `not_applicable` states instead of silently upgrading stored booleans to verified truth
|
||||
- appendix and technical-detail visibility now depend on the effective profile and current internal-only / PII boundary
|
||||
|
||||
## Spec 366 Follow-up
|
||||
|
||||
Spec 366 productizes the same route as a management-ready report surface:
|
||||
|
||||
- text-only co-branding is derived from existing workspace/environment truth; no logo upload, theme persistence, or editor UI was added
|
||||
- the KPI/decision strip is derived only from stored review-pack, environment-review, evidence, profile, and disclosure data; unsupported metrics remain omitted or marked not measured
|
||||
- profile layout modes now drive section prominence: executive/customer profiles lead with the management story, technical profiles pull evidence earlier, and auditor profiles make evidence/disclosure/appendix content more prominent
|
||||
- browser evidence now covers customer executive ready, customer executive limited, internal MSP, customer technical mobile-ish width, auditor appendix, and print-preview toolbar hiding
|
||||
|
||||
## Target Direction
|
||||
|
||||
Keep this report calm, bounded, and print-friendly. Future follow-up should focus on browser evidence and hierarchy polish, not on a second rendering runtime or a broader delivery taxonomy.
|
||||
|
||||
@ -49,7 +49,7 @@ # Route Inventory
|
||||
| UI-041 | `/admin/workspaces/{workspace}/environments/{environment}/review-packs` | resource | Review Packs | Reviews | environment-bound | route exists | environment entitlement | Reviews | Evidence / Audit | Domain Pattern Surface | repo-verified | - | - | Export artifact list. |
|
||||
| UI-042 | `/admin/workspaces/{workspace}/environments/{environment}/review-packs/{record}` | resource | Review Pack Detail | Reviews | environment record | route exists | environment + record entitlement | Reviews | Evidence / Audit | Strategic Surface | repo-verified | - | [report](page-reports/ui-042-review-pack-detail.md) | Spec 356 makes rendered-report preview the primary inspect affordance while ZIP download and regenerate remain secondary/operator-scoped. |
|
||||
| UI-043 | `/admin/review-packs/{reviewPack}/download` | controller | Review Pack Download | Reviews | workspace/environment artifact | route exists | download authorization expected | Reviews | Evidence / Audit | Design-System Cleanup Surface | repo-verified | - | - | Action endpoint, not page; include in coverage due customer artifact impact. |
|
||||
| UI-099 | `/admin/review-packs/{reviewPack}/report` | controller | Rendered Review Report | Reviews | workspace/environment artifact | route exists | signed review-pack view access plus current-export / ready / not-expired authority | Reviews | Evidence / Audit | Strategic Surface | repo-verified | - | [report](page-reports/ui-099-rendered-review-report.md) | Spec 356 adds an HTML-first stakeholder report route derived from the current review-pack contract; it is read-only and current-pack-only. |
|
||||
| UI-099 | `/admin/review-packs/{reviewPack}/report` | controller | Rendered Review Report | Reviews | workspace/environment artifact | route exists | signed review-pack view access plus current-export / ready / not-expired authority | Reviews | Evidence / Audit | Strategic Surface | repo-verified | [desktop](../../specs/366-management-report-layout-branded-report-themes-v1/artifacts/screenshots/01-customer-executive-report.png) | [report](page-reports/ui-099-rendered-review-report.md) | Spec 366 adds management-first cover, text co-branding, KPI strip, profile-aware hierarchy, and print/mobile-ish browser evidence while keeping the route read-only and current-pack-only. |
|
||||
| UI-044 | `/admin/evidence/overview` | route + page | Evidence Overview | Evidence / audit | workspace hub | route exists | workspace member | Evidence / Audit | Reviews | Strategic Surface | repo-verified | - | - | Workspace-wide evidence landing. |
|
||||
| UI-045 | `/admin/workspaces/{workspace}/environments/{environment}/evidence` | resource | Evidence Snapshots | Evidence / audit | environment-bound | route exists | environment entitlement | Evidence / Audit | Reviews | Domain Pattern Surface | repo-verified | - | - | Environment evidence list. |
|
||||
| UI-046 | `/admin/workspaces/{workspace}/environments/{environment}/evidence/{record}` | resource | Evidence Snapshot Detail | Evidence / audit | environment record | route exists | environment + record entitlement | Evidence / Audit | Support / Diagnostics | Strategic Surface | repo-verified | - | - | Raw/support evidence must stay progressively disclosed. |
|
||||
|
||||
@ -22,6 +22,7 @@ ### Explicit delta in this plan
|
||||
|
||||
- add canonical operator-safe phase metadata and label derivation for `baseline_capture` and `baseline_compare`
|
||||
- add one bounded composite summary path for `tenant.review.compose` from current aggregate operation truth
|
||||
- ensure workspace/canonical baseline capture, compare, and resume launch paths pass the concrete tenant id into the existing `run-enqueued` event and opt newly created human-initiated baseline capture/resume runs into the existing queued database notification path
|
||||
- keep counted, terminal, and generic activity semantics unchanged except for consuming more truthful non-counted detail
|
||||
- document the narrowed candidate boundary explicitly: review-pack and evidence-snapshot overlap remain with Spec 271, while provider health and support diagnostics remain deferred until repo-real queued progress truth exists
|
||||
|
||||
@ -120,9 +121,9 @@ ## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: yes, for active progress meaning only
|
||||
- **Central contract reused**: existing OperationRun Start UX Contract plus `OperationRunProgressContract`
|
||||
- **Delegated UX behaviors**: queued toast, canonical run links, `run-enqueued` event, and terminal-notification lifecycle remain delegated and unchanged
|
||||
- **Delegated UX behaviors**: queued toast, canonical run links, `run-enqueued` event, and terminal-notification lifecycle remain delegated. Workspace/canonical baseline launch and resume surfaces pass the concrete tenant id into the existing event path because `Filament::getTenant()` is unavailable on those routes.
|
||||
- **Surface-owned behavior kept local**: current launch inputs, detailed diagnostics, and domain-specific result explanations stay local to their current surfaces
|
||||
- **Queued DB-notification policy**: `N/A` - unchanged
|
||||
- **Queued DB-notification policy**: explicit opt-in only. Baseline capture start and baseline evidence resume reuse the existing `OperationRunQueued` database notification via `OperationRunService`; no new notification class, queue policy, or sidebar notification path is introduced.
|
||||
- **Terminal notification path**: unchanged central lifecycle mechanism
|
||||
- **Exception path**: none
|
||||
|
||||
@ -257,4 +258,4 @@ ## Proportionality Review
|
||||
- **Narrowest correct implementation**: update only selected repo-real phase/composite families and keep all other work on current generic fallback or counted semantics.
|
||||
- **Ownership cost created**: selected context writes in existing jobs/services, focused tests, and one standards update.
|
||||
- **Alternative intentionally rejected**: a workflow engine, child-run graph, or fake percentage model was rejected because all would be structurally heavier and less truthful than the current-release need.
|
||||
- **Release truth**: current-release truth. The repo already contains the contract, the shell adopter, the baseline phase hints, and the tenant-review aggregate truth needed for this rollout.
|
||||
- **Release truth**: current-release truth. The repo already contains the contract, the shell adopter, the baseline phase hints, and the tenant-review aggregate truth needed for this rollout.
|
||||
|
||||
@ -51,9 +51,9 @@ ## OperationRun UX Impact *(mandatory)*
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: yes
|
||||
- **Shared OperationRun UX contract/layer reused**: existing OperationRun Start UX Contract plus `App\Support\OpsUx\OperationRunProgressContract` and `App\Support\OpsUx\OperationUxPresenter`
|
||||
- **Delegated start/completion UX behaviors**: queued toast wording, canonical `View operation` links, tenant-safe URL resolution, current `run-enqueued` browser events, and existing terminal notifications remain delegated to the current shared path and are unchanged in this slice
|
||||
- **Delegated start/completion UX behaviors**: queued toast wording, canonical `View operation` links, tenant-safe URL resolution, current `run-enqueued` browser events, and existing terminal notifications remain delegated to the current shared path. Workspace/canonical baseline launch and resume surfaces must pass the concrete tenant id into the existing event path because `Filament::getTenant()` is absent outside tenant-scoped routes.
|
||||
- **Local surface-owned behavior that remains**: current baseline and tenant-review launch inputs plus current run-detail diagnostics stay local to their existing surfaces; non-counted progress semantics do not
|
||||
- **Queued DB-notification policy**: `N/A` - unchanged
|
||||
- **Queued DB-notification policy**: explicit opt-in only. Baseline capture start and baseline evidence resume reuse the existing `OperationRunQueued` database notification via `OperationRunService`; no new notification class, queue policy, or sidebar notification path is introduced.
|
||||
- **Terminal notification path**: unchanged central lifecycle mechanism
|
||||
- **Exception required?**: none
|
||||
|
||||
@ -228,6 +228,8 @@ ### Functional Requirements
|
||||
- **FR-012**: This slice MUST NOT add new `summary_counts` keys, new progress capabilities, a new dashboard card, a child-run graph, a workflow engine, or provider health/support-diagnostics progress behavior.
|
||||
- **FR-013**: This slice MUST NOT reopen review-pack or evidence-snapshot progress semantics owned by Spec 271 unless current repo truth proves they still need non-counted treatment in a later follow-up spec.
|
||||
- **FR-014**: The feature MUST update `docs/ui/tenantpilot-enterprise-ui-standards.md` so maintainers can see which current run families may claim phase/composite progress and which families remain activity-only, counted, or deferred.
|
||||
- **FR-015**: Workspace/canonical baseline capture, compare, and resume launch surfaces MUST pass the concrete tenant id into the existing `run-enqueued` event so tenant-bound operation activity feedback refreshes even when `Filament::getTenant()` is null.
|
||||
- **FR-016**: Newly created human-initiated baseline capture and baseline evidence resume runs MUST use `OperationRunService::dispatchOrFail(..., emitQueuedNotification: true)` so the existing queued sidebar database notification path is written consistently.
|
||||
|
||||
### Authorization and Safety Requirements
|
||||
|
||||
@ -307,4 +309,4 @@ ## Risks
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None blocking safe implementation. If a selected run family cannot produce stable operator-safe non-counted detail without speculative copy, that family must drop back to current generic fallback behavior rather than widening the slice.
|
||||
- None blocking safe implementation. If a selected run family cannot produce stable operator-safe non-counted detail without speculative copy, that family must drop back to current generic fallback behavior rather than widening the slice.
|
||||
|
||||
@ -130,6 +130,9 @@ ## Phase 7: Polish & Cross-Cutting Validation
|
||||
- [x] T022 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/OpsUx/ActivityFeedbackSurfaceTest.php tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php tests/Feature/BaselineDriftEngine/CaptureBaselineContentTest.php tests/Feature/Baselines/BaselineCaptureTest.php tests/Feature/Baselines/BaselineCompareResumeTokenTest.php tests/Feature/Baselines/BaselineCompareExecutionGuardTest.php tests/Feature/TenantReview/TenantReviewOperationsUxTest.php tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php tests/Feature/Operations/TenantlessOperationRunViewerTest.php tests/Feature/TenantReview/TenantReviewRbacTest.php tests/Feature/Baselines/BaselineProfileAuthorizationTest.php`.
|
||||
- [x] T023 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` for touched platform files.
|
||||
- [x] T024 [P] Review touched code to confirm Filament stays on Livewire v4, provider registration remains unchanged in `apps/platform/bootstrap/providers.php`, no new assets were registered, no new globally searchable resource behavior was introduced, no new parallel polling loop was added, and the full precedence chain `phased > composite > counted > activity` still holds with counted mode requiring truthful `processed` plus `total`.
|
||||
- [x] T025 [P] [Bugfix] Update `apps/platform/app/Support/OpsUx/OpsUxBrowserEvents.php`, baseline profile actions, and canonical operation-detail resume actions so workspace/canonical baseline capture, compare, and resume starts pass the concrete tenant id into the existing `run-enqueued` event.
|
||||
- [x] T026 [P] [Bugfix] Update `apps/platform/app/Services/Baselines/BaselineCaptureService.php` and `apps/platform/app/Services/Baselines/BaselineEvidenceCaptureResumeService.php` so newly created human-initiated baseline capture and evidence-resume runs use `OperationRunService::dispatchOrFail(..., emitQueuedNotification: true)`.
|
||||
- [x] T027 [P] [Bugfix] Extend `apps/platform/tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php` and `apps/platform/tests/Feature/Filament/OperationRunResumeCaptureActionTest.php` to prove tenant-bound progress refresh events and queued sidebar database notifications for baseline capture, compare, and resume starts.
|
||||
|
||||
---
|
||||
|
||||
@ -189,4 +192,4 @@ ## Deferred Follow-Ups / Non-Goals
|
||||
- review-pack or evidence-snapshot non-counted progress overlap with Spec 271
|
||||
- child-run graph persistence through `child_run_ids` or `operation_run_ids`
|
||||
- `273 - Tenant Dashboard Active Operations Summary Card`
|
||||
- any workflow-engine or AI-generated progress explanation layer
|
||||
- any workflow-engine or AI-generated progress explanation layer
|
||||
|
||||
|
After Width: | Height: | Size: 623 KiB |
|
After Width: | Height: | Size: 666 KiB |
|
After Width: | Height: | Size: 935 KiB |
|
After Width: | Height: | Size: 779 KiB |
|
After Width: | Height: | Size: 908 KiB |
|
After Width: | Height: | Size: 601 KiB |
|
After Width: | Height: | Size: 398 KiB |
@ -0,0 +1,77 @@
|
||||
# Requirements Checklist: Spec 366 - Management Report Layout & Branded Report Themes v1
|
||||
|
||||
**Purpose**: Validate the preparation quality for the Spec 366 package before implementation.
|
||||
**Created**: 2026-06-08
|
||||
**Feature**: `366-management-report-layout-branded-report-themes-v1`
|
||||
**Scope**: Preparation only. No application implementation was performed while completing this checklist.
|
||||
|
||||
## Candidate Selection Gate
|
||||
|
||||
- [x] CHK001 Selected candidate exists in user-provided material: "Spec 366 - Management Report Layout & Branded Report Themes v1".
|
||||
- [x] CHK002 Selected candidate is not already covered by an existing Spec 366 package.
|
||||
- [x] CHK003 Related Specs 356 and 357 were checked for completed-spec signals and treated as historical context only.
|
||||
- [x] CHK004 Candidate aligns with current report/productization direction after the rendered report and profile/disclosure policy foundations.
|
||||
- [x] CHK005 Candidate is scoped as a bounded implementation-ready slice over the existing authenticated Review Pack rendered-report route.
|
||||
- [x] CHK006 Close alternatives are explicitly deferred: scheduled delivery, compliance framework reports, customer portal consumption, native PDF, and AI/HITL review.
|
||||
- [x] CHK007 Smallest viable implementation slice is stated: management-ready rendered report layout plus bounded text co-branding/theme derivation.
|
||||
|
||||
**Result**: PASS
|
||||
|
||||
## Spec Readiness Gate
|
||||
|
||||
- [x] CHK008 `spec.md`, `plan.md`, `tasks.md`, and this checklist exist.
|
||||
- [x] CHK009 Problem statement, user value, smallest version, and explicit non-goals are present.
|
||||
- [x] CHK010 Functional requirements, non-functional requirements, acceptance criteria, and success criteria are stated.
|
||||
- [x] CHK011 Assumptions and open questions are documented; no open question blocks implementation.
|
||||
- [x] CHK012 Plan identifies likely affected repo surfaces and does not introduce a new report engine, native PDF stack, delivery flow, portal, AI, persisted theme, or upload UI.
|
||||
- [x] CHK013 Tasks are ordered by setup, tests, user stories, validation, and close-out.
|
||||
- [x] CHK014 Tasks include explicit Unit, Feature, Browser, regression, formatting, and diff validation commands.
|
||||
- [x] CHK015 RBAC, workspace/environment isolation, evidence/result truth, disclosure policy, and customer-safe rendering requirements are addressed.
|
||||
- [x] CHK016 OperationRun semantics are explicitly N/A and existing Review Pack generation behavior is protected.
|
||||
- [x] CHK017 Provider boundary is explicitly N/A for new provider coupling; no Graph/provider render call is permitted.
|
||||
- [x] CHK018 UI/Productization coverage and screenshot requirements are addressed.
|
||||
- [x] CHK019 Scope is small enough for a later bounded implementation loop.
|
||||
|
||||
**Result**: PASS
|
||||
|
||||
## Filament v5 / Livewire v4 Checklist
|
||||
|
||||
- [x] CHK020 Livewire v4.0+ compliance is stated; no Livewire v3 API is planned.
|
||||
- [x] CHK021 No panel provider change is planned; Laravel 12 Filament providers remain in `apps/platform/bootstrap/providers.php`.
|
||||
- [x] CHK022 No globally searchable resource change is planned; if implementation unexpectedly touches global search, View/Edit safety or disabled search must be verified.
|
||||
- [x] CHK023 No destructive/high-impact action is planned; report toolbar actions remain read-only navigation/download/print.
|
||||
- [x] CHK024 Asset strategy is stated: no new registered Filament assets expected, and `filament:assets` deployment impact is required only if assets are unexpectedly registered.
|
||||
- [x] CHK025 Testing plan names the pages/surfaces/actions covered by Unit, Feature, and Browser tests.
|
||||
|
||||
**Result**: PASS
|
||||
|
||||
## Prep Analysis Checklist
|
||||
|
||||
- [x] CHK026 Spec does not contain unresolved template placeholders.
|
||||
- [x] CHK027 Plan does not contain unresolved template placeholders.
|
||||
- [x] CHK028 Tasks do not contain unresolved template placeholders.
|
||||
- [x] CHK029 Candidate Selection Gate is captured in both `spec.md` and `plan.md`.
|
||||
- [x] CHK030 Completed-spec guardrail result is captured in `spec.md` and `plan.md`.
|
||||
- [x] CHK031 Tasks include tests before implementation work.
|
||||
- [x] CHK032 Tasks include guardrails preventing out-of-scope runtime additions.
|
||||
- [x] CHK033 Tasks include explicit validation and close-out work.
|
||||
- [x] CHK034 Preparation artifacts only changed Spec 366 files.
|
||||
|
||||
**Result**: PASS. `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` passed for the Spec 366 feature directory after task generation.
|
||||
|
||||
## Prep Analysis Fixes Applied
|
||||
|
||||
- [x] CHK041 Aligned `plan.md` with `tasks.md` by making `repo-truth-map.md` a required implementation audit artifact instead of an optional artifact.
|
||||
- [x] CHK042 Renamed the planned Unit test family from a resolver-specific name to neutral `Spec366ReportThemeContractTest`, because `ReportThemeResolver` remains an optional implementation detail.
|
||||
- [x] CHK043 Re-ran placeholder scan after fixes; no unresolved placeholder markers, template markers, or old resolver-specific Unit test references remain.
|
||||
- [x] CHK044 Re-ran `git diff --check`; no whitespace errors were reported.
|
||||
- [x] CHK045 Applied follow-up analysis fixes for C1/G1/I1/I2/A1: `UI-EX-001` custom-surface exception, state-only UI impact, neutral theme-contract wording, explicit accessibility/focus task coverage, and measurable visual-quality criteria.
|
||||
|
||||
## Manual Review Prompts for Later Implementation
|
||||
|
||||
- [x] CHK035 Confirm repo truth before runtime edits; do not assume optional branding fields exist.
|
||||
- [x] CHK036 Confirm customer-facing report copy never overclaims certification, approval, or external shareability.
|
||||
- [x] CHK037 Confirm mandatory disclosure, readiness, limitations, and TenantPilot source metadata remain visible in every profile.
|
||||
- [x] CHK038 Confirm browser screenshots are captured and stored under the Spec 366 artifact directory.
|
||||
- [x] CHK039 Confirm UI coverage files are updated or no-update rationale is recorded after implementation.
|
||||
- [x] CHK040 Confirm no migrations, packages, env vars, queues, scheduler changes, storage topology changes, global search changes, panel/provider changes, native PDF package, upload UI, portal, delivery workflow, AI, or compliance framework scope were added.
|
||||
@ -0,0 +1,380 @@
|
||||
# Implementation Plan: Spec 366 - Management Report Layout & Branded Report Themes v1
|
||||
|
||||
**Branch**: `366-management-report-layout-branded-report-themes-v1` | **Date**: 2026-06-08 | **Spec**: [spec.md](./spec.md)
|
||||
**Input**: Feature specification from `/specs/366-management-report-layout-branded-report-themes-v1/spec.md`
|
||||
|
||||
**Note**: This plan is a preparation artifact only. It defines the implementation path and validation gate; it does not implement application code.
|
||||
|
||||
## Summary
|
||||
|
||||
Spec 366 productizes the existing authenticated Review Pack rendered-report route into a management-ready, profile-aware, print-friendly report with bounded text co-branding. It builds on the repo-real Spec 356 HTML report renderer and Spec 357 static profile/disclosure policy. The implementation should improve the existing rendered-report family, not create a new report engine, PDF stack, delivery workflow, customer portal, or theme persistence layer.
|
||||
|
||||
The narrow technical approach is:
|
||||
|
||||
1. Verify current report truth and capture a small current-layout audit.
|
||||
2. Add tests first for derived theme/layout behavior, rendered output, disclosure preservation, print toolbar behavior, and browser screenshots.
|
||||
3. Add a bounded derived report theme/layout contract only where it reduces controller/view duplication and improves testability.
|
||||
4. Refactor the existing rendered-report Blade into profile-aware sections/partials only if that makes the report safer to review.
|
||||
5. Keep all report truth derived from existing Review Pack, Environment Review, Evidence Snapshot, profile, and disclosure policy data.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: PHP 8.4.15, Laravel 12.52.0
|
||||
**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1, Laravel Sail 1.x, Tailwind CSS 4.2.2 through the existing platform build
|
||||
**Storage**: PostgreSQL via existing models only. No new table, migration, persisted report theme, or upload storage is planned.
|
||||
**Testing**: Pest 4 Unit, Feature, and one bounded Browser smoke. Browser tests use Pest 4 browser testing and should assert no JavaScript errors/console logs.
|
||||
**Validation Lanes**: fast-feedback, confidence, browser.
|
||||
**Target Platform**: Laravel web application under `apps/platform`.
|
||||
**Project Type**: Laravel + Filament app with a custom authenticated Blade report view under the existing Review Pack route family.
|
||||
**Performance Goals**: Rendered report remains stored-data-only. No Graph/provider calls during report render.
|
||||
**Constraints**: Disclosure policy is authoritative; branding cannot hide readiness/limitations; no raw JSON/secrets/default diagnostics; no new asset registration unless implementation proves it necessary.
|
||||
**Scale/Scope**: One existing rendered-report route/view family and existing owner-surface links. No new IA, panel, or navigation.
|
||||
|
||||
## Repo Truth Captured During Prep
|
||||
|
||||
- Current branch after Spec Kit scaffold: `366-management-report-layout-branded-report-themes-v1`
|
||||
- Baseline HEAD before prep edits: `6ac0913f feat: implement operations UI operator actions regression gate (#436)`
|
||||
- Starting worktree: clean on `platform-dev`; after scaffold only the new Spec 366 artifacts are intended to change.
|
||||
- No existing `specs/366-management-report-layout-branded-report-themes-v1/` directory or branch existed before preparation.
|
||||
- Spec 356 and Spec 357 packages contain completed-task and validation signals and are treated as historical context only.
|
||||
- Current package baseline from Laravel Boost:
|
||||
- PHP 8.4.15
|
||||
- Laravel 12.52.0
|
||||
- Filament 5.2.1
|
||||
- Livewire 4.1.4
|
||||
- Pest 4.3.1
|
||||
- PostgreSQL
|
||||
|
||||
Relevant implementation files discovered:
|
||||
|
||||
```text
|
||||
apps/platform/app/Http/Controllers/ReviewPackRenderedReportController.php
|
||||
apps/platform/app/Services/ReviewPackService.php
|
||||
apps/platform/app/Support/ReviewPacks/ReportProfileRegistry.php
|
||||
apps/platform/app/Support/ReviewPacks/ReportDisclosurePolicy.php
|
||||
apps/platform/app/Support/ReviewPacks/ReviewPackOutputReadiness.php
|
||||
apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php
|
||||
apps/platform/app/Filament/Resources/EnvironmentReviewResource.php
|
||||
apps/platform/app/Filament/Resources/EnvironmentReviewResource/Pages/ViewEnvironmentReview.php
|
||||
apps/platform/app/Filament/Resources/ReviewPackResource.php
|
||||
apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php
|
||||
apps/platform/resources/views/review-packs/rendered-report.blade.php
|
||||
apps/platform/lang/en/localization.php
|
||||
apps/platform/lang/de/localization.php
|
||||
apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php
|
||||
apps/platform/tests/Feature/ReviewPack/Spec357RenderedReportProfileTest.php
|
||||
apps/platform/tests/Browser/Spec357ReportProfilesSmokeTest.php
|
||||
apps/platform/tests/Unit/Support/ReviewPacks/Spec357ReportProfileRegistryTest.php
|
||||
apps/platform/tests/Unit/Support/ReviewPacks/Spec357ReportDisclosurePolicyTest.php
|
||||
```
|
||||
|
||||
Repo-specific baseline decisions:
|
||||
|
||||
- `ReviewPackRenderedReportController` already builds the rendered-report payload and currently owns base branding, hero, management summary, source metadata, and profile/disclosure integration.
|
||||
- `ReportProfileRegistry` already defines `customer_executive`, `customer_technical`, `internal_msp_review`, `auditor_appendix`, and placeholder `framework_readiness`.
|
||||
- `ReportDisclosurePolicy` already emits mandatory disclosures, warnings, blocking reasons, proof states, and appendix/technical-detail visibility decisions.
|
||||
- `rendered-report.blade.php` already has a report toolbar outside `main.report-canvas`, print CSS that hides toolbar/screen-only controls, a cover/hero section, management summary, profile, limitations, evidence basis, appendix, and disclosure areas.
|
||||
- Existing tests already prove some Spec 366-adjacent behavior, so implementation must avoid duplicating broad Spec 356/357 proof and instead add the missing layout/theme/productization checks.
|
||||
|
||||
## Candidate Selection Gate
|
||||
|
||||
- **Selected candidate**: Management Report Layout & Branded Report Themes v1.
|
||||
- **Source**: Direct user-provided Spec 366 draft.
|
||||
- **Why selected**: It is a bounded follow-through after the report renderer and profile/disclosure policy are repo-real.
|
||||
- **Deferred alternatives**: scheduled delivery, compliance framework profiles, customer portal report consumption, native PDF, and AI/HITL report review are deferred because they depend on a stable management-ready report layout.
|
||||
- **Completed-spec guardrail**: Specs 356 and 357 are completed/validated context only; they are not rewritten. No completed Spec 366 package existed.
|
||||
- **Result**: PASS.
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: changed customer-facing report surface.
|
||||
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**:
|
||||
- existing rendered Review Pack report URL generated by `ReviewPackService::generateRenderedReportUrl()`
|
||||
- `ReviewPackRenderedReportController`
|
||||
- `resources/views/review-packs/rendered-report.blade.php`
|
||||
- existing report launch links on Environment Review and Review Pack detail surfaces
|
||||
- **No-impact class, if applicable**: N/A.
|
||||
- **Native vs custom classification summary**: existing custom Blade report view inside an authenticated Laravel/Filament app. Keep owner surfaces Filament-native; report canvas may stay custom because it is a print artifact.
|
||||
- **Custom surface exception**: `UI-EX-001: Legitimate Custom Surface Exception` applies to the rendered-report canvas and toolbar/print treatment. The product reason is that the customer-facing governance report requires cover identity, readiness, KPI/decision strip, profile-aware appendix hierarchy, disclosure, and print behavior that ordinary CRUD/detail/table primitives do not express cleanly.
|
||||
- **Smallest custom behavior**: one local Blade report canvas with bounded toolbar/print CSS, profile-aware section ordering, and text-only co-branding. No second renderer, general design system, native PDF stack, customer portal, theme editor, upload UI, or report-layout framework.
|
||||
- **Standardized behavior retained**: authorization, workspace/environment entitlement, Review Pack launch links, owner surfaces, profile registry, disclosure policy, localization, and read-only action semantics remain on existing Laravel/Filament/Review Pack paths.
|
||||
- **Custom surface proof**: Feature/Browser tasks must prove report hierarchy, disclosure visibility, toolbar-hidden print behavior, screenshots, accessibility/focus basics, no text overlap, no JavaScript/console errors, and UI coverage update or no-update rationale.
|
||||
- **Shared-family relevance**: evidence/report viewer, status/readiness messaging, report toolbar, customer-safe disclosure, source metadata.
|
||||
- **State layers in scope**: report payload, report view, print CSS, profile-specific section ordering, derived theme/co-branding slots.
|
||||
- **Audience modes in scope**: customer executive, customer technical, internal MSP review, controlled auditor appendix.
|
||||
- **Decision/diagnostic/raw hierarchy plan**: management decision content first; evidence/technical appendix second; raw/support data absent by default.
|
||||
- **Raw/support gating plan**: no raw JSON or support payload in default report output. If existing technical details are rendered for internal/auditor profiles, keep them structured and non-secret.
|
||||
- **One-primary-action / duplicate-truth control**: report toolbar remains outside canvas; report content itself stands alone with a single readable management story.
|
||||
- **Handling modes by drift class or surface**: review-mandatory for any customer-facing disclosure, print CSS, appendix hierarchy, or branding change.
|
||||
- **Repository-signal treatment**: update `ui-099-rendered-review-report.md` and/or coverage matrix if the implemented visual hierarchy materially changes the page contract; otherwise record a no-update rationale.
|
||||
- **Special surface test profiles**: report-viewer / customer-facing artifact surface.
|
||||
- **Required tests or manual smoke**: Feature route/output tests plus one bounded Browser smoke with screenshots.
|
||||
- **Exception path and spread control**: custom report CSS remains local to the rendered report unless implementation proves a shared asset is required. No new Filament asset registration is planned.
|
||||
- **Active feature PR close-out entry**: Guardrail + Smoke Coverage.
|
||||
- **UI/Productization coverage decision**: material existing-page/report-surface change; coverage docs must be updated or explicitly closed out.
|
||||
- **Coverage artifacts to update**: likely `docs/ui-ux-enterprise-audit/page-reports/ui-099-rendered-review-report.md`; maybe design coverage matrix if the surface classification changes.
|
||||
- **No-impact rationale**: N/A.
|
||||
- **Navigation / Filament provider-panel handling**: no panel provider or navigation change planned. Laravel 12 Filament providers remain in `apps/platform/bootstrap/providers.php`.
|
||||
- **Screenshot or page-report need**: yes, screenshots under `specs/366-management-report-layout-branded-report-themes-v1/artifacts/screenshots/`.
|
||||
|
||||
## Shared Pattern & System Fit
|
||||
|
||||
- **Cross-cutting feature marker**: yes.
|
||||
- **Systems touched**: existing Review Pack rendered report, profile registry, disclosure policy, Review Pack service URL generation, Environment Review/Review Pack owner-surface report links, localization.
|
||||
- **Shared abstractions reused**: `ReportProfileRegistry`, `ReportDisclosurePolicy`, `ReviewPackOutputReadiness`, `ReviewPackOutputResolutionGuidance`, `ReviewPackService`.
|
||||
- **New abstraction introduced? why?**: One bounded derived report theme/layout contract is planned because Spec 366 needs a testable contract for text co-branding, layout mode, KPI strip, and print/report section metadata. The concrete implementation may be a `ReportThemeResolver` or a controller-local view-model if that is narrower.
|
||||
- **Why the existing abstraction was sufficient or insufficient**: Existing profile/disclosure abstractions are sufficient for audience and safety. They do not define theme/co-branding slots, profile-specific section ordering, or KPI strip composition.
|
||||
- **Bounded deviation / spread control**: The new shape must live inside `App\Support\ReviewPacks` or the existing controller payload; it must not become a cross-domain theme engine.
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: no.
|
||||
- **Central contract reused**: N/A.
|
||||
- **Delegated UX behaviors**: N/A.
|
||||
- **Surface-owned behavior kept local**: N/A.
|
||||
- **Queued DB-notification policy**: N/A.
|
||||
- **Terminal notification path**: N/A.
|
||||
- **Exception path**: none. Existing Review Pack generation OperationRun behavior is out of scope.
|
||||
|
||||
## Provider Boundary & Portability Fit
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no.
|
||||
- **Provider-owned seams**: none.
|
||||
- **Platform-core seams**: report profile/audience, rendered report source metadata, evidence/readiness truth.
|
||||
- **Neutral platform terms / contracts preserved**: workspace, managed environment, report, profile, audience, readiness, evidence basis, review pack.
|
||||
- **Retained provider-specific semantics and why**: Existing stored report/pack sections may contain provider-specific evidence summaries where already allowed by profile/disclosure policy; no new provider coupling is added.
|
||||
- **Bounded extraction or follow-up path**: none.
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before implementation. Re-check after design and before code merge.*
|
||||
|
||||
- Inventory-first: PASS. Report rendering consumes existing last-observed/released review and Review Pack truth.
|
||||
- Read/write separation: PASS. Report route remains read-only. No restore/remediation/provider write action is introduced.
|
||||
- Graph contract path: PASS. No Graph/provider calls during report render.
|
||||
- Deterministic capabilities: PASS. Existing `REVIEW_PACK_VIEW` capability and policies remain authoritative.
|
||||
- RBAC-UX: PASS. Existing cross-workspace/environment not-found semantics and missing-capability 403 stay in place.
|
||||
- Workspace isolation: PASS. Existing Review Pack/Environment Review workspace/environment scope must remain.
|
||||
- Destructive-like actions: PASS. No destructive or high-impact action is introduced.
|
||||
- Global search: PASS. No resource global-search behavior changes.
|
||||
- Run observability: PASS. No new OperationRun type or queue path.
|
||||
- OperationRun start UX: PASS / N/A. No OperationRun start/link UX touched.
|
||||
- Ops-UX lifecycle and summary counts: PASS / N/A.
|
||||
- Data minimization: PASS. No secrets/raw provider payloads/default raw JSON in report output.
|
||||
- Test governance: PASS. Unit, Feature, and Browser lanes are explicit and bounded.
|
||||
- Proportionality: PASS. Bounded report theme/layout resolver is justified only for current report productization and remains derived/non-persisted.
|
||||
- No premature abstraction: PASS with constraint. Do not create theme persistence, editor, or generalized rendering framework.
|
||||
- Persisted truth: PASS. No new persisted truth.
|
||||
- Behavioral state: PASS. No new status/state family.
|
||||
- UI semantics: PASS. Directly map report profile/readiness/disclosure truth into layout.
|
||||
- Shared pattern first: PASS. Use existing report profile/disclosure services first.
|
||||
- Provider boundary: PASS. No provider-specific theme or platform-core leakage.
|
||||
- V1 explicitness / few layers: PASS. Keep local and derived.
|
||||
- Spec discipline / bloat check: PASS. Proportionality review is complete.
|
||||
- Badge semantics: PASS. If status-like badges are changed, use existing safe report/status presentation rather than ad-hoc truth.
|
||||
- Filament-native UI: PASS. Owner surfaces remain Filament-native; custom report canvas is covered by `UI-EX-001: Legitimate Custom Surface Exception` and remains bounded to the existing print/report artifact.
|
||||
- UI/UX surface taxonomy: PASS. Rendered report surface classified.
|
||||
- Decision-first operating model: PASS. Report first screen supports management decision.
|
||||
- Audience-aware disclosure: PASS. Customer/internal/auditor profile hierarchy is explicit.
|
||||
- Filament UI Action Surface Contract: PASS. No new Filament mutating actions; report toolbar actions are navigation/download/print only.
|
||||
- UI/Productization coverage: PASS. Coverage update/no-update rationale required.
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
- **Test purpose / classification by changed surface**:
|
||||
- Unit: derived theme/layout/profile mapping.
|
||||
- Feature: rendered-report route authorization, content, disclosure, ZIP invariance, print CSS, no provider calls.
|
||||
- Browser: visual/smoke proof for profile variants, print view, and screenshot artifacts.
|
||||
- **Affected validation lanes**: fast-feedback, confidence, browser.
|
||||
- **Why this lane mix is the narrowest sufficient proof**: Most safety is server-rendered HTML/content and deterministic derived data. Browser proof is required only for real report hierarchy, print toolbar behavior, JS/console smoke, and screenshots.
|
||||
- **Narrowest proving command(s)**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Unit/Support/ReviewPacks/Spec366ReportThemeContractTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec366ManagementReportLayoutSmokeTest.php --compact`
|
||||
- **Fixture / helper / factory / seed / context cost risks**: Reuse Spec357 rendered-report fixtures and helpers where possible. New helpers must be local to Spec366 files.
|
||||
- **Expensive defaults or shared helper growth introduced?**: no.
|
||||
- **Heavy-family additions, promotions, or visibility changes**: one explicit Browser smoke file only.
|
||||
- **Surface-class relief / special coverage rule**: customer-facing report surface requires browser screenshots and no raw leakage checks.
|
||||
- **Closing validation and reviewer handoff**: Re-run Spec356/Spec357/ReviewPack/EnvironmentReview regressions and verify no ZIP/download/authorization behavior changed.
|
||||
- **Budget / baseline / trend follow-up**: none expected beyond one browser file.
|
||||
- **Review-stop questions**:
|
||||
- Does branding hide or weaken disclosure/readiness truth?
|
||||
- Does any metric lack a repo-backed source?
|
||||
- Does customer executive layout still show appendix too prominently?
|
||||
- Does print hide toolbar and keep disclosure visible?
|
||||
- Did implementation add persistence/upload/theme editor/native PDF/delivery scope?
|
||||
- **Escalation path**: document-in-feature for deferred optional logo/accent; follow-up-spec for scheduled delivery, theme CRUD, PDF, portal, AI, or compliance reports.
|
||||
- **Active feature PR close-out entry**: Guardrail + Smoke Coverage.
|
||||
- **Why no dedicated follow-up spec is needed**: This is the dedicated layout/theme productization slice. Larger delivery/compliance/portal/AI items stay separate follow-ups.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/366-management-report-layout-branded-report-themes-v1/
|
||||
├── spec.md
|
||||
├── plan.md
|
||||
├── tasks.md
|
||||
├── checklists/
|
||||
│ └── requirements.md
|
||||
└── artifacts/
|
||||
└── screenshots/ # created during implementation/browser smoke
|
||||
```
|
||||
|
||||
Implementation tasks will create a repo-truth map and may create a current-layout audit:
|
||||
|
||||
```text
|
||||
specs/366-management-report-layout-branded-report-themes-v1/repo-truth-map.md
|
||||
specs/366-management-report-layout-branded-report-themes-v1/current-report-layout-audit.md
|
||||
```
|
||||
|
||||
`repo-truth-map.md` is required by the implementation task list to lock the current report fields, repo-backed metrics, baseline commit, and any narrower controller-local theme decision. `current-report-layout-audit.md` is optional and should only be created when screenshots or manual review reveal concrete layout problems that guide implementation. Neither artifact is runtime code.
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
Expected implementation paths:
|
||||
|
||||
```text
|
||||
apps/platform/app/
|
||||
├── Http/Controllers/ReviewPackRenderedReportController.php
|
||||
├── Services/ReviewPackService.php
|
||||
├── Support/ReviewPacks/
|
||||
│ ├── ReportProfileRegistry.php
|
||||
│ ├── ReportDisclosurePolicy.php
|
||||
│ ├── ReviewPackOutputReadiness.php
|
||||
│ ├── ReviewPackOutputResolutionGuidance.php
|
||||
│ └── ReportThemeResolver.php # only if justified during implementation
|
||||
└── Filament/Resources/
|
||||
├── EnvironmentReviewResource.php
|
||||
└── ReviewPackResource.php
|
||||
|
||||
apps/platform/resources/views/review-packs/
|
||||
├── rendered-report.blade.php
|
||||
└── partials/ # only if splitting reduces review risk
|
||||
├── report-toolbar.blade.php
|
||||
├── report-cover.blade.php
|
||||
├── report-state-hero.blade.php
|
||||
├── report-kpi-strip.blade.php
|
||||
├── report-executive-summary.blade.php
|
||||
├── report-appendix.blade.php
|
||||
└── report-disclosure-footer.blade.php
|
||||
|
||||
apps/platform/lang/
|
||||
├── en/localization.php
|
||||
└── de/localization.php
|
||||
|
||||
apps/platform/tests/
|
||||
├── Unit/Support/ReviewPacks/Spec366ReportThemeContractTest.php
|
||||
├── Feature/ReviewPack/Spec366RenderedReportLayoutTest.php
|
||||
└── Browser/Spec366ManagementReportLayoutSmokeTest.php
|
||||
```
|
||||
|
||||
**Structure Decision**: Stay inside the existing Review Pack rendered-report family. Add no new app module, no new base folder outside existing conventions, and no new package.
|
||||
|
||||
## Data / Domain Model Implications
|
||||
|
||||
- No new persisted entity, table, enum, lifecycle state, queue family, or independent report artifact.
|
||||
- Derived theme/layout data may include:
|
||||
- `prepared_by`
|
||||
- `prepared_for`
|
||||
- `generated_by`
|
||||
- `generated_at`
|
||||
- `accent`
|
||||
- `logo`
|
||||
- `layout_mode`
|
||||
- `kpi_strip`
|
||||
- `section_order`
|
||||
- `logo` and `accent` must stay null/default unless existing safe repo-backed fields are verified.
|
||||
- KPI strip values must be derived only from existing report/review/evidence payloads.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 0 - Repo Verification and Current Layout Audit
|
||||
|
||||
- Confirm Specs 356 and 357 are stable in the current branch.
|
||||
- Re-read current controller/view/tests.
|
||||
- Create `repo-truth-map.md` to record current fields, gaps, repo-backed metrics, optional branding fields, baseline commit, and any narrower controller-local theme decision.
|
||||
- Create `current-report-layout-audit.md` only if browser screenshots or manual review reveal concrete layout problems that guide implementation.
|
||||
- Stop if Spec 357 profile/disclosure behavior is absent or failing.
|
||||
|
||||
### Phase 1 - Tests First
|
||||
|
||||
- Add Unit tests for derived theme/layout contract or verify current controller payload if no new class is justified.
|
||||
- Add Feature tests for route output, profile layout, co-branding, print CSS, disclosure preservation, no ZIP change, no provider call, no raw/localization leakage.
|
||||
- Add Browser smoke for profile variants, print class/print behavior, screenshots, and no JS/console errors.
|
||||
|
||||
### Phase 2 - Theme/Layout Contract
|
||||
|
||||
- Add `ReportThemeResolver` or equivalent controller-local view-model shape only if justified by tests and current code.
|
||||
- Use workspace and managed-environment names for text co-branding.
|
||||
- Keep optional logo/accent null/default unless repo-backed fields already exist.
|
||||
- Add layout-mode and section-order data derived from `ReportProfileRegistry`.
|
||||
|
||||
### Phase 3 - Rendered Report Layout Productization
|
||||
|
||||
- Update `ReviewPackRenderedReportController` to provide stable theme/layout/KPI data.
|
||||
- Update or split `rendered-report.blade.php` into partials only where reviewability improves.
|
||||
- Render management-ready first screen: cover, state hero, KPI strip, executive summary, key risks/decisions, evidence basis, accepted risks, next action, disclosure.
|
||||
- Ensure appendix is secondary for `customer_executive` and more prominent for `auditor_appendix`.
|
||||
- Keep toolbar outside report canvas and hidden in print.
|
||||
|
||||
### Phase 4 - Localization and Report Copy
|
||||
|
||||
- Add EN/DE keys for new dominant report copy only.
|
||||
- Avoid raw localization keys in output.
|
||||
- Keep action labels and source metadata consistent with existing Review Pack/report vocabulary.
|
||||
|
||||
### Phase 5 - UI Coverage, Browser Smoke, and Close-Out
|
||||
|
||||
- Run focused validation commands.
|
||||
- Save screenshots under the Spec 366 artifact directory.
|
||||
- Update UI audit coverage or record no-update rationale.
|
||||
- Record no migrations/packages/env/queues/scheduler/storage/asset changes.
|
||||
|
||||
## Rollout and Deployment Considerations
|
||||
|
||||
- **Staging**: Validate rendered report profile variants, print behavior, and review-pack download unchanged.
|
||||
- **Production**: No schema change expected. Deploy as ordinary app/view/test change.
|
||||
- **Migrations**: none planned.
|
||||
- **Environment variables**: none planned.
|
||||
- **Queues / scheduler**: none planned.
|
||||
- **Storage**: no new storage path except test/browser screenshot artifacts in the repo during implementation.
|
||||
- **Filament assets**: no new registered Filament assets expected. If implementation registers assets unexpectedly, deployment must include `cd apps/platform && php artisan filament:assets`; otherwise existing deployment asset handling is unchanged.
|
||||
- **Reverse proxy / SSL**: no impact.
|
||||
- **Rollback/forward**: revert code/view/localization changes; no data migration rollback.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|---|---|---|
|
||||
| Bounded derived report theme/layout resolver or equivalent view-model | Current-release report productization needs deterministic co-branding, layout, KPI, and profile ordering proof | Scattered controller/view variables would be harder to test and could let profile/disclosure/branding drift |
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **Current operator problem**: MSPs need a professional, customer-safe, print-ready report artifact over existing Review Pack truth.
|
||||
- **Existing structure is insufficient because**: It proves a report can render, but not yet that the report has a stable management layout/theme contract across profiles and print states.
|
||||
- **Narrowest correct implementation**: Local derived theme/layout contract inside the Review Pack report family, rendered by the existing route/view.
|
||||
- **Ownership cost created**: One small Unit test family, one Feature test file, one Browser smoke, localization keys, and optional partial files.
|
||||
- **Alternative intentionally rejected**: persisted themes, logo upload, a generic report engine, native PDF, scheduled delivery, AI, or compliance report framework.
|
||||
- **Release truth**: Current-release report productization.
|
||||
|
||||
## Filament v5 / Livewire v4 Output Contract
|
||||
|
||||
- **Livewire v4.0+ compliance**: The application uses Livewire 4.1.4; this spec does not introduce Livewire v3 APIs.
|
||||
- **Provider registration location**: No panel provider change planned. Laravel 12 Filament providers remain registered in `apps/platform/bootstrap/providers.php`.
|
||||
- **Global search**: No globally searchable resource is changed or enabled. If implementation unexpectedly touches `ReviewPackResource` or `EnvironmentReviewResource` global search, it must verify safe View/Edit pages or keep global search disabled.
|
||||
- **Destructive/high-impact actions**: None introduced. Existing Review Pack download/render/print actions remain read-only. Any unexpected mutating action must stop implementation and update spec/plan first.
|
||||
- **Asset strategy**: No new registered Filament assets planned; no new `filament:assets` deployment requirement unless implementation proves otherwise.
|
||||
- **Testing plan**: Pest Unit/Feature/Browser tests as listed above, plus Spec356/Spec357/ReviewPack/EnvironmentReview regressions.
|
||||
|
||||
## Stop Conditions
|
||||
|
||||
Stop and update spec/plan before code implementation continues if:
|
||||
|
||||
- Spec 357 profile/disclosure policy is not present or not stable.
|
||||
- A native PDF package, new dependency, persisted theme table, upload UI, or theme editor appears necessary.
|
||||
- Report metrics require new data collection or a new source of truth.
|
||||
- A customer portal, public link, scheduled delivery, approval workflow, or AI narrative becomes necessary.
|
||||
- Branding cannot be implemented from existing workspace/managed-environment truth without new persisted fields.
|
||||
- Any write/mutation action is proposed inside the report surface.
|
||||
@ -0,0 +1,65 @@
|
||||
# Repo Truth Map: Spec 366 Management Report Layout
|
||||
|
||||
## Branch And Baseline
|
||||
|
||||
- Branch: `366-management-report-layout-branded-report-themes-v1`
|
||||
- Baseline commit: `6ac0913f feat: implement operations UI operator actions regression gate (#436)`
|
||||
- Initial worktree state: new `specs/366-management-report-layout-branded-report-themes-v1/` artifacts only.
|
||||
|
||||
## Inspected Runtime Seams
|
||||
|
||||
- Rendered route/controller: `apps/platform/app/Http/Controllers/ReviewPackRenderedReportController.php`
|
||||
- Rendered Blade report: `apps/platform/resources/views/review-packs/rendered-report.blade.php`
|
||||
- Review pack URL/download contract: `apps/platform/app/Services/ReviewPackService.php`
|
||||
- Existing profile/disclosure/readiness seams:
|
||||
- `apps/platform/app/Support/ReviewPacks/ReportProfileRegistry.php`
|
||||
- `apps/platform/app/Support/ReviewPacks/ReportDisclosurePolicy.php`
|
||||
- `apps/platform/app/Support/ReviewPacks/ReviewPackOutputReadiness.php`
|
||||
- `apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php`
|
||||
- Owner launch surfaces:
|
||||
- `apps/platform/app/Filament/Resources/EnvironmentReviewResource.php`
|
||||
- `apps/platform/app/Filament/Resources/EnvironmentReviewResource/Pages/ViewEnvironmentReview.php`
|
||||
- `apps/platform/app/Filament/Resources/ReviewPackResource.php`
|
||||
- `apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php`
|
||||
|
||||
## Current Truth Used
|
||||
|
||||
- Prepared-by text comes from the current workspace name, with `TenantPilot` fallback.
|
||||
- Prepared-for text comes from the managed environment name, with the localized tenant fallback.
|
||||
- Generated-by stays `TenantPilot`.
|
||||
- Generated-at is the current review-pack `generated_at`.
|
||||
- Readiness, customer-safe/internal/limited state, disclosure proof, and appendix visibility remain derived from existing review-pack, environment-review, evidence-snapshot, profile, and disclosure-policy data.
|
||||
- KPI values are derived from stored governance-package arrays when present:
|
||||
- governance status from current output guidance
|
||||
- evidence coverage from the evidence snapshot basis
|
||||
- key risks from `governance_package.top_findings`
|
||||
- open decisions from `governance_package.decision_summary.entries` or `governance_package.governance_decisions`
|
||||
|
||||
## Gaps And Decisions
|
||||
|
||||
- No repo-backed logo, accent customization, persisted report theme, theme editor, or upload lifecycle exists. Decision: keep logo `null`, accent default `teal`, and all branding derived-only.
|
||||
- Unsupported metrics must not be faked. Decision: measured empty arrays render `0`; missing metrics render localized `Not measured`.
|
||||
- Existing Spec 357 profile registry is sufficient for layout modes. Decision: no new profile taxonomy or profile CRUD.
|
||||
- Partializing the Blade view was not needed for this bounded change; stable test hooks and section-rank helpers kept reviewability acceptable without adding more files.
|
||||
- `current-report-layout-audit.md` was not created because UI-099 already existed in the UI audit registry and Spec 366 produced fresh browser screenshot evidence after implementation.
|
||||
|
||||
## Scope Gates
|
||||
|
||||
- Migrations: none.
|
||||
- Packages/dependencies: none.
|
||||
- Environment variables: none.
|
||||
- Queue families/scheduler: none.
|
||||
- Storage topology: unchanged; existing review-pack ZIP download remains the structured artifact.
|
||||
- Panel/provider registration: unchanged; Laravel 12 Filament providers remain in `apps/platform/bootstrap/providers.php`.
|
||||
- Global search: unchanged.
|
||||
- Filament assets: none registered; no `filament:assets` deployment change.
|
||||
- Native PDF package/runtime: not added.
|
||||
- Upload UI/theme editor/customer portal/scheduled delivery/AI/framework-report semantics: not added.
|
||||
|
||||
## Validation Notes
|
||||
|
||||
- Spec 366 browser evidence lives in `specs/366-management-report-layout-branded-report-themes-v1/artifacts/screenshots/`.
|
||||
- UI coverage registry updated for UI-099 rendered report; route classification is unchanged.
|
||||
- Adjacent regression fixes:
|
||||
- `Spec347ReviewPackOutputReadinessSmokeTest` fixture now stores the same non-certification disclosure on the review pack summary that the normalized review already carries.
|
||||
- `ReviewOutputResolveActionMapper` no longer maps `export_not_ready` to `create_next_review`, preserving `export_executive_pack` as the primary action for published reviews that still need an export.
|
||||
@ -0,0 +1,386 @@
|
||||
# Feature Specification: Spec 366 - Management Report Layout & Branded Report Themes v1
|
||||
|
||||
**Feature Branch**: `366-management-report-layout-branded-report-themes-v1`
|
||||
**Created**: 2026-06-08
|
||||
**Status**: Draft / Ready for implementation
|
||||
**Input**: User-provided draft "Spec 366 - Management Report Layout & Branded Report Themes v1"
|
||||
|
||||
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: The rendered Review Pack report is repo-real after Specs 356 and 357, but it still needs a management-ready report layout and bounded co-branding treatment so MSPs can use it as a customer-facing governance artifact without hiding limitations, evidence state, or TenantPilot source truth.
|
||||
- **Today's failure**: A stakeholder can open a rendered report, but the report can still read too much like an admin/detail output or appendix-first renderer instead of a polished management report. Branding is controller-local text only, profile layout hierarchy is not yet a first-class report contract, and print/readability expectations are not locked by a dedicated Spec 366 gate.
|
||||
- **User-visible improvement**: MSP and customer stakeholders can open a report and immediately see prepared-by/prepared-for identity, report profile, readiness state, KPI/decision strip, executive story, key risks, evidence basis, accepted risks, appendix hierarchy, and mandatory disclosures in a print-friendly layout.
|
||||
- **Smallest enterprise-capable version**: Productize the existing rendered-report route with profile-aware layout ordering, a bounded derived report theme contract, repo-backed KPI strip, improved report canvas/print hierarchy, localized dominant copy, and focused Unit/Feature/Browser validation.
|
||||
- **Explicit non-goals**: No report engine rewrite, no second renderer, no Review Pack ZIP contract change, no native PDF package, no logo upload UI, no report theme CRUD, no scheduled delivery, no public/customer portal, no AI summary, no framework-specific compliance report, no hiding mandatory disclosures.
|
||||
- **Permanent complexity imported**: One bounded report theme resolver or equivalent derived view-model shape, additional profile-aware layout fields, partialized report Blade only where it reduces review risk, localized report copy, focused Unit/Feature/Browser tests, and screenshot artifacts.
|
||||
- **Why now**: Specs 356 and 357 established the HTML report, static profile registry, disclosure policy, and profile-aware safety boundary. The next sellability gap is report productization: layout, co-branding, print readiness, and management presentation.
|
||||
- **Why not local**: Leaving branding and layout as scattered controller/view decisions would make it hard to prove that profile changes, print output, and mandatory disclosures stay consistent across report states.
|
||||
- **Approval class**: Workflow Compression / Core Enterprise.
|
||||
- **Red flags triggered**: New derived resolver/view-model, customer-facing report surface, profile-aware visual hierarchy. Defense: the resolver is non-persisted, bounded to the existing Review Pack rendered-report family, and justified by current profile/disclosure/report states already implemented in Specs 356 and 357.
|
||||
- **Score**: Nutzen: 3 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 3 | Wiederverwendung: 1 | **Gesamt: 12/12**
|
||||
- **Decision**: approve as a bounded productization spec over the existing rendered-report route.
|
||||
|
||||
## Candidate Selection Gate
|
||||
|
||||
- **Selected candidate**: Management Report Layout & Branded Report Themes v1.
|
||||
- **Source**: Direct user-provided Spec 366 draft, aligned with the open report/productization trajectory after Specs 355, 356, and 357.
|
||||
- **Why selected**: It is the next narrow report productization slice after the profile/disclosure policy exists. It improves customer/MSP usefulness without introducing delivery, portal, PDF, AI, or compliance-framework scope.
|
||||
- **Close alternatives deferred**:
|
||||
- Scheduled Report Delivery Foundation v1: deferred because report layout/truth must be stable before delivery approvals or send ledgers.
|
||||
- Compliance Report Profile Foundation v1 and NIS2/CIS reports: deferred because framework-specific semantics would expand beyond the current report profile/disclosure base.
|
||||
- Customer Portal Report Consumption Boundary: deferred because the authenticated admin report must be customer-safe and print-ready first.
|
||||
- Private AI/HITL Report Review Preparation: deferred until the report profile, disclosure, layout, and delivery approval foundations are stable.
|
||||
- **Completed-spec guardrail result**:
|
||||
- `specs/356-review-pack-pdf-html-renderer-v1/` contains completed tasks and validation signals; it is historical context only.
|
||||
- `specs/357-report-profiles-disclosure-policy-v1/` contains completed tasks and validation signals; it is historical context only.
|
||||
- `specs/365-operations-ui-operator-actions-regression-gate/` is unrelated completed/immediate predecessor context for platform action maturity and is not modified.
|
||||
- No existing `specs/366-management-report-layout-branded-report-themes-v1/` package existed before this prep.
|
||||
- **Roadmap relationship**: Report/productization follow-through on top of Review Pack rendered report, profile, disclosure, and platform sellable smoke work.
|
||||
- **Smallest viable slice**: Authenticated rendered report layout/theme productization only.
|
||||
- **Gate result**: PASS.
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: workspace
|
||||
- **Primary Routes**:
|
||||
- Existing authenticated rendered Review Pack report URL generated by `ReviewPackService::generateRenderedReportUrl()`
|
||||
- Existing Review Pack detail route that opens the rendered report
|
||||
- Existing Environment Review detail route that opens the current rendered report
|
||||
- **Data Ownership**: Existing workspace/managed-environment-owned `review_packs`, `environment_reviews`, evidence snapshot truth, and report summary payloads. No new table or independent persisted theme/profile entity is introduced.
|
||||
- **RBAC**: Existing workspace membership, managed-environment entitlement, `ReviewPackPolicy`, `Capabilities::REVIEW_PACK_VIEW`, and current rendered-report authorization stay authoritative.
|
||||
|
||||
For this workspace-scope report:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: N/A. This is an existing signed rendered-report route over a concrete Review Pack; it must not introduce new environment query-context behavior.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: The rendered report must keep existing deny-as-not-found behavior for non-members or wrong environment/workspace, and 403 for members missing review-pack view capability.
|
||||
|
||||
## 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
|
||||
- [ ] New table/form added
|
||||
- [x] New report state presentation 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**:
|
||||
- `apps/platform/app/Http/Controllers/ReviewPackRenderedReportController.php`
|
||||
- `apps/platform/resources/views/review-packs/rendered-report.blade.php`
|
||||
- existing rendered report links from `EnvironmentReviewResource` and `ReviewPackResource`
|
||||
- **Current or new page archetype**: Existing rendered review report / review-pack detail supporting surface. UI audit references include `docs/ui-ux-enterprise-audit/page-reports/ui-042-review-pack-detail.md` and `docs/ui-ux-enterprise-audit/page-reports/ui-099-rendered-review-report.md`.
|
||||
- **Design depth**: Strategic / customer-facing report surface.
|
||||
- **Repo-truth level**: repo-verified existing route and rendered-report family.
|
||||
- **Existing pattern reused**: Specs 356/357 rendered-report route, `ReportProfileRegistry`, `ReportDisclosurePolicy`, `ReviewPackOutputResolutionGuidance`, review-pack authorization, current report view tests, and Spec 357 browser smoke conventions.
|
||||
- **New pattern required**: Bounded report theme/layout contract only. No general design system or reporting framework.
|
||||
- **Screenshot required**: yes. Implementation must save report-state screenshots under `specs/366-management-report-layout-branded-report-themes-v1/artifacts/screenshots/`.
|
||||
- **Page audit required**: update `ui-099-rendered-review-report.md` if the report hierarchy materially changes; otherwise record a proportional no-update rationale in close-out.
|
||||
- **Customer-safe review required**: yes. The rendered report is customer-facing when the profile is customer-facing.
|
||||
- **Dangerous-action review required**: no. Report actions are read-only/print/download/navigation; any new mutating action is out of scope.
|
||||
- **Coverage files updated or explicitly not needed**:
|
||||
- [x] `docs/ui-ux-enterprise-audit/route-inventory.md`
|
||||
- [x] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`
|
||||
- [x] `docs/ui-ux-enterprise-audit/page-reports/...`
|
||||
- [x] `docs/ui-ux-enterprise-audit/strategic-surfaces.md` - no update needed; UI-099 remains P1 Strategic Surface and now has evidence.
|
||||
- [x] `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md` - no update needed; no new unresolved group.
|
||||
- [x] `docs/ui-ux-enterprise-audit/unresolved-pages.md` - no update needed; UI-099 is not unresolved.
|
||||
- [ ] `N/A - no reachable UI surface impact`
|
||||
- **Coverage artifact decision**: Implementation must update the rendered-report page report/design coverage if the visual hierarchy or customer-safe report contract materially changes. If the change is contained to the existing report archetype and page-report guidance remains accurate, close-out must record why no coverage file changed.
|
||||
- **No-impact rationale when applicable**: N/A.
|
||||
|
||||
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)*
|
||||
|
||||
- **Cross-cutting feature?**: yes
|
||||
- **Interaction class(es)**: evidence/report viewer, status/readiness messaging, print/report toolbar, artifact links, customer-safe disclosure.
|
||||
- **Systems touched**:
|
||||
- `ReportProfileRegistry`
|
||||
- `ReportDisclosurePolicy`
|
||||
- `ReviewPackOutputResolutionGuidance`
|
||||
- `ReviewPackOutputReadiness`
|
||||
- `ReviewPackService::generateRenderedReportUrl()`
|
||||
- `ReviewPackRenderedReportController`
|
||||
- rendered report Blade template and localization files
|
||||
- **Existing pattern(s) to extend**: current rendered-report payload, profile/disclosure policy, customer-safe Review Pack handoff, current read-only report toolbar.
|
||||
- **Shared contract / presenter / builder / renderer to reuse**: Reuse `ReportProfileRegistry`, `ReportDisclosurePolicy`, existing Review Pack summary payloads, and current authorization/download link seams first.
|
||||
- **Why the existing shared path is sufficient or insufficient**: Existing paths are sufficient for profile resolution, disclosure, readiness, signed URL generation, and base HTML rendering. They are insufficient for an explicit report theme/layout contract and management-ready section ordering.
|
||||
- **Allowed deviation and why**: Add one bounded `ReportThemeResolver` or equivalent local view-model method if controller-local branding/layout fields become too hard to test or duplicate. Do not add persistence, CRUD, upload, or global theme registry.
|
||||
- **Consistency impact**: Profile labels, audience labels, readiness warnings, disclosure footer, source metadata, and co-branding slots must stay consistent between route output, print output, and browser screenshots.
|
||||
- **Review focus**: Verify the layout does not bypass disclosure policy or profile fail-closed behavior, and that branding never changes report truth.
|
||||
|
||||
## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)*
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: no
|
||||
- **Shared OperationRun UX contract/layer reused**: N/A
|
||||
- **Delegated start/completion UX behaviors**: N/A
|
||||
- **Local surface-owned behavior that remains**: N/A
|
||||
- **Queued DB-notification policy**: N/A
|
||||
- **Terminal notification path**: N/A
|
||||
- **Exception required?**: none. Existing Review Pack generation and OperationRun associations must not be changed by this spec.
|
||||
|
||||
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no
|
||||
- **Boundary classification**: N/A
|
||||
- **Seams affected**: N/A
|
||||
- **Neutral platform terms preserved or introduced**: report, profile, audience, readiness, evidence basis, review pack, managed environment, workspace.
|
||||
- **Provider-specific semantics retained and why**: Provider-specific details remain only in existing report payload sections when already profile/disclosure-safe. No provider calls or provider-specific theme rules are introduced.
|
||||
- **Why this does not deepen provider coupling accidentally**: The report uses stored Review Pack/review/evidence truth only and does not inspect Graph/provider runtime state.
|
||||
- **Follow-up path**: none.
|
||||
|
||||
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
|
||||
|
||||
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Rendered report toolbar | yes | Existing custom Blade report chrome | report viewer, print, artifact links | report route, toolbar, print CSS | yes - `UI-EX-001: Legitimate Custom Surface Exception` | Toolbar remains outside print canvas |
|
||||
| Report canvas cover/state/KPI/executive layout | yes | Existing custom Blade report canvas | status messaging, evidence/report viewer | report canvas, profile layout | yes - `UI-EX-001: Legitimate Custom Surface Exception` | Customer-facing strategic report surface |
|
||||
| Report appendix/disclosure footer | yes | Existing custom Blade report sections | evidence/report viewer, mandatory disclosure | section ordering, footer | yes - `UI-EX-001: Legitimate Custom Surface Exception` | Disclosure policy remains authoritative |
|
||||
| Co-branding/theme slots | yes | Derived text presentation | report identity / customer artifact | cover/footer | no | No upload UI or persisted theme |
|
||||
|
||||
## UI-EX-001 Legitimate Custom Surface Exception
|
||||
|
||||
- **Exception type**: `UI-EX-001: Legitimate Custom Surface Exception`.
|
||||
- **Product reason**: The rendered report is a customer-facing, print-oriented governance artifact. It must express cover identity, readiness, KPI/decision strip, executive story, profile-aware appendix hierarchy, disclosures, and print behavior in a layout that ordinary Filament CRUD/detail/table primitives do not express cleanly.
|
||||
- **Smallest custom behavior required**: Keep one local Blade report canvas and toolbar/print treatment for the existing rendered-report route, with profile-aware section ordering and text-only co-branding slots. Do not create a general design system, second report renderer, theme editor, native PDF stack, or customer portal.
|
||||
- **What remains standardized**: Authorization, workspace/environment entitlement, Review Pack launch links, owner surfaces, policy checks, profile resolution, disclosure policy, localization, and validation stay on existing Laravel/Filament/Review Pack paths. Report actions stay read-only navigation/download/print and must not introduce destructive or mutating action semantics.
|
||||
- **State owner**: `ReviewPackRenderedReportController` owns the derived report payload for this route; `ReportProfileRegistry` owns effective profile/layout inputs; `ReportDisclosurePolicy` owns mandatory disclosure and safety limitations; the Blade view owns presentation and print CSS only.
|
||||
- **Proof / review evidence**: Implementation tasks T012, T046-T053, and T058-T060 must validate report hierarchy, print-toolbar behavior, screenshots, no JS/console errors, no text overlap, accessibility/focus basics, and UI coverage update or no-update rationale.
|
||||
- **Spread control**: Custom layout remains bounded to `resources/views/review-packs/rendered-report.blade.php` and optional local partials for reviewability. Any repeated UI primitive, status/badge semantics, or broader report-layout framework must stop and update spec/plan first.
|
||||
|
||||
## 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 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Rendered report cover/state/KPI area | Primary Decision Surface for report consumption | Customer/MSP reads the report to decide whether it is shareable and what needs attention | prepared by/for, profile, readiness, KPI/decision strip, executive summary, evidence state, next action | appendix, technical posture, source metadata | Primary because the report must stand alone in review meetings | Follows Review Pack rendered-report profile/disclosure workflow | Reduces need to inspect admin pages to understand report state |
|
||||
| Supporting appendix | Tertiary Evidence / Diagnostics | Auditor or technical reader needs proof/context after the management story | concise appendix heading and availability | section completeness, source metadata, evidence/operation references | Secondary unless profile is `auditor_appendix` | Follows profile-aware disclosure | Keeps executive reports readable while retaining proof |
|
||||
|
||||
## 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 executive rendered report | customer-read-only, operator-MSP | report identity, customer-safe readiness, KPI strip, executive summary, key risks, evidence basis, disclosures | limited or absent | raw JSON and internal rationale absent | Review next action / print / download through existing toolbar | technical appendix minimized, raw/internal fields hidden | Hero states the blocker once; sections add evidence only |
|
||||
| Customer technical rendered report | customer-read-only, operator-MSP | readiness, evidence basis, technical posture summary, risks, accepted risks, disclosures | structured technical sections | raw payloads absent | Review evidence or follow next action | internal-only warnings and raw diagnostics hidden | Profile layout decides detail level |
|
||||
| Internal MSP rendered report | operator-MSP, support-platform | internal warning, operator summary, blockers, evidence/readiness, disclosures | allowed structured diagnostics | raw/support evidence still not primary | Review limitations / open source detail | raw JSON, secrets, provider payloads | Internal label prevents customer-safe overclaiming |
|
||||
| Auditor appendix rendered report | auditor-controlled, operator-MSP | evidence basis, section completeness, operation/source references, disclosure | structured appendix | raw payloads absent by default | Review evidence basis | legal attestation and compliance certification absent | Appendix prominence is profile-owned |
|
||||
|
||||
## 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 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Rendered report | Report / Artifact Viewer | Custom Surface / customer-safe report viewer | Print report, open review detail, download review pack | Authenticated signed report URL | N/A | Toolbar outside report canvas | N/A - no destructive actions | Review Pack list/detail | rendered report URL from `ReviewPackService` | workspace, managed environment, profile, audience | Governance review report | readiness, limitations, evidence basis, generated/source metadata, disclosure | `UI-EX-001: Legitimate Custom Surface Exception` - custom print/report canvas is bounded to the existing Specs 356/357 rendered-report route because native CRUD/detail primitives do not express the customer-facing report artifact layout |
|
||||
|
||||
## 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 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Rendered report | MSP operator, customer stakeholder, controlled auditor | Decide whether report can be shared and what top risks/actions matter | Report viewer | Is this report shareable, what does it say, and what should happen next? | report identity, profile/audience, readiness hero, KPI strip, executive summary, risks/decisions, evidence basis, disclosures | appendix, source metadata, technical posture | readiness, evidence completeness, customer-safe boundary, disclosure proof, generated/source metadata | read-only | Print report, open review detail, open review pack detail, download review pack | none |
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: no. Theme/layout output is derived from existing Review Pack, Environment Review, workspace, managed environment, report profile, and disclosure policy truth.
|
||||
- **New persisted entity/table/artifact?**: no.
|
||||
- **New abstraction?**: yes, a bounded derived report theme/layout contract. The concrete implementation may be a `ReportThemeResolver` or a controller-local view-model shape if that is narrower.
|
||||
- **New enum/state/reason family?**: no new persisted state. Derived layout modes may map directly from existing `ReportProfileRegistry` profile keys.
|
||||
- **New cross-domain UI framework/taxonomy?**: no. This is a local rendered-report productization contract.
|
||||
- **Current operator problem**: MSP/customer reports need a stable, testable management layout and co-branding contract that cannot hide limitations or evidence state.
|
||||
- **Existing structure is insufficient because**: The current controller/view can render base report output, but controller-local branding/layout decisions are hard to test as a reusable report contract and do not yet guarantee profile-aware hierarchy, KPI strip, or print/theme constraints for Spec 366.
|
||||
- **Narrowest correct implementation**: Add one derived theme/layout contract for text-only theme slots and layout metadata, then render through the existing report route/view. Prefer a controller-local view-model if it is narrower; create a `ReportThemeResolver` only if it reduces duplication and improves proof without becoming a general theme framework.
|
||||
- **Ownership cost**: One Unit test family for theme/layout derivation, Feature tests for rendered output, one bounded Browser smoke with screenshots, and localization maintenance.
|
||||
- **Alternative intentionally rejected**: Persisted theme records, logo upload, theme editor, new report engine, or a generalized report layout framework.
|
||||
- **Release truth**: Current-release productization on existing report artifacts.
|
||||
|
||||
### Compatibility posture
|
||||
|
||||
This feature assumes a pre-production environment. Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec. Canonical replacement is preferred over preservation.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Unit, Feature, Browser.
|
||||
- **Validation lane(s)**: fast-feedback for derived theme/layout tests, confidence for rendered-report route and safety tests, browser for report visual/print smoke.
|
||||
- **Why this classification and these lanes are sufficient**: Unit tests prove deterministic theme/profile derivation; Feature tests prove authorization, route output, disclosure, print CSS, no ZIP mutation, no provider calls, and no raw/localization leakage; Browser smoke proves the real rendered report hierarchy, screenshots, and print-toolbar behavior.
|
||||
- **New or expanded test families**:
|
||||
- `Spec366ReportThemeContractTest`
|
||||
- `Spec366RenderedReportLayoutTest`
|
||||
- `Spec366ManagementReportLayoutSmokeTest`
|
||||
- **Fixture / helper cost impact**: Reuse existing Review Pack rendered-report fixtures from Specs 356/357 where possible. Add only explicit Spec366 helper states for profile variants and ready/limited/internal reports.
|
||||
- **Heavy-family visibility / justification**: One explicit Browser smoke because this is a customer-facing visual/print report.
|
||||
- **Special surface test profile**: shared-detail-family / report-viewer / customer-facing artifact surface.
|
||||
- **Standard-native relief or required special coverage**: Special browser coverage required for report hierarchy, print CSS, screenshots, and no raw leakage.
|
||||
- **Reviewer handoff**: Reviewers must verify that mandatory disclosures render in every profile, branding is text-only/repo-backed, customer-executive appendix stays secondary, and internal/limited reports cannot look customer-safe.
|
||||
- **Budget / baseline / trend impact**: One bounded browser smoke file and screenshot artifacts. No broad browser family.
|
||||
- **Escalation needed**: `document-in-feature` if optional logo/accent is not repo-backed and is deferred; `follow-up-spec` for theme CRUD, scheduled delivery, native PDF, customer portal, AI, or framework-specific reports.
|
||||
- **Active feature PR close-out entry**: Guardrail + Smoke Coverage.
|
||||
- **Planned validation commands**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Unit/Support/ReviewPacks/Spec366ReportThemeContractTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec366ManagementReportLayoutSmokeTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec356`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec357`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=ReviewPack`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=EnvironmentReview`
|
||||
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
|
||||
- `git diff --check`
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Read a management-ready report first page (Priority: P1)
|
||||
|
||||
As an MSP operator or customer stakeholder, I can open the rendered report and understand within seconds who prepared it, who it is for, whether it is shareable, what the top status is, and what the primary next action or limitation is.
|
||||
|
||||
**Why this priority**: This is the core sellability gap. If the first page is not management-ready and honest, delivery, portal, AI, or framework reports would only amplify the problem.
|
||||
|
||||
**Independent Test**: Render a customer-safe report and a limited report through the existing signed URL and assert the cover/state/KPI/executive sections appear before appendix/detail content, with mandatory disclosures still visible.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a current customer-safe Review Pack with a `customer_executive` profile, **When** an entitled user opens the rendered report, **Then** the report shows prepared by, prepared for, generated by, profile/audience, readiness hero, KPI/decision strip, management summary, and disclosure footer.
|
||||
2. **Given** a limited report, **When** an entitled user opens it, **Then** the state hero says it is limited or not externally shareable and does not show customer-safe success copy.
|
||||
3. **Given** any report, **When** the report renders, **Then** no raw JSON, localization keys, secret-like fields, provider payloads, or internal-only rationale appear as default-visible customer content.
|
||||
|
||||
### User Story 2 - Use profile-aware report hierarchy (Priority: P1)
|
||||
|
||||
As an operator, I can choose or open the effective report profile and get the correct layout hierarchy for customer executive, customer technical, internal MSP review, or auditor appendix use.
|
||||
|
||||
**Why this priority**: Spec 357 made profiles real; Spec 366 must make those profiles visibly meaningful in report layout rather than just metadata labels.
|
||||
|
||||
**Independent Test**: Render all implemented profiles and verify section order and appendix prominence match the profile contract.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** `customer_executive`, **When** the report renders, **Then** management summary, KPI strip, risks/decisions, evidence coverage, next actions, and disclosure precede any minimal appendix.
|
||||
2. **Given** `customer_technical`, **When** the report renders, **Then** evidence basis, permission posture, baseline drift, operations health, risks, and accepted risks are more visible while customer-safe restrictions still apply.
|
||||
3. **Given** `internal_msp_review`, **When** the report renders, **Then** internal warnings and operator limitations are prominent.
|
||||
4. **Given** `auditor_appendix`, **When** the report renders, **Then** evidence basis, section completeness, source metadata, and appendix content are more prominent without becoming a legal attestation.
|
||||
|
||||
### User Story 3 - Show controlled co-branding without weakening truth (Priority: P2)
|
||||
|
||||
As an MSP, I can show a text-only co-branded report identity using existing workspace/environment truth, while TenantPilot source metadata and mandatory limitations remain visible.
|
||||
|
||||
**Why this priority**: MSPs need the artifact to feel deliverable, but branding must remain presentation, not product truth.
|
||||
|
||||
**Independent Test**: Render reports for workspace/environment fixtures with and without workspace/environment names and verify text co-branding falls back safely and never removes TenantPilot-generated metadata or readiness/disclosure blocks.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** workspace and managed-environment names exist, **When** the report renders, **Then** it shows prepared by workspace/MSP, prepared for environment/customer, and generated by TenantPilot.
|
||||
2. **Given** optional logo/accent fields are not repo-backed, **When** implementation is performed, **Then** no upload UI, image storage, theme table, or fake logo/accent appears.
|
||||
3. **Given** a limited or internal report, **When** branding renders, **Then** branding does not hide warnings, evidence state, profile/audience, source metadata, or non-certification disclosure.
|
||||
|
||||
### User Story 4 - Print and screenshot the report safely (Priority: P2)
|
||||
|
||||
As an operator, I can print or screenshot the report for review meetings without printing admin toolbar actions, app shell artifacts, or hidden unsafe content.
|
||||
|
||||
**Why this priority**: The report is intended for meetings and customer delivery, so print behavior and screenshot proof are part of the product contract.
|
||||
|
||||
**Independent Test**: Browser smoke renders profile variants, simulates print CSS where supported, verifies toolbar hidden from print canvas, asserts no JavaScript errors, and saves screenshots under the Spec 366 artifact folder.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the report toolbar is visible on screen, **When** print mode is active, **Then** toolbar and screen-only controls are hidden while the report canvas and disclosure footer remain visible.
|
||||
2. **Given** a browser smoke run, **When** screenshots are captured, **Then** screenshots for customer executive ready/limited, internal MSP, customer technical, auditor appendix, and print view are stored under the Spec 366 artifacts path.
|
||||
3. **Given** a small/mobile-ish viewport, **When** the report renders, **Then** text remains readable and sections stack without overlapping.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
- **FR-366-001**: The rendered report MUST present a management-ready first screen with report identity, readiness/state hero, KPI/decision strip, executive summary, evidence basis, and next action or limitation.
|
||||
- **FR-366-002**: Report toolbar actions MUST remain outside the report canvas and MUST be hidden in print.
|
||||
- **FR-366-003**: The state hero MUST distinguish customer-safe, limited, internal-only, output-not-ready, and external-sharing-blocked states without false safe language.
|
||||
- **FR-366-004**: The KPI/decision strip MUST use only repo-backed values from existing Review Pack/review/evidence/report payloads; unsupported metrics MUST be omitted or shown as not measured/not available.
|
||||
- **FR-366-005**: The executive summary MUST be management-friendly and MUST NOT expose raw state keys, implementation field names, provider payload terms, or localization keys as dominant copy.
|
||||
- **FR-366-006**: The layout MUST adapt to the effective `ReportProfileRegistry` profile for `customer_executive`, `customer_technical`, `internal_msp_review`, and `auditor_appendix`.
|
||||
- **FR-366-007**: The report MUST render text co-branding slots: prepared by, prepared for, generated by, generated at, report profile, and report audience.
|
||||
- **FR-366-008**: Optional logo/accent/footer organization styling MUST be used only if repo-backed safe fields already exist; otherwise it MUST be deferred without adding persistence or upload UI.
|
||||
- **FR-366-009**: Mandatory disclosures from `ReportDisclosurePolicy` MUST remain visible in every profile and print output.
|
||||
- **FR-366-010**: Customer executive reports MUST keep appendix and technical detail secondary unless disclosure policy requires prominent limitations.
|
||||
- **FR-366-011**: Internal MSP and auditor appendix reports MAY expose more structured technical context, but raw JSON, secrets, fingerprints, provider payloads, and stack traces MUST remain hidden by default.
|
||||
- **FR-366-012**: Existing Review Pack ZIP/download behavior MUST remain unchanged.
|
||||
- **FR-366-013**: The rendered report MUST remain read-only and MUST NOT create, mutate, queue, deliver, approve, or send report artifacts.
|
||||
- **FR-366-014**: The rendered report route MUST keep existing authorization, current-export guard, expiry guard, and workspace/environment entitlement behavior.
|
||||
- **FR-366-015**: Implementation MUST not perform live provider/Graph calls while rendering theme, layout, or report content.
|
||||
- **FR-366-016**: Dominant report copy MUST be localized in EN and DE where new strings are introduced.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
- **NFR-366-001**: Enterprise visual quality - report output MUST pass the Spec 366 screenshot set without text overlap, preserve first-screen scanability for audience/readiness/evidence basis, remain print-legible, and either update `ui-099-rendered-review-report.md` or record a no-update rationale.
|
||||
- **NFR-366-002**: Deterministic rendering - same report/profile/theme input produces stable structure.
|
||||
- **NFR-366-003**: Accessibility - semantic headings, readable labels, keyboard-accessible toolbar controls, and no text overlap.
|
||||
- **NFR-366-004**: Print-friendly - toolbar hidden, disclosure visible, page breaks avoid broken cards/sections where practical.
|
||||
- **NFR-366-005**: Conservative customer-safe behavior - uncertain readiness must render limitations rather than safe/shareable claims.
|
||||
- **NFR-366-006**: No runtime expansion - no migrations, packages, env vars, queues, scheduler, storage topology, or panel provider changes are expected.
|
||||
|
||||
## Data / Truth Source Requirements
|
||||
|
||||
- Existing Review Pack summary payloads, Environment Review state, Evidence Snapshot state, `ReportProfileRegistry`, and `ReportDisclosurePolicy` remain the report truth inputs.
|
||||
- Branding is presentation only and derived from existing workspace/managed-environment values.
|
||||
- TenantPilot source metadata, generated timestamp, profile/audience, readiness/limitation state, and non-certification disclosure must remain visible.
|
||||
- Internal rationale and raw payloads must not be promoted into customer-safe report truth.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- Missing workspace name falls back to `TenantPilot` or a safe prepared-by label.
|
||||
- Missing managed-environment/customer name falls back to the existing environment/report label without exposing internal IDs as primary copy.
|
||||
- Unknown or placeholder report profile continues to fail closed through `ReportProfileRegistry` fallback behavior.
|
||||
- Evidence missing/partial/stale states make limitations visible near the top.
|
||||
- Customer-facing profiles requested for internal/PII output remain limited or blocked and cannot appear customer-safe.
|
||||
- Metrics with no repo-backed source are omitted or shown as not measured, never faked as zero.
|
||||
- Print mode hides toolbar but keeps disclosure and source metadata visible.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- **AC-366-001**: Customer executive report first screen shows management-ready identity, state hero, KPI strip, executive summary, evidence basis, and disclosure footer.
|
||||
- **AC-366-002**: Profile variants produce visibly different hierarchy while preserving mandatory disclosure output.
|
||||
- **AC-366-003**: Prepared by / prepared for / generated by / generated at are visible and use repo-backed values or safe fallbacks.
|
||||
- **AC-366-004**: Limited/internal reports never render as customer-safe or externally approved.
|
||||
- **AC-366-005**: Toolbar is outside the report canvas and hidden by print CSS.
|
||||
- **AC-366-006**: Review Pack ZIP/download contract remains unchanged.
|
||||
- **AC-366-007**: No new persistence, upload UI, profile CRUD, scheduled delivery, customer portal, AI, native PDF package, or compliance framework report is introduced.
|
||||
- **AC-366-008**: Focused Unit, Feature, Browser, Spec356, Spec357, ReviewPack, EnvironmentReview, Pint, and diff validation commands are planned and pass during implementation close-out.
|
||||
- **AC-366-009**: Browser screenshots are captured or an explicit repo-based reason is documented for any missing screenshot state.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- **SC-366-001**: A reviewer can identify report audience, shareability/readiness, top management summary, and evidence basis within the first visible report screen.
|
||||
- **SC-366-002**: All implemented profiles render without JavaScript/console errors in the bounded browser smoke.
|
||||
- **SC-366-003**: Tests prove no visible localization keys, raw JSON, stack traces, secrets, fingerprints, provider payloads, or false certification/shareability copy appear in default report output.
|
||||
- **SC-366-004**: Required screenshots show customer executive ready, customer executive limited, internal MSP, customer technical, auditor appendix, print view, and toolbar-hidden print behavior.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Native server-side PDF package or PDF generation stack.
|
||||
- Report profile CRUD or persisted report themes.
|
||||
- Logo upload, image storage, theme editor, or white-label portal.
|
||||
- Scheduled delivery, email/Teams sending, approval workflow, send ledger, or public links.
|
||||
- AI-generated narratives or HITL review.
|
||||
- NIS2, BSI, CIS, or other framework-specific report semantics.
|
||||
- Customer portal report consumption.
|
||||
- Review Pack ZIP contract changes.
|
||||
- New `OperationRun` semantics.
|
||||
|
||||
## Follow-up Spec Candidates
|
||||
|
||||
- Spec 367 - Scheduled Report Delivery Foundation v1.
|
||||
- Spec 368 - Compliance Report Profile Foundation v1.
|
||||
- Spec 369 - NIS2 / BSI Readiness Report v1.
|
||||
- Spec 370 - CIS Baseline Report v1.
|
||||
- Spec 371 - Customer Portal Report Consumption Boundary.
|
||||
- Spec 372 - Private AI/HITL Report Review Preparation.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Specs 356 and 357 are committed and stable on the current branch.
|
||||
- The existing authenticated rendered report route, profile registry, disclosure policy, and Review Pack authorization remain the baseline.
|
||||
- Workspace and managed-environment names are sufficient for text co-branding in v1.
|
||||
- Optional logo/accent fields are not assumed to exist; they remain deferred unless implementation verifies safe repo-backed fields.
|
||||
- The feature can be completed without migrations, new packages, env vars, queues, scheduler changes, storage changes, or Filament asset registration.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None blocking. Optional logo/accent support is intentionally treated as repo-backed-only and otherwise deferred.
|
||||
@ -0,0 +1,223 @@
|
||||
# Tasks: Spec 366 - Management Report Layout & Branded Report Themes v1
|
||||
|
||||
**Input**: Design documents from `/specs/366-management-report-layout-branded-report-themes-v1/`
|
||||
**Prerequisites**: `spec.md`, `plan.md`, `checklists/requirements.md`
|
||||
|
||||
**Tests**: Required. This feature changes an existing customer-facing rendered report surface. Use Pest 4 Unit, Feature, and one bounded Browser smoke. Keep Browser proof explicit and limited to Spec 366.
|
||||
|
||||
**Operations**: No new `OperationRun` start/completion/link behavior is in scope. Existing Review Pack generation behavior must remain unchanged.
|
||||
|
||||
**RBAC**: Existing Review Pack rendered-report authorization remains authoritative. Preserve workspace/environment entitlement, deny-as-not-found for non-members/out-of-scope records, and 403 for members missing `REVIEW_PACK_VIEW`.
|
||||
|
||||
**UI / Surface Guardrails**: This is a customer-facing report viewer surface. The implementation must update UI/productization coverage docs or record a proportional no-update rationale in close-out.
|
||||
|
||||
**Filament v5 / Livewire v4**: No panel provider change is planned. Livewire v4.0+ compliance must be preserved; do not introduce Livewire v3 APIs.
|
||||
|
||||
## 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 smoke addition is explicit.
|
||||
- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default; any widening is isolated or documented.
|
||||
- [x] Planned validation commands cover the change without pulling in unrelated lane cost.
|
||||
- [x] The declared surface test profile is `report-viewer / customer-facing artifact surface`.
|
||||
- [x] Any material budget, baseline, trend, or escalation note is recorded in the active spec or close-out.
|
||||
|
||||
## Phase 1: Setup and Repo Verification
|
||||
|
||||
**Purpose**: Re-confirm the baseline and keep the implementation from reopening completed report foundations.
|
||||
|
||||
- [x] T001 Re-read `specs/366-management-report-layout-branded-report-themes-v1/spec.md`, `specs/366-management-report-layout-branded-report-themes-v1/plan.md`, and `specs/366-management-report-layout-branded-report-themes-v1/checklists/requirements.md` before runtime changes.
|
||||
- [x] T002 [P] Re-read completed context only in `specs/356-review-pack-pdf-html-renderer-v1/spec.md`, `specs/356-review-pack-pdf-html-renderer-v1/plan.md`, `specs/357-report-profiles-disclosure-policy-v1/spec.md`, and `specs/357-report-profiles-disclosure-policy-v1/plan.md`; do not rewrite those packages.
|
||||
- [x] T003 Confirm branch/worktree intent with `git status --short --branch` and record the baseline commit in `specs/366-management-report-layout-branded-report-themes-v1/repo-truth-map.md`.
|
||||
- [x] T004 [P] Inspect current report seams in `apps/platform/app/Http/Controllers/ReviewPackRenderedReportController.php`, `apps/platform/resources/views/review-packs/rendered-report.blade.php`, and `apps/platform/app/Services/ReviewPackService.php`.
|
||||
- [x] T005 [P] Inspect current profile/disclosure seams in `apps/platform/app/Support/ReviewPacks/ReportProfileRegistry.php`, `apps/platform/app/Support/ReviewPacks/ReportDisclosurePolicy.php`, `apps/platform/app/Support/ReviewPacks/ReviewPackOutputReadiness.php`, and `apps/platform/app/Support/ReviewPacks/ReviewPackOutputResolutionGuidance.php`.
|
||||
- [x] T006 [P] Inspect current owner-surface rendered-report links in `apps/platform/app/Filament/Resources/EnvironmentReviewResource.php`, `apps/platform/app/Filament/Resources/EnvironmentReviewResource/Pages/ViewEnvironmentReview.php`, `apps/platform/app/Filament/Resources/ReviewPackResource.php`, and `apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php`.
|
||||
- [x] T007 Record current report layout gaps, existing repo-backed metrics, optional branding fields, and fields that are not repo-backed in `specs/366-management-report-layout-branded-report-themes-v1/repo-truth-map.md`.
|
||||
- [x] T008 If current rendered-report screenshots are needed for implementation decisions, create `specs/366-management-report-layout-branded-report-themes-v1/current-report-layout-audit.md`; otherwise record in `repo-truth-map.md` why the audit artifact is not needed.
|
||||
- [x] T009 Confirm no migration, package, env var, queue family, scheduler change, storage-topology change, panel/provider change, global-search change, native PDF package, upload UI, customer portal, scheduled delivery, or AI/runtime work is required.
|
||||
- [x] T010 Confirm Filament v5 / Livewire v4.0+ compliance and that panel providers remain registered in `apps/platform/bootstrap/providers.php`.
|
||||
|
||||
## Phase 2: Foundational Tests and Guardrails
|
||||
|
||||
**Purpose**: Add failing proof for theme/layout, report safety, profile behavior, print behavior, and screenshot smoke before implementation.
|
||||
|
||||
- [x] T011 [P] Add `apps/platform/tests/Unit/Support/ReviewPacks/Spec366ReportThemeContractTest.php` covering prepared-by/prepared-for/generated-by fallbacks, generated-at formatting input, default accent/logo behavior, profile layout-mode mapping, and no persistence requirement.
|
||||
- [x] T012 [P] Add `apps/platform/tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php` covering rendered report cover/state/KPI/executive-summary order, semantic heading order, readable labels, mandatory disclosures, profile/audience metadata, toolbar-before-canvas, print CSS, and no localization key leakage.
|
||||
- [x] T013 [P] Add `apps/platform/tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php` cases proving limited/internal/PII reports never render as customer-safe or externally approved.
|
||||
- [x] T014 [P] Add `apps/platform/tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php` cases proving Review Pack ZIP/download behavior, current-export guard, expiry guard, and rendered-report authorization remain unchanged.
|
||||
- [x] T015 [P] Add `apps/platform/tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php` assertions that report render performs no Graph/provider calls by binding a fail-hard `GraphClientInterface`.
|
||||
- [x] T016 [P] Add `apps/platform/tests/Browser/Spec366ManagementReportLayoutSmokeTest.php` using existing Spec357 browser helper conventions where practical.
|
||||
- [x] T017 [P] Add browser flows in `apps/platform/tests/Browser/Spec366ManagementReportLayoutSmokeTest.php` for customer executive ready, customer executive limited, internal MSP, customer technical, auditor appendix, print-view class/CSS behavior, keyboard/focus basics, and mobile-ish width.
|
||||
- [x] T018 Add local Spec366 fixture/helper functions inside `apps/platform/tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php` and `apps/platform/tests/Browser/Spec366ManagementReportLayoutSmokeTest.php`; reuse Spec356/Spec357 patterns without introducing broad shared test defaults.
|
||||
|
||||
## Phase 3: User Story 1 - Management-Ready First Screen (Priority: P1)
|
||||
|
||||
**Goal**: The report first screen answers who prepared it, who it is for, whether it is shareable, what the governance state is, what top metrics matter, and what should happen next.
|
||||
|
||||
**Independent Test**: The Feature test renders ready and limited reports and asserts the cover/state/KPI/executive-summary hierarchy appears before appendix/detail content.
|
||||
|
||||
- [x] T019 [P] [US1] Add customer-executive ready-state assertions to `apps/platform/tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php`.
|
||||
- [x] T020 [P] [US1] Add limited-state and internal-state assertions to `apps/platform/tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php`.
|
||||
- [x] T021 [US1] Update `apps/platform/app/Http/Controllers/ReviewPackRenderedReportController.php` to expose a stable management first-screen payload: hero, report identity, profile/audience, KPI/decision strip, executive story, evidence basis, top risks/decisions, and next action.
|
||||
- [x] T022 [US1] Ensure `apps/platform/app/Http/Controllers/ReviewPackRenderedReportController.php` derives KPI/decision strip values only from existing Review Pack summary, Environment Review, Evidence Snapshot, profile, and disclosure-policy data.
|
||||
- [x] T023 [US1] Update `apps/platform/resources/views/review-packs/rendered-report.blade.php` so the cover/state/KPI/executive summary render before limitations/evidence/appendix sections.
|
||||
- [x] T024 [US1] In `apps/platform/resources/views/review-packs/rendered-report.blade.php`, render unsupported metrics as omitted or "not measured" / "not available"; do not fake zero counts.
|
||||
- [x] T025 [US1] In `apps/platform/resources/views/review-packs/rendered-report.blade.php`, ensure default-visible report copy avoids raw state keys, implementation field names, provider payload terms, and localization keys.
|
||||
- [x] T026 [US1] Keep mandatory disclosure data from `apps/platform/app/Support/ReviewPacks/ReportDisclosurePolicy.php` visible in the report after first-screen changes.
|
||||
- [x] T027 [US1] Preserve existing source metadata, review id, review-pack id, profile, audience, generated timestamp, and TenantPilot-generated marker in `apps/platform/resources/views/review-packs/rendered-report.blade.php`.
|
||||
|
||||
## Phase 4: User Story 2 - Profile-Aware Report Hierarchy (Priority: P1)
|
||||
|
||||
**Goal**: Existing Spec357 profiles produce visibly different report hierarchy while preserving disclosure and fail-closed behavior.
|
||||
|
||||
**Independent Test**: Feature and Browser tests render all implemented profiles and verify section order, appendix prominence, and safety copy.
|
||||
|
||||
- [x] T028 [P] [US2] Add profile-order assertions for `customer_executive` and `customer_technical` to `apps/platform/tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php`.
|
||||
- [x] T029 [P] [US2] Add profile-order assertions for `internal_msp_review` and `auditor_appendix` to `apps/platform/tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php`.
|
||||
- [x] T030 [P] [US2] Add fallback assertions for unknown or placeholder profile requests to `apps/platform/tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php`.
|
||||
- [x] T031 [US2] Update `apps/platform/app/Http/Controllers/ReviewPackRenderedReportController.php` to derive profile layout mode and section order from `apps/platform/app/Support/ReviewPacks/ReportProfileRegistry.php`.
|
||||
- [x] T032 [US2] If partialization improves reviewability, split profile sections from `apps/platform/resources/views/review-packs/rendered-report.blade.php` into `apps/platform/resources/views/review-packs/partials/report-cover.blade.php`, `report-state-hero.blade.php`, `report-kpi-strip.blade.php`, `report-executive-summary.blade.php`, `report-appendix.blade.php`, and `report-disclosure-footer.blade.php`.
|
||||
- [x] T033 [US2] Keep `customer_executive` appendix minimal in `apps/platform/resources/views/review-packs/rendered-report.blade.php` or the new report partials.
|
||||
- [x] T034 [US2] Make `auditor_appendix` evidence basis, section completeness, source metadata, and appendix content more prominent in `apps/platform/resources/views/review-packs/rendered-report.blade.php` or the new report partials.
|
||||
- [x] T035 [US2] Ensure `internal_msp_review` renders internal warning and operator limitations clearly in `apps/platform/resources/views/review-packs/rendered-report.blade.php`.
|
||||
- [x] T036 [US2] Preserve `ReportProfileRegistry` fail-closed behavior in `apps/platform/app/Support/ReviewPacks/ReportProfileRegistry.php`; do not broaden implemented profiles or add framework-report semantics.
|
||||
|
||||
## Phase 5: User Story 3 - Controlled Co-Branding and Theme Contract (Priority: P2)
|
||||
|
||||
**Goal**: Text-only report co-branding uses existing workspace/environment truth and never weakens report safety or disclosure.
|
||||
|
||||
**Independent Test**: Unit and Feature tests verify theme derivation, fallback behavior, and absence of upload/persistence/theme-editor behavior.
|
||||
|
||||
- [x] T037 [P] [US3] Add unit assertions in `apps/platform/tests/Unit/Support/ReviewPacks/Spec366ReportThemeContractTest.php` for workspace name, environment name, missing-name fallback, generated-by, null logo, and default accent.
|
||||
- [x] T038 [P] [US3] Add feature assertions in `apps/platform/tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php` that branding does not hide state hero, limitations, profile/audience, source metadata, or non-certification disclosure.
|
||||
- [x] T039 [US3] Create `apps/platform/app/Support/ReviewPacks/ReportThemeResolver.php` only if implementation confirms a derived resolver is narrower and safer than controller-local theme data.
|
||||
- [x] T040 [US3] If `ReportThemeResolver.php` is created, keep it derived-only: no model, no table, no config write, no upload, no cache across requests.
|
||||
- [x] T041 [US3] If `ReportThemeResolver.php` is not created, document the narrower controller-local decision in `specs/366-management-report-layout-branded-report-themes-v1/repo-truth-map.md` and point `Spec366ReportThemeContractTest.php` at the actual derived shape.
|
||||
- [x] T042 [US3] Update `apps/platform/app/Http/Controllers/ReviewPackRenderedReportController.php` to resolve `prepared_by`, `prepared_for`, `generated_by`, `generated_at`, `accent`, and `logo` from existing workspace/environment/report truth or safe defaults.
|
||||
- [x] T043 [US3] In `apps/platform/resources/views/review-packs/rendered-report.blade.php`, render co-branding slots as text-first identity; do not add logo upload, image storage, or theme editor UI.
|
||||
- [x] T044 [US3] Verify optional logo/accent support in `apps/platform/app/Http/Controllers/ReviewPackRenderedReportController.php` stays null/default unless safe repo-backed fields already exist.
|
||||
- [x] T045 [US3] Ensure `apps/platform/resources/views/review-packs/rendered-report.blade.php` keeps TenantPilot generated metadata visible even when workspace/MSP branding is present.
|
||||
|
||||
## Phase 6: User Story 4 - Print, Screenshot, and Responsive Smoke (Priority: P2)
|
||||
|
||||
**Goal**: The report can be printed and screenshotted without app/admin toolbar artifacts or visual overlap.
|
||||
|
||||
**Independent Test**: Browser smoke captures profile and print-view screenshots, verifies toolbar-hidden print behavior, and asserts no JS/console errors.
|
||||
|
||||
- [x] T046 [P] [US4] Add browser assertion in `apps/platform/tests/Browser/Spec366ManagementReportLayoutSmokeTest.php` that `data-testid="rendered-report-toolbar"` appears before `data-testid="rendered-report-canvas"` on screen and toolbar controls are keyboard-focusable.
|
||||
- [x] T047 [P] [US4] Add browser assertion in `apps/platform/tests/Browser/Spec366ManagementReportLayoutSmokeTest.php` that print-preview class or print CSS hides toolbar/screen-only controls while keeping report canvas and disclosure footer visible.
|
||||
- [x] T048 [P] [US4] Add browser screenshot capture for `01-customer-executive-report.png` and `02-customer-executive-limited-report.png` under `specs/366-management-report-layout-branded-report-themes-v1/artifacts/screenshots/`.
|
||||
- [x] T049 [P] [US4] Add browser screenshot capture for `03-internal-msp-report.png`, `04-customer-technical-report.png`, and `05-auditor-appendix-report.png` under `specs/366-management-report-layout-branded-report-themes-v1/artifacts/screenshots/`.
|
||||
- [x] T050 [P] [US4] Add browser screenshot capture for `06-print-view.png` and `07-report-toolbar-hidden-print.png` under `specs/366-management-report-layout-branded-report-themes-v1/artifacts/screenshots/`.
|
||||
- [x] T051 [US4] Update print CSS in `apps/platform/resources/views/review-packs/rendered-report.blade.php` or report partials so toolbar/screen-only controls are hidden and disclosure/source metadata remain visible.
|
||||
- [x] T052 [US4] Update report canvas CSS in `apps/platform/resources/views/review-packs/rendered-report.blade.php` or report partials to prevent text overlap and keep small/mobile-ish viewport stacking readable.
|
||||
- [x] T053 [US4] In `apps/platform/tests/Browser/Spec366ManagementReportLayoutSmokeTest.php`, assert `assertNoJavaScriptErrors()` and `assertNoConsoleLogs()` for all profile flows.
|
||||
|
||||
## Phase 7: Localization and Copy
|
||||
|
||||
**Purpose**: Add only the required dominant report copy and keep EN/DE output free of raw keys.
|
||||
|
||||
- [x] T054 [P] Add or update EN keys in `apps/platform/lang/en/localization.php` for prepared by, prepared for, generated by, report profile, governance status, KPI/decision strip, evidence coverage, open decisions, key risks, supporting appendix, not measured, not available, internal report, and external sharing warning.
|
||||
- [x] T055 [P] Add or update DE keys in `apps/platform/lang/de/localization.php` for the same keys added in `apps/platform/lang/en/localization.php`.
|
||||
- [x] T056 Update `apps/platform/tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php` to assert no `localization.` key appears in rendered output.
|
||||
- [x] T057 Update `apps/platform/tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php` to assert the report does not show "Certified report", "Approved compliance report", or "Share with customer" unless a future spec explicitly permits those terms.
|
||||
|
||||
## Phase 8: UI Coverage and Documentation Artifacts
|
||||
|
||||
**Purpose**: Keep UI/Productization Coverage in sync without adding broad docs.
|
||||
|
||||
- [x] T058 Inspect `docs/ui-ux-enterprise-audit/page-reports/ui-099-rendered-review-report.md` and update it if Spec 366 materially changes rendered-report hierarchy, profile behavior, or screenshot expectations.
|
||||
- [x] T059 Inspect `docs/ui-ux-enterprise-audit/design-coverage-matrix.md` and `docs/ui-ux-enterprise-audit/route-inventory.md`; update only if rendered-report classification or route coverage changes.
|
||||
- [x] T060 If coverage docs are not changed, record a proportional no-update rationale in `specs/366-management-report-layout-branded-report-themes-v1/repo-truth-map.md` or implementation close-out notes.
|
||||
- [x] T061 Do not create general documentation files outside required Spec Kit/UI coverage artifacts unless the implementation proves a specific existing registry artifact must be updated.
|
||||
|
||||
## Phase 9: Validation and Close-Out
|
||||
|
||||
**Purpose**: Prove Spec 366 and adjacent report regressions before handoff.
|
||||
|
||||
- [x] T062 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Unit/Support/ReviewPacks/Spec366ReportThemeContractTest.php --compact`.
|
||||
- [x] T063 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php --compact`.
|
||||
- [x] T064 Run `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec366ManagementReportLayoutSmokeTest.php --compact`.
|
||||
- [x] T065 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec356`.
|
||||
- [x] T066 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec357`.
|
||||
- [x] T067 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=ReviewPack`.
|
||||
- [x] T068 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=EnvironmentReview`.
|
||||
- [x] T069 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`.
|
||||
- [x] T070 Run `git diff --check`.
|
||||
- [x] T071 Review the final diff for migrations, packages, env vars, queues, scheduler changes, storage topology changes, panel provider changes, global search changes, registered Filament assets, native PDF, upload UI, customer portal, scheduled delivery, AI, and compliance framework scope.
|
||||
- [x] T072 Confirm no new Filament assets were registered; if assets were registered unexpectedly, update `specs/366-management-report-layout-branded-report-themes-v1/plan.md` with the `filament:assets` deployment impact before merge.
|
||||
- [x] T073 Confirm Livewire v4.0+ compliance remains unchanged and no Livewire v3 APIs were introduced in changed files under `apps/platform/`.
|
||||
- [x] T074 Confirm globally searchable resources were not enabled or changed; if global search was touched unexpectedly, document View/Edit page safety or keep global search disabled.
|
||||
- [x] T075 Confirm no destructive/high-impact report action was added; report toolbar actions remain read-only navigation/download/print.
|
||||
- [x] T076 Confirm `specs/366-management-report-layout-branded-report-themes-v1/artifacts/screenshots/` contains required screenshots or a repo-based reason for any missing screenshot state.
|
||||
- [x] T077 Record implementation close-out notes in `specs/366-management-report-layout-branded-report-themes-v1/tasks.md` or the final implementation response: changed files, no-migration status, no-asset status, test results, browser smoke result, coverage-doc decision, and deferred follow-up candidates.
|
||||
|
||||
## Explicit Non-Goals
|
||||
|
||||
- [x] NT001 Do not rebuild the Review Pack renderer.
|
||||
- [x] NT002 Do not create a second report renderer.
|
||||
- [x] NT003 Do not change Review Pack ZIP/download contracts.
|
||||
- [x] NT004 Do not add a native PDF package or dependency.
|
||||
- [x] NT005 Do not build report profile CRUD or persisted report themes.
|
||||
- [x] NT006 Do not add logo upload, image storage, or a theme editor.
|
||||
- [x] NT007 Do not add customer portal, public sharing links, scheduled delivery, email/Teams delivery, or approval workflow.
|
||||
- [x] NT008 Do not add AI-generated narratives or HITL AI review.
|
||||
- [x] NT009 Do not add NIS2, BSI, CIS, or other framework-specific report semantics.
|
||||
- [x] NT010 Do not hide or weaken mandatory disclosures, readiness, limitations, evidence state, internal-only warning, PII warning, or TenantPilot source metadata.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Phase 1 must finish before tests and implementation.
|
||||
- Phase 2 tests should be added before Phase 3-6 implementation.
|
||||
- US1 and US2 are the MVP path and should land before US3/US4 refinements.
|
||||
- US3 depends on the theme/layout contract decision from Phase 2 and Phase 3.
|
||||
- US4 depends on the report canvas and section hierarchy from US1/US2.
|
||||
- Phase 7 localization can run alongside US1-US4 after new copy is known.
|
||||
- Phase 8 coverage close-out should happen after the runtime diff is known.
|
||||
- Phase 9 runs last.
|
||||
|
||||
## Parallel Execution Examples
|
||||
|
||||
After Phase 1 verification:
|
||||
|
||||
```text
|
||||
T011, T012, T016 can run in parallel because they create different test files.
|
||||
T054 and T055 can run in parallel after copy keys are identified.
|
||||
T058 and T059 can run in parallel after runtime/UI changes are known.
|
||||
```
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
1. MVP first: US1 + US2 with route/feature tests proving management-ready and profile-aware layout.
|
||||
2. Add co-branding/theme derivation only if it stays derived, local, and testable.
|
||||
3. Add print/browser screenshot proof after the report layout is stable.
|
||||
4. Keep every deferred idea as a follow-up spec rather than hidden scope.
|
||||
|
||||
## Expected Task Count
|
||||
|
||||
- Total implementation tasks: 77
|
||||
- Non-goal guardrails: 10
|
||||
- MVP tasks: T001-T036 plus required validation subset T062-T070
|
||||
|
||||
## Implementation Close-Out
|
||||
|
||||
- Runtime changes stayed bounded to the existing rendered report route, derived report theme/layout support, localization copy, and one adjacent resolution-action mapping guard.
|
||||
- No migrations, packages, env vars, queues, scheduler changes, storage topology changes, panel provider changes, global-search changes, registered Filament assets, native PDF runtime, upload UI, customer portal, scheduled delivery, AI, or framework-specific report semantics were added.
|
||||
- UI coverage artifacts were updated for UI-099 and screenshot evidence was generated under `artifacts/screenshots/`.
|
||||
- Adjacent regression fixes were applied only where validation exposed stale or conflicting repo truth:
|
||||
- Spec347 browser fixture now stores the non-certification disclosure on the generated review pack summary.
|
||||
- `export_not_ready` no longer maps to `create_next_review`, preserving `export_executive_pack` as the primary published-review header action.
|
||||
- Validation completed:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Unit/Support/ReviewPacks/Spec366ReportThemeContractTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/ReviewPack/Spec366RenderedReportLayoutTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec366ManagementReportLayoutSmokeTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec347ReviewPackOutputReadinessSmokeTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Filament/EnvironmentReviewHeaderDisciplineTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/EnvironmentReview/Spec351EnvironmentReviewResolveActionTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/EnvironmentReview/Spec350EnvironmentReviewResolutionGuidanceTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec357`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=ReviewPack`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=EnvironmentReview`
|
||||
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
|
||||
- `git diff --check`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec356` returned `No tests found`; the active adjacent rendered-report coverage is exercised through Spec357 and the new Spec366 browser/feature tests.
|
||||