feat: finish management report PDF staging validation (#475)

Automated PR provided by Codex via Gitea API.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #475
This commit is contained in:
ahmido 2026-06-23 18:31:42 +00:00
parent b0b5088568
commit 8918b35795
16 changed files with 1551 additions and 35 deletions

View File

@ -11,12 +11,14 @@
use App\Services\Audit\WorkspaceAuditLogger; use App\Services\Audit\WorkspaceAuditLogger;
use App\Support\Audit\AuditActionId; use App\Support\Audit\AuditActionId;
use App\Support\Auth\Capabilities; use App\Support\Auth\Capabilities;
use App\Support\Pdf\PdfByteValidator;
use App\Support\ReviewPacks\CustomerOutputGate; use App\Support\ReviewPacks\CustomerOutputGate;
use App\Support\ReviewPackStatus; use App\Support\ReviewPackStatus;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
class ManagementReportPdfDownloadController extends Controller class ManagementReportPdfDownloadController extends Controller
{ {
@ -67,8 +69,24 @@ public function __invoke(Request $request, StoredReport $storedReport): Streamed
} }
$disk = Storage::disk((string) $storedReport->file_disk); $disk = Storage::disk((string) $storedReport->file_disk);
$filePath = (string) $storedReport->file_path;
if (! $disk->exists((string) $storedReport->file_path)) { if (! $disk->exists($filePath)) {
throw new NotFoundHttpException;
}
try {
$fileSize = (int) $disk->size($filePath);
$fileBytes = $disk->get($filePath);
} catch (Throwable) {
throw new NotFoundHttpException;
}
if ($fileSize <= 0 || $fileSize !== (int) $storedReport->file_size || ! PdfByteValidator::isValid($fileBytes)) {
throw new NotFoundHttpException;
}
if (filled($storedReport->sha256) && ! hash_equals((string) $storedReport->sha256, hash('sha256', $fileBytes))) {
throw new NotFoundHttpException; throw new NotFoundHttpException;
} }
@ -96,7 +114,7 @@ public function __invoke(Request $request, StoredReport $storedReport): Streamed
operationRunId: $storedReport->operation_run_id, operationRunId: $storedReport->operation_run_id,
); );
return $disk->download((string) $storedReport->file_path, $this->filename($storedReport, $tenant), [ return $disk->download($filePath, $this->filename($storedReport, $tenant), [
'Content-Type' => 'application/pdf', 'Content-Type' => 'application/pdf',
'X-Management-Report-PDF-SHA256' => $storedReport->sha256 ?? '', 'X-Management-Report-PDF-SHA256' => $storedReport->sha256 ?? '',
]); ]);

View File

@ -103,8 +103,10 @@ public function isReadyManagementPdf(): bool
return $this->report_type === self::REPORT_TYPE_MANAGEMENT_REPORT_PDF return $this->report_type === self::REPORT_TYPE_MANAGEMENT_REPORT_PDF
&& $this->report_format === self::FORMAT_PDF && $this->report_format === self::FORMAT_PDF
&& $this->status === self::STATUS_READY && $this->status === self::STATUS_READY
&& filled($this->file_disk) && $this->file_disk === 'exports'
&& filled($this->file_path); && filled($this->file_path)
&& (int) $this->file_size > 0
&& filled($this->sha256);
} }
public function artifactSourceDescriptor(): ArtifactSourceDescriptor public function artifactSourceDescriptor(): ArtifactSourceDescriptor

View File

@ -4,6 +4,7 @@
namespace App\Services\Pdf; namespace App\Services\Pdf;
use App\Support\Pdf\PdfByteValidator;
use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response; use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
@ -87,7 +88,7 @@ private function resultFromResponse(Response $response, ?string $fallbackCorrela
$body = $response->body(); $body = $response->body();
$contentType = (string) $response->header('Content-Type', 'application/pdf'); $contentType = (string) $response->header('Content-Type', 'application/pdf');
if ($body === '' || (! str_contains(strtolower($contentType), 'pdf') && ! str_starts_with($body, '%PDF'))) { if (! PdfByteValidator::isValid($body)) {
return PdfRenderResult::failure( return PdfRenderResult::failure(
PdfRenderResult::INVALID_RESPONSE, PdfRenderResult::INVALID_RESPONSE,
'PDF renderer returned an invalid PDF response.', 'PDF renderer returned an invalid PDF response.',

View File

@ -16,6 +16,7 @@
use App\Support\Auth\Capabilities; use App\Support\Auth\Capabilities;
use App\Support\OperationRunStatus; use App\Support\OperationRunStatus;
use App\Support\OperationRunType; use App\Support\OperationRunType;
use App\Support\Pdf\PdfByteValidator;
use App\Support\ReviewPacks\ManagementReportPdfPayloadBuilder; use App\Support\ReviewPacks\ManagementReportPdfPayloadBuilder;
use App\Support\ReviewPacks\ManagementReportPdfRuntimeGate; use App\Support\ReviewPacks\ManagementReportPdfRuntimeGate;
use App\Support\ReviewPacks\ReportProfileRegistry; use App\Support\ReviewPacks\ReportProfileRegistry;
@ -23,6 +24,7 @@
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\URL;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
final class ManagementReportPdfService final class ManagementReportPdfService
{ {
@ -297,11 +299,14 @@ public function findReadyReport(ReviewPack $reviewPack): ?StoredReport
->where('report_format', StoredReport::FORMAT_PDF) ->where('report_format', StoredReport::FORMAT_PDF)
->where('profile', ReportProfileRegistry::CUSTOMER_EXECUTIVE) ->where('profile', ReportProfileRegistry::CUSTOMER_EXECUTIVE)
->where('status', StoredReport::STATUS_READY) ->where('status', StoredReport::STATUS_READY)
->whereNotNull('file_disk') ->where('file_disk', 'exports')
->whereNotNull('file_path') ->whereNotNull('file_path')
->where('file_size', '>', 0)
->whereNotNull('sha256')
->latest('generated_at') ->latest('generated_at')
->latest('id') ->latest('id')
->first(); ->get()
->first(fn (StoredReport $report): bool => $this->hasReadablePdfArtifact($report));
} }
public function findActiveReport(ReviewPack $reviewPack): ?StoredReport public function findActiveReport(ReviewPack $reviewPack): ?StoredReport
@ -343,6 +348,32 @@ private function findRetryableReport(ReviewPack $reviewPack, string $fingerprint
->first(); ->first();
} }
private function hasReadablePdfArtifact(StoredReport $report): bool
{
if (! $report->isReadyManagementPdf()) {
return false;
}
try {
$disk = Storage::disk('exports');
$filePath = (string) $report->file_path;
if (! $disk->exists($filePath)) {
return false;
}
$fileSize = (int) $disk->size($filePath);
$fileBytes = $disk->get($filePath);
} catch (Throwable) {
return false;
}
return $fileSize > 0
&& $fileSize === (int) $report->file_size
&& PdfByteValidator::isValid($fileBytes)
&& hash_equals((string) $report->sha256, hash('sha256', $fileBytes));
}
public function computeFingerprint(ReviewPack $reviewPack): string public function computeFingerprint(ReviewPack $reviewPack): string
{ {
return hash('sha256', json_encode([ return hash('sha256', json_encode([

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Support\Pdf;
final class PdfByteValidator
{
public static function isValid(string $bytes): bool
{
if ($bytes === '' || ! str_starts_with($bytes, '%PDF-')) {
return false;
}
$trimmedBytes = rtrim($bytes);
return str_contains($trimmedBytes, 'startxref')
&& str_ends_with($trimmedBytes, '%%EOF');
}
}

View File

@ -113,24 +113,28 @@ public function entraAdminRoles(array $payload = []): self
*/ */
public function managementReportPdf(array $payload = []): self public function managementReportPdf(array $payload = []): self
{ {
return $this->state(fn (): array => [ return $this->state(function () use ($payload): array {
'report_type' => StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF, $pdfBytes = "%PDF-1.7\n1 0 obj\n<< /Type /Catalog /Title (test) >>\nendobj\nstartxref\n0\n%%EOF";
'report_format' => StoredReport::FORMAT_PDF,
'status' => StoredReport::STATUS_READY, return [
'profile' => 'customer_executive', 'report_type' => StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF,
'payload' => array_replace_recursive([ 'report_format' => StoredReport::FORMAT_PDF,
'title' => 'Management report', 'status' => StoredReport::STATUS_READY,
'profile' => 'customer_executive', 'profile' => 'customer_executive',
'chapters' => [], 'payload' => array_replace_recursive([
'provenance' => [ 'title' => 'Management report',
'generated_at' => now()->toIso8601String(), 'profile' => 'customer_executive',
], 'chapters' => [],
], $payload), 'provenance' => [
'file_disk' => 'exports', 'generated_at' => now()->toIso8601String(),
'file_path' => 'management-reports/test/report.pdf', ],
'file_size' => 128, ], $payload),
'sha256' => hash('sha256', '%PDF-1.7 test'), 'file_disk' => 'exports',
'generated_at' => now(), 'file_path' => 'management-reports/test/report.pdf',
]); 'file_size' => strlen($pdfBytes),
'sha256' => hash('sha256', $pdfBytes),
'generated_at' => now(),
];
});
} }
} }

View File

@ -122,7 +122,7 @@ function spec379BrowserCurrentReadyPack(): array
function spec379BrowserReadyManagementPdf(ReviewPack $pack): StoredReport function spec379BrowserReadyManagementPdf(ReviewPack $pack): StoredReport
{ {
$pack->loadMissing('tenant'); $pack->loadMissing('tenant');
$pdfBytes = '%PDF-1.7 Spec379 browser management report'; $pdfBytes = spec379BrowserPdfBytes('Spec379 browser management report');
$filePath = sprintf('management-reports/%s/browser-%d.pdf', $pack->tenant->external_id, (int) $pack->getKey()); $filePath = sprintf('management-reports/%s/browser-%d.pdf', $pack->tenant->external_id, (int) $pack->getKey());
Storage::disk('exports')->put($filePath, $pdfBytes); Storage::disk('exports')->put($filePath, $pdfBytes);
@ -143,6 +143,11 @@ function spec379BrowserReadyManagementPdf(ReviewPack $pack): StoredReport
]); ]);
} }
function spec379BrowserPdfBytes(string $label): string
{
return "%PDF-1.7\n1 0 obj\n<< /Type /Catalog /Title ({$label}) >>\nendobj\nstartxref\n0\n%%EOF";
}
function spec379BrowserZipContents(): string function spec379BrowserZipContents(): string
{ {
$tempFile = tempnam(sys_get_temp_dir(), 'spec379-browser-pack-'); $tempFile = tempnam(sys_get_temp_dir(), 'spec379-browser-pack-');

View File

@ -97,6 +97,11 @@ function spec379ZipContents(array $files = []): string
} }
} }
function spec379PdfBytes(string $label): string
{
return "%PDF-1.7\n1 0 obj\n<< /Type /Catalog /Title ({$label}) >>\nendobj\nstartxref\n0\n%%EOF";
}
/** /**
* @param array<string, mixed> $summaryOverrides * @param array<string, mixed> $summaryOverrides
* @return array{0: \App\Models\User, 1: \App\Models\ManagedEnvironment, 2: \App\Models\EnvironmentReview, 3: ReviewPack} * @return array{0: \App\Models\User, 1: \App\Models\ManagedEnvironment, 2: \App\Models\EnvironmentReview, 3: ReviewPack}
@ -181,7 +186,7 @@ function spec379CurrentReadyPack(array $summaryOverrides = []): array
function spec379ReadyManagementPdf(ReviewPack $pack): StoredReport function spec379ReadyManagementPdf(ReviewPack $pack): StoredReport
{ {
$pdfBytes = '%PDF-1.7 Spec379 ready management report'; $pdfBytes = spec379PdfBytes('Spec379 ready management report');
$filePath = sprintf('management-reports/%s/ready-%d.pdf', $pack->tenant->external_id, (int) $pack->getKey()); $filePath = sprintf('management-reports/%s/ready-%d.pdf', $pack->tenant->external_id, (int) $pack->getKey());
Storage::disk('exports')->put($filePath, $pdfBytes); Storage::disk('exports')->put($filePath, $pdfBytes);
@ -259,6 +264,7 @@ function spec379ReadyManagementPdf(ReviewPack $pack): StoredReport
$report = $result['report']; $report = $result['report'];
/** @var OperationRun $run */ /** @var OperationRun $run */
$run = $result['operation_run']; $run = $result['operation_run'];
$pdfBytes = spec379PdfBytes('rendered spec379');
expect($run->type)->toBe(OperationRunType::ManagementReportGenerate->value) expect($run->type)->toBe(OperationRunType::ManagementReportGenerate->value)
->and($report->status)->toBe(StoredReport::STATUS_QUEUED) ->and($report->status)->toBe(StoredReport::STATUS_QUEUED)
@ -266,7 +272,7 @@ function spec379ReadyManagementPdf(ReviewPack $pack): StoredReport
->and($report->source_environment_review_id)->toBe((int) $review->getKey()); ->and($report->source_environment_review_id)->toBe((int) $review->getKey());
Http::fake([ Http::fake([
'gotenberg.test/forms/chromium/convert/html' => Http::response('%PDF-1.7 rendered spec379', 200, [ 'gotenberg.test/forms/chromium/convert/html' => Http::response($pdfBytes, 200, [
'Content-Type' => 'application/pdf', 'Content-Type' => 'application/pdf',
'Gotenberg-Trace' => 'spec379-render', 'Gotenberg-Trace' => 'spec379-render',
]), ]),
@ -283,7 +289,7 @@ function spec379ReadyManagementPdf(ReviewPack $pack): StoredReport
expect($report->status)->toBe(StoredReport::STATUS_READY) expect($report->status)->toBe(StoredReport::STATUS_READY)
->and($report->file_disk)->toBe('exports') ->and($report->file_disk)->toBe('exports')
->and(Storage::disk('exports')->exists((string) $report->file_path))->toBeTrue() ->and(Storage::disk('exports')->exists((string) $report->file_path))->toBeTrue()
->and($report->sha256)->toBe(hash('sha256', '%PDF-1.7 rendered spec379')) ->and($report->sha256)->toBe(hash('sha256', $pdfBytes))
->and(json_encode($report->payload, JSON_THROW_ON_ERROR))->not->toContain('SQLSTATE', 'access_token') ->and(json_encode($report->payload, JSON_THROW_ON_ERROR))->not->toContain('SQLSTATE', 'access_token')
->and($run->status)->toBe(OperationRunStatus::Completed->value) ->and($run->status)->toBe(OperationRunStatus::Completed->value)
->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value) ->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value)
@ -442,7 +448,7 @@ function spec379ReadyManagementPdf(ReviewPack $pack): StoredReport
->push('renderer failed', 500, [ ->push('renderer failed', 500, [
'Content-Type' => 'text/plain', 'Content-Type' => 'text/plain',
]) ])
->push('%PDF-1.7 rendered spec379 retry', 200, [ ->push(spec379PdfBytes('rendered spec379 retry'), 200, [
'Content-Type' => 'application/pdf', 'Content-Type' => 'application/pdf',
'Gotenberg-Trace' => 'spec379-renderer-retry', 'Gotenberg-Trace' => 'spec379-renderer-retry',
]), ]),
@ -506,7 +512,7 @@ function spec379ReadyManagementPdf(ReviewPack $pack): StoredReport
$run = $result['operation_run']; $run = $result['operation_run'];
Http::fake([ Http::fake([
'gotenberg.test/forms/chromium/convert/html' => Http::response('%PDF-1.7 rendered spec379', 200, [ 'gotenberg.test/forms/chromium/convert/html' => Http::response(spec379PdfBytes('rendered spec379 storage failure'), 200, [
'Content-Type' => 'application/pdf', 'Content-Type' => 'application/pdf',
'Gotenberg-Trace' => 'spec379-storage-failure', 'Gotenberg-Trace' => 'spec379-storage-failure',
]), ]),

View File

@ -84,7 +84,7 @@ function spec392RouteCurrentReviewPack(bool $customerSafeReady = true): array
function spec392ReadyManagementPdf(ReviewPack $pack): StoredReport function spec392ReadyManagementPdf(ReviewPack $pack): StoredReport
{ {
$pdfBytes = '%PDF-1.7 Spec392 management report'; $pdfBytes = spec392PdfBytes('Spec392 management report');
$filePath = sprintf('management-reports/%s/spec392-%d.pdf', $pack->tenant->external_id, (int) $pack->getKey()); $filePath = sprintf('management-reports/%s/spec392-%d.pdf', $pack->tenant->external_id, (int) $pack->getKey());
Storage::disk('exports')->put($filePath, $pdfBytes); Storage::disk('exports')->put($filePath, $pdfBytes);
@ -104,6 +104,11 @@ function spec392ReadyManagementPdf(ReviewPack $pack): StoredReport
]); ]);
} }
function spec392PdfBytes(string $label): string
{
return "%PDF-1.7\n1 0 obj\n<< /Type /Catalog /Title ({$label}) >>\nendobj\nstartxref\n0\n%%EOF";
}
it('Spec392 blocks unsafe customer-output downloads but allows authorized internal preview', function (): void { it('Spec392 blocks unsafe customer-output downloads but allows authorized internal preview', function (): void {
[$owner, $tenant, $review, $pack] = spec392RouteCurrentReviewPack(customerSafeReady: false); [$owner, $tenant, $review, $pack] = spec392RouteCurrentReviewPack(customerSafeReady: false);
[$readonly] = createUserWithTenant( [$readonly] = createUserWithTenant(

View File

@ -0,0 +1,283 @@
<?php
declare(strict_types=1);
use App\Jobs\GenerateManagementReportPdfJob;
use App\Models\AuditLog;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ReviewPack;
use App\Models\StoredReport;
use App\Models\User;
use App\Services\Graph\GraphClientInterface;
use App\Services\ReviewPacks\ManagementReportPdfService;
use App\Support\Audit\AuditActionId;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\ReviewPacks\ReportProfileRegistry;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Tests\Support\FailHardGraphClient;
beforeEach(function (): void {
Storage::fake('exports');
Storage::fake('public');
app()->instance(GraphClientInterface::class, new FailHardGraphClient);
});
function spec404ConfigurePdfRenderer(): void
{
config([
'tenantpilot.pdf_renderer' => [
'enabled' => true,
'runtime_validated' => true,
'driver' => 'gotenberg',
'base_url' => 'http://gotenberg.test',
'health_path' => '/health',
'html_route' => '/forms/chromium/convert/html',
'timeout_seconds' => 30,
'connect_timeout_seconds' => 5,
'max_html_bytes' => 1024 * 1024,
'max_asset_bytes' => 512 * 1024,
'max_output_bytes' => 2 * 1024 * 1024,
'correlation_header' => 'Gotenberg-Trace',
'output_filename' => 'tenantpilot-management-report',
],
]);
}
/**
* @return array{0: User, 1: ManagedEnvironment, 2: ReviewPack}
*/
function spec404CurrentReadyPack(): array
{
[$user, $tenant] = createUserWithTenant(role: 'owner', clearCapabilityCaches: true);
$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();
$review = markEnvironmentReviewCustomerSafeReady($review);
$zipContents = spec404ZipContents();
$filePath = sprintf('review-packs/%s/spec404-current.zip', $tenant->external_id);
Storage::disk('exports')->put($filePath, $zipContents);
$pack = ReviewPack::factory()->ready()->create([
'managed_environment_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'environment_review_id' => (int) $review->getKey(),
'evidence_snapshot_id' => (int) $snapshot->getKey(),
'initiated_by_user_id' => (int) $user->getKey(),
'options' => [
'include_pii' => false,
'include_operations' => true,
],
'summary' => [
'governance_package' => [
'executive_summary' => 'Spec404 validates the management report PDF runtime path.',
],
'control_interpretation' => [
'non_certification_disclosure' => 'TenantPilot summarizes available evidence and does not certify compliance.',
],
],
'file_path' => $filePath,
'file_disk' => 'exports',
'file_size' => strlen($zipContents),
'sha256' => hash('sha256', $zipContents),
'expires_at' => now()->addDay(),
]);
$review->forceFill([
'current_export_review_pack_id' => (int) $pack->getKey(),
])->save();
return [$user, $tenant, $pack->fresh(['tenant', 'environmentReview'])];
}
function spec404ZipContents(): string
{
$tempFile = tempnam(sys_get_temp_dir(), 'spec404-review-pack-');
if ($tempFile === false) {
throw new RuntimeException('Failed to allocate Spec404 archive.');
}
try {
$zip = new ZipArchive;
$result = $zip->open($tempFile, ZipArchive::CREATE | ZipArchive::OVERWRITE);
if ($result !== true) {
throw new RuntimeException("Failed to create Spec404 archive: {$result}");
}
$zip->addFromString('metadata.json', json_encode(['fixture' => 'spec404'], JSON_THROW_ON_ERROR));
$zip->addFromString('executive-summary.md', 'Spec404 current review pack.');
$zip->close();
$contents = file_get_contents($tempFile);
if (! is_string($contents) || $contents === '') {
throw new RuntimeException('Spec404 archive is empty.');
}
return $contents;
} finally {
if (file_exists($tempFile)) {
unlink($tempFile);
}
}
}
function spec404PdfBytes(string $label): string
{
return "%PDF-1.7\n1 0 obj\n<< /Type /Catalog /Title ({$label}) >>\nendobj\nstartxref\n0\n%%EOF";
}
function spec404TruncatedPdfBytes(): string
{
return "%PDF-1.7\n1 0 obj\n<< /Type /Catalog >>\nendobj\n";
}
function spec404ManagementPdfWithStoredBytes(
ReviewPack $pack,
string $bytes,
?string $sha256 = null,
?int $fileSize = null,
string $fileDisk = 'exports',
): StoredReport
{
$filePath = sprintf('management-reports/%s/spec404-%d.pdf', $pack->tenant->external_id, (int) $pack->getKey());
Storage::disk($fileDisk)->put($filePath, $bytes);
return StoredReport::factory()->managementReportPdf([
'title' => 'Spec404 Management Report',
])->create([
'workspace_id' => (int) $pack->workspace_id,
'managed_environment_id' => (int) $pack->managed_environment_id,
'source_environment_review_id' => (int) $pack->environment_review_id,
'source_review_pack_id' => (int) $pack->getKey(),
'profile' => ReportProfileRegistry::CUSTOMER_EXECUTIVE,
'file_disk' => $fileDisk,
'file_path' => $filePath,
'file_size' => $fileSize ?? max(1, strlen($bytes)),
'sha256' => $sha256 ?? hash('sha256', $bytes),
'generated_at' => now(),
]);
}
it('Spec404 rejects renderer success responses that are not valid PDF bytes', function (string $rendererBytes): void {
Queue::fake();
spec404ConfigurePdfRenderer();
[$user, , $pack] = spec404CurrentReadyPack();
$result = app(ManagementReportPdfService::class)->startGeneration($pack, $user);
/** @var StoredReport $report */
$report = $result['report'];
/** @var OperationRun $run */
$run = $result['operation_run'];
Http::fake([
'gotenberg.test/forms/chromium/convert/html' => Http::response($rendererBytes, 200, [
'Content-Type' => 'application/pdf',
'Gotenberg-Trace' => 'spec404-corrupt-render',
]),
]);
app()->call([new GenerateManagementReportPdfJob(
storedReportId: (int) $report->getKey(),
operationRunId: (int) $run->getKey(),
), 'handle']);
$report->refresh();
$run->refresh();
expect($report->status)->toBe(StoredReport::STATUS_FAILED)
->and($report->file_path)->toBeNull()
->and($report->sha256)->toBeNull()
->and($run->status)->toBe(OperationRunStatus::Completed->value)
->and($run->outcome)->toBe(OperationRunOutcome::Failed->value)
->and(data_get($run->failure_summary, '0.code'))->toBe('management_report_pdf.invalid_response');
Http::assertSent(fn (Request $request): bool => $request->url() === 'http://gotenberg.test/forms/chromium/convert/html');
})->with([
'non-pdf bytes' => ['not a pdf'],
'truncated pdf bytes' => [spec404TruncatedPdfBytes()],
]);
it('Spec404 refuses signed management PDF downloads when stored bytes are invalid', function (string $bytes, ?string $sha256, ?int $fileSize = null): void {
[$user, , $pack] = spec404CurrentReadyPack();
$report = spec404ManagementPdfWithStoredBytes($pack, $bytes, $sha256, $fileSize);
$url = app(ManagementReportPdfService::class)->generateDownloadUrl($report, [
'source_surface' => 'spec404',
]);
$this->actingAs($user)
->get($url)
->assertNotFound();
expect(AuditLog::query()
->where('action', AuditActionId::ManagementReportPdfDownloaded->value)
->count())->toBe(0);
})->with([
'zero-byte file' => ['', hash('sha256', '')],
'non-pdf file' => ['plain text report', hash('sha256', 'plain text report')],
'truncated pdf file' => [spec404TruncatedPdfBytes(), hash('sha256', spec404TruncatedPdfBytes())],
'sha mismatch' => [spec404PdfBytes('Spec404 valid-looking report'), str_repeat('0', 64)],
'file-size mismatch' => [spec404PdfBytes('Spec404 valid-looking report'), hash('sha256', spec404PdfBytes('Spec404 valid-looking report')), 999],
]);
it('Spec404 refuses ready management PDF downloads stored outside the private exports disk', function (): void {
[$user, , $pack] = spec404CurrentReadyPack();
$pdfBytes = spec404PdfBytes('Spec404 wrong disk report');
$report = spec404ManagementPdfWithStoredBytes(
pack: $pack,
bytes: $pdfBytes,
sha256: hash('sha256', $pdfBytes),
fileSize: strlen($pdfBytes),
fileDisk: 'public',
);
$url = app(ManagementReportPdfService::class)->generateDownloadUrl($report, [
'source_surface' => 'spec404',
]);
expect($report->isReadyManagementPdf())->toBeFalse()
->and(app(ManagementReportPdfService::class)->findReadyReport($pack))->toBeNull();
$this->actingAs($user)
->get($url)
->assertNotFound();
expect(AuditLog::query()
->where('action', AuditActionId::ManagementReportPdfDownloaded->value)
->count())->toBe(0);
});
it('Spec404 does not treat ready metadata as downloadable when the private file is missing', function (): void {
[$user, , $pack] = spec404CurrentReadyPack();
$pdfBytes = spec404PdfBytes('Spec404 missing private file');
$report = spec404ManagementPdfWithStoredBytes($pack, $pdfBytes);
Storage::disk('exports')->delete((string) $report->file_path);
$service = app(ManagementReportPdfService::class);
$url = $service->generateDownloadUrl($report, [
'source_surface' => 'spec404',
]);
expect($report->isReadyManagementPdf())->toBeTrue()
->and($service->findReadyReport($pack))->toBeNull()
->and($service->generationDecision($pack)['state'])->not->toBe('ready');
$this->actingAs($user)
->get($url)
->assertNotFound();
expect(AuditLog::query()
->where('action', AuditActionId::ManagementReportPdfDownloaded->value)
->count())->toBe(0);
});

View File

@ -29,6 +29,11 @@ function configureSpec378PdfRenderer(array $overrides = []): void
]); ]);
} }
function spec378PdfBytes(string $label): string
{
return "%PDF-1.7\n1 0 obj\n<< /Type /Catalog /Title ({$label}) >>\nendobj\nstartxref\n0\n%%EOF";
}
it('keeps the renderer disabled without issuing outbound HTTP', function (): void { it('keeps the renderer disabled without issuing outbound HTTP', function (): void {
configureSpec378PdfRenderer(['enabled' => false]); configureSpec378PdfRenderer(['enabled' => false]);
@ -113,9 +118,10 @@ function configureSpec378PdfRenderer(array $overrides = []): void
it('renders server generated HTML through the configured Gotenberg route', function (): void { it('renders server generated HTML through the configured Gotenberg route', function (): void {
configureSpec378PdfRenderer(); configureSpec378PdfRenderer();
$pdfBytes = spec378PdfBytes('rendered');
Http::fake([ Http::fake([
'gotenberg.test/forms/chromium/convert/html' => Http::response('%PDF-1.7 rendered', 200, [ 'gotenberg.test/forms/chromium/convert/html' => Http::response($pdfBytes, 200, [
'Content-Type' => 'application/pdf', 'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename=management-report.pdf', 'Content-Disposition' => 'attachment; filename=management-report.pdf',
'Gotenberg-Trace' => 'spec378-render', 'Gotenberg-Trace' => 'spec378-render',
@ -133,7 +139,7 @@ function configureSpec378PdfRenderer(array $overrides = []): void
)); ));
expect($result->successful())->toBeTrue() expect($result->successful())->toBeTrue()
->and($result->pdfBytes)->toBe('%PDF-1.7 rendered') ->and($result->pdfBytes)->toBe($pdfBytes)
->and($result->contentType)->toBe('application/pdf') ->and($result->contentType)->toBe('application/pdf')
->and($result->outputFilename)->toBe('management-report.pdf') ->and($result->outputFilename)->toBe('management-report.pdf')
->and($result->correlationId)->toBe('spec378-render'); ->and($result->correlationId)->toBe('spec378-render');
@ -215,7 +221,7 @@ function configureSpec378PdfRenderer(array $overrides = []): void
]); ]);
Http::fake([ Http::fake([
'gotenberg.test/forms/chromium/convert/html' => Http::response('%PDF-1.7 document body', 200, [ 'gotenberg.test/forms/chromium/convert/html' => Http::response(spec378PdfBytes('document body'), 200, [
'Content-Type' => 'application/pdf', 'Content-Type' => 'application/pdf',
'Gotenberg-Trace' => 'spec378-output', 'Gotenberg-Trace' => 'spec378-output',
]), ]),

View File

@ -0,0 +1,94 @@
# Requirements Checklist: Spec 404 - Management Report PDF Staging Validation
**Feature**: `specs/404-management-report-pdf-staging-validation/`
**Review date**: 2026-06-23
**Scope**: Preparation artifact quality only. No application implementation performed.
## Candidate Selection Gate
- [x] The selected candidate was directly provided by the operator as Spec 404.
- [x] The selected candidate matches manual backlog order #1 in `docs/product/spec-candidates.md`.
- [x] The active automatic candidate queue was reviewed and still reports no safe automatic next-best-prep target.
- [x] The candidate aligns with `docs/product/roadmap.md` P1 management-report PDF runtime validation and release hardening.
- [x] The selected package does not reopen completed Specs 378, 379, 400, or 403.
- [x] No existing `specs/404-management-report-pdf-staging-validation/` package existed before preparation.
- [x] The smallest slice is runtime validation and minimal hardening over the existing Spec 379 PDF path.
- [x] Close alternatives are deferred instead of hidden inside this package.
- [x] Candidate Selection Gate result: PASS as a manual operator-promoted candidate.
## Spec Completeness
- [x] Problem statement is clear and product-oriented.
- [x] Business/product value is explicit.
- [x] Primary users/operators are named.
- [x] Scope fields cover routes/surfaces, ownership, RBAC, and leakage checks.
- [x] Functional requirements are testable.
- [x] Non-functional requirements cover security, reliability, performance, deployment, and test governance.
- [x] User stories include independent tests and acceptance scenarios.
- [x] Edge cases are documented.
- [x] Out-of-scope boundaries forbid report redesign, new renderer, new surfaces, lifecycle/retention, JSONB migration, and broad audit scope.
- [x] Success criteria are measurable.
- [x] Assumptions, risks, and open questions are explicit.
## Constitution and Proportionality
- [x] Spec Candidate Check is filled out.
- [x] Approval class is exactly one class: Core Enterprise.
- [x] Score is recorded and above the minimum threshold.
- [x] Proportionality Review is completed.
- [x] No new persisted table/entity/source of truth is approved by default.
- [x] No new renderer framework, report framework, taxonomy, status family, lifecycle framework, or UI framework is approved by default.
- [x] Runtime changes are limited to confirmed in-scope P0/P1 proof defects.
- [x] The spec requires stopping and updating spec/plan before broader architecture or product scope.
## Product Surface Contract
- [x] `docs/product/standards/product-surface-contract.md` is referenced.
- [x] No-legacy posture is recorded.
- [x] Product Surface Impact is completed for existing report/download/customer-output surfaces.
- [x] Page archetypes are identified as Report Page, Receipt Page, and Technical Annex for diagnostics.
- [x] Surface-budget expectations and Technical Annex/deep-link demotion are documented.
- [x] Canonical status vocabulary expectations are documented.
- [x] Product Surface exceptions are `none`.
- [x] Browser proof is required and focused.
- [x] Human Product Sanity is required.
- [x] Implementation-report close-out fields are required.
- [x] Completed historical specs are read-only context and must not be rewritten.
## Plan Completeness
- [x] Plan identifies PHP/Laravel/Filament/Livewire/Pest/PostgreSQL/Sail/Dokploy context.
- [x] Plan names existing runtime code surfaces likely affected if defects are found.
- [x] Plan distinguishes existing Spec 379 implementation from Spec 404 validation scope.
- [x] Plan includes UI/Product Surface, Filament/Livewire/deployment, shared-pattern, OperationRun, provider-boundary, constitution, and test-governance posture.
- [x] Plan defines local, staging-like, and actual staging/Dokploy validation phases.
- [x] Plan requires a PDF Runtime Matrix.
- [x] Plan defines PASS, PASS WITH CONDITIONS, and FAIL gate semantics.
- [x] Plan includes stop conditions.
- [x] Plan does not contradict repository architecture or current code truth.
## Task Completeness
- [x] Tasks are ordered by preparation, inventory, matrix, tests, local proof, hardening, staging validation, browser/human sanity, and close-out.
- [x] Tasks are small and verifiable.
- [x] Tasks require tests before runtime fixes where practical.
- [x] Tasks include explicit lane classification.
- [x] Tasks include Product Surface and Filament output-contract close-out fields.
- [x] Tasks require authorization, cross-workspace, customer-safe, evidence/currentness, failure, storage, queue, and PDF render validation.
- [x] Tasks include actual staging/Dokploy proof or honest missing-proof handling.
- [x] Tasks include non-goals preventing scope creep.
- [x] Tasks include final validation commands and implementation-report completion.
## Open Questions and Readiness
- [x] Actual staging/Dokploy access is recorded as an open implementation-time question, not a preparation blocker.
- [x] PDF validation tool availability is recorded as an implementation-time question, not a preparation blocker.
- [x] No open question blocks starting the implementation loop because missing staging access is handled by gate semantics.
- [x] Spec Readiness Gate result: PASS for implementation preparation.
## Review Outcome
- [x] Review outcome class: `acceptable-special-case` for a bounded runtime validation and release-hardening gate.
- [x] Workflow outcome: `keep`.
- [x] Final note location: future implementation report `specs/404-management-report-pdf-staging-validation/implementation-report.md`.
- [x] No application implementation was performed during preparation.

View File

@ -0,0 +1,200 @@
# Spec 404 Implementation Report - Management Report PDF Staging Validation
## A. Candidate Gate Result
- **Active spec**: `specs/404-management-report-pdf-staging-validation/`
- **Selected slice**: runtime validation and minimal hardening over the existing Spec 379 Management Report PDF path.
- **Branch**: `404-management-report-pdf-staging-validation-session-1782230243`
- **Starting HEAD**: `b0b50885 feat: add evidence anchor runtime closure contract proofs (#474)`
- **Initial dirty state**: untracked `specs/404-management-report-pdf-staging-validation/` only.
- **Initial `git diff --check`**: PASS.
- **Final Candidate Gate Result**: `PASS WITH CONDITIONS`.
- **Production enablement status**: blocked until actual staging/Dokploy validation proves renderer health/conversion, queue worker execution, private storage sharing, APP_URL/assets, signed download authorization, and relevant logs.
## B. Scope Included And Excluded
Included:
- Inventory of existing Spec 379 PDF generation, renderer, storage, signed download, authorization, OperationRun, audit, config, deployment controls, and tests.
- PDF Runtime Matrix for the existing management-report PDF path.
- Focused Spec404 regression tests for confirmed runtime proof gaps.
- Minimal hardening inside existing PDF/report/StoredReport/download seams only.
- Local test and browser proof where available.
Excluded:
- New report templates, report sections, charts, navigation, report center, delivery, scheduling, email, customer portal, AI summary, billing PDF scope, renderer packages, queue architecture, lifecycle/retention, JSONB migrations, completed-spec rewrites, generated PDFs, credentials, private URLs, secrets, customer data, and sensitive logs.
## C. Dirty State And Files
Initial state:
```text
404-management-report-pdf-staging-validation-session-1782230243
b0b50885 feat: add evidence anchor runtime closure contract proofs (#474)
?? specs/404-management-report-pdf-staging-validation/
```
Tracked files changed by implementation:
- `apps/platform/app/Http/Controllers/ManagementReportPdfDownloadController.php`
- `apps/platform/app/Models/StoredReport.php`
- `apps/platform/app/Services/Pdf/PdfRendererClient.php`
- `apps/platform/app/Services/ReviewPacks/ManagementReportPdfService.php`
- `apps/platform/database/factories/StoredReportFactory.php`
- `apps/platform/tests/Browser/Spec379ManagementReportPdfSmokeTest.php`
- `apps/platform/tests/Feature/ReviewPack/Spec379ManagementReportPdfTest.php`
- `apps/platform/tests/Feature/ReviewPack/Spec392CustomerOutputRouteGateTest.php`
- `apps/platform/tests/Unit/Pdf/Spec378PdfRenderingGatewayTest.php`
Untracked files changed by implementation:
- `apps/platform/app/Support/Pdf/PdfByteValidator.php`
- `apps/platform/tests/Feature/ReviewPack/Spec404ManagementReportPdfRuntimeValidationTest.php`
- `specs/404-management-report-pdf-staging-validation/implementation-report.md`
- `specs/404-management-report-pdf-staging-validation/spec.md`
- `specs/404-management-report-pdf-staging-validation/plan.md`
- `specs/404-management-report-pdf-staging-validation/tasks.md`
- `specs/404-management-report-pdf-staging-validation/checklists/requirements.md`
No completed historical spec rewrite assertion: confirmed. Specs 378, 379, 400, and 403 were read as context only and not edited.
Runtime UI files changed: no. Runtime behavior changed only in backend PDF renderer/download validation paths. Existing Review Pack, report, and download surfaces were smoke-tested through the focused browser path; no page-report or route-inventory update was required because no route, page, action, navigation entry, label, modal, table, or visible state vocabulary was added or redesigned.
Staging/Dokploy access check: no repo-contained Dokploy target, deployment shell, staging service-network command, or redacted operator output is available in this workspace. Existing Spec 380 staging evidence also records the same external blocker. Therefore the remaining Staging/Dokploy rows are an external release-gate condition, not a repo-local implementation defect.
## D. PDF Runtime Matrix
| Row | Runtime mechanism | Proof performed | Result | Risk | Follow-up |
|---|---|---|---|---|---|
| Generation | `ManagementReportPdfService::startGeneration()` creates/reuses `OperationRun`, creates `StoredReport`, dispatches `GenerateManagementReportPdfJob` | Code inventory, Spec379 tests, Spec404 corrupt-render regression | PASS WITH CONDITIONS | P1 | Actual staging worker proof still required |
| Renderer dependency | `PdfRenderingGateway` and `PdfRendererClient` call internal Gotenberg route and reject structurally invalid PDF bytes | Code inventory, local gateway health 200, local render 18,992 bytes, Spec404 invalid-response and truncated-response tests | PASS WITH CONDITIONS | P1 | Actual Dokploy renderer health/conversion proof still required |
| Asset/CSS handling | Server-generated HTML with inline CSS, no external assets by default | Local render with inline CSS and no external assets | PASS WITH CONDITIONS | P2 | Staging/Dokploy asset and APP_URL proof still required |
| Storage/write permissions | Job writes to private `exports` disk and stores file metadata | Existing Spec379 storage failure test; Spec404 signed-route byte validation tests | PASS WITH CONDITIONS | P1 | Staging worker/web shared volume proof still required |
| Queue/worker | OperationRun dispatches queued job | Existing Spec379 queued generation tests; local queue container present | PASS WITH CONDITIONS | P1 | Actual staging worker process proof still required |
| Download authorization | Signed route re-checks source pack, actor entitlement, capability, customer-output gate, file existence, actual bytes, structural PDF markers, file size, and SHA-256 | Existing Spec379/392 coverage plus Spec404 invalid stored-bytes tests | PASS WITH CONDITIONS | P1 | Staging authorized/wrong-scope proof still required |
| Workspace/environment scope | `StoredReport`, source pack, source review, tenant, and actor are re-resolved on download | Existing Spec379 scoped authorization tests | PASS WITH CONDITIONS | P1 | Staging wrong-scope proof still required |
| Customer-safe content | Payload builder sanitizes source strings and applies customer-executive disclosure gate | Existing Spec379 content tests; local PDF string scan found expected title and no forbidden local/secret strings | PASS WITH CONDITIONS | P1 | Full staging/customer fixture inspection still required |
| Evidence/currentness labels | Payload uses released current Review Pack and Spec403 evidence/currentness closure | Code inventory and Spec403 context | PASS WITH CONDITIONS | P1 | Staging generated report inspection still required |
| Failure handling | Renderer/storage/disclosure failures set failed/blocked state and OperationRun terminal outcome; missing/corrupt ready artifacts are hidden from ready state | Existing Spec379 tests plus Spec404 invalid renderer/stored-byte/missing-file tests | PASS WITH CONDITIONS | P1 | Actual staging logs still required |
| Staging/Dokploy validation | Dokploy/staging env, renderer health, queue worker, private storage, APP_URL/assets, logs | Not accessible from local workspace | MISSING PROOF | P1 | Keep final result no stronger than PASS WITH CONDITIONS |
## E. Runtime Changes Made
- `PdfByteValidator` centralizes the bounded local PDF byte sanity check: body starts with `%PDF-`, includes `startxref`, and ends with `%%EOF` after trailing whitespace is ignored.
- `PdfRendererClient` now rejects successful HTTP responses whose body is not structurally valid PDF bytes, even if the response advertises `Content-Type: application/pdf`.
- `StoredReport::isReadyManagementPdf()` now requires positive `file_size` and a stored SHA-256.
- `StoredReport::isReadyManagementPdf()` and `ManagementReportPdfService::findReadyReport()` now require the private `exports` disk and ignore ready reports stored on any other disk.
- `ManagementReportPdfService::findReadyReport()` now verifies the private artifact exists, is readable, matches persisted file size, is structurally valid PDF bytes, and matches the stored SHA-256 before surfacing a ready report to the owner surface.
- `ManagementReportPdfDownloadController` now verifies the private file exists, has positive stored size, matches the stored file size, is structurally valid PDF bytes, and matches the stored SHA-256 before auditing and streaming.
These changes are bounded to existing Spec379 PDF/report/download seams and do not add new product scope, routes, storage, renderer infrastructure, or UI surfaces.
## F. Tests Added Or Updated
- Added `apps/platform/tests/Feature/ReviewPack/Spec404ManagementReportPdfRuntimeValidationTest.php`.
- Proves renderer "success" with non-PDF or truncated PDF bytes fails closed as `management_report_pdf.invalid_response`.
- Proves signed download returns 404 and writes no download audit for zero-byte, non-PDF, truncated-PDF, file-size-mismatched, and SHA-mismatched stored artifacts.
- Proves otherwise valid management PDF records stored outside the private `exports` disk are not treated as ready and return 404 without download audit.
- Proves ready metadata without a readable private file is not surfaced as a ready report and returns 404 without download audit.
- Updated existing Spec378/379/392/browser PDF test fixtures to use structurally valid minimal PDF bytes that satisfy the new runtime sanity check.
## G. Browser Proof
- **Result**: PASS.
- **Command**: `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec379ManagementReportPdfSmokeTest.php --compact`
- **Observed**: 1 passed, 18 assertions.
- **Path**: authorized operator opens Review Pack detail, observes Management PDF unavailable when runtime validation is blocked, then observes Download management PDF when a ready artifact exists.
- **Console/runtime/network**: test asserted no JavaScript errors and no console logs.
- **Limitation**: focused browser proof reused the existing Spec379 browser smoke and did not exercise actual Dokploy/staging.
Required focused path:
```text
authorized operator -> Review Pack detail -> management PDF state -> download/open PDF
```
Unauthorized/wrong-workspace proof: covered locally by existing focused feature tests for signed download and customer-output gates; actual staging wrong-scope proof remains pending under T050.
## H. PDF Validation Proof
- **Local Gotenberg health through Laravel gateway**: PASS, HTTP 200.
- **Local render through Laravel gateway**: PASS, generated 18,992 bytes, SHA-256 `8c442e485ee47f6617cecb5da92ac05df69e46220c98b04fa2100cb5ceb7fc86`, bytes started with `%PDF-`.
- **Host-side inspection**: `file` reported `PDF document, version 1.4, 1 pages`; `xxd` confirmed the `%PDF-1.4` header and expected `Spec404 Management Report Visual Proof` title metadata.
- **First-page visual proof**: PASS. macOS Quick Look rendered a 927x1200 first-page PNG from the generated PDF. Visual inspection confirmed readable report text, not blank, not raw HTML, and no obvious overlap.
- **Forbidden string scan**: no `SQLSTATE`, bearer/access/refresh token, client secret, private key, password, localhost, local/container path, `gotenberg`, `APP_URL`, `TENANTPILOT`, or `http://` string was found. The expected title was present.
- **Artifact handling**: temporary generated proof file was deleted and is not present in the worktree.
- **Limitation**: Poppler/qpdf/mutool/gs were not installed, so the local visual proof used macOS Quick Look. Actual staging/Dokploy generated-PDF validation remains a release condition.
## I. Customer-Safe Boundary Proof
- Existing Spec379 payload/content tests prove secret-shaped source strings are redacted from stored payloads.
- Spec404 local PDF string scan found expected title metadata and no forbidden local/secret strings.
- Customer-output gate download blocking remains covered by Spec392.
- No raw provider payloads, source keys, stack traces, local paths, renderer URLs, or secrets were added to the PDF path or report.
## J. Remaining Findings
Confirmed in-scope findings fixed:
- **P1**: Renderer success accepted non-PDF bytes when the response advertised `Content-Type: application/pdf`. Fixed and covered by Spec404.
- **P1**: Renderer/download paths accepted truncated PDF-shaped bytes with a valid `%PDF-` prefix. Fixed and covered by Spec404.
- **P1**: Ready stored report download path checked file existence but not actual stored byte validity before streaming. Fixed and covered by Spec404.
- **P2**: Owner surface could treat ready metadata as a downloadable ready report even when the private file was missing or unreadable. Fixed and covered by Spec404.
- **P2**: Ready stored report download path did not reject artifacts whose persisted `file_size` no longer matched the private file bytes. Fixed and covered by Spec404.
- **P2**: Ready stored report path did not explicitly reject otherwise valid management PDFs stored outside the private `exports` disk. Fixed and covered by Spec404.
Remaining condition:
- **P1 MISSING PROOF**: Actual staging/Dokploy runtime proof is unavailable from this workspace. This blocks a `PASS` result and production-style enablement.
## K. Deferred Items
- Actual Dokploy/staging validation if credentials, logs, deployed environment, and non-secret fixture are not available.
- Governance artifact lifecycle/retention runtime.
- JSON-to-JSONB data-layer hardening.
- Report redesign, delivery center, scheduling/email, customer portal, technical/auditor reports.
- Full browser/UX/runtime audit.
## L. Filament v5 Output Contract Close-Out
- **Livewire v4 compliance**: Livewire 4.1.4 confirmed by Laravel Boost; no Livewire v3 APIs planned.
- **Panel provider registration location**: `apps/platform/bootstrap/providers.php`; no panel provider change planned.
- **Global search posture**: `ReviewPackResource` and `StoredReportResource` remain globally non-searchable for this spec; no global search enablement planned.
- **Destructive/high-impact actions**: `Generate management PDF` remains high-impact artifact creation using `Action::make(...)->action(...)`, `->requiresConfirmation()`, server-side `REVIEW_PACK_MANAGE`, OperationRun, audit, and tests. Download remains signed/server-authorized and audited.
- **Asset strategy**: no new Filament assets. Existing deployment process should still include `cd apps/platform && php artisan filament:assets` when Filament assets are published/registered by deployment.
- **Deployment impact**: env vars and runtime validation remain `TENANTPILOT_PDF_RENDERER_*`, `APP_URL`, queue worker, private `exports` storage, and internal Gotenberg service. No migrations, scheduler changes, new assets, or package changes planned.
## M. Validation Commands
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec404`
- Result: PASS, 9 tests, 33 assertions.
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec379`
- Result: PASS, 21 tests, 173 assertions.
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec378`
- Result: PASS, 11 tests, 45 assertions.
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/Spec392CustomerOutputRouteGateTest.php`
- Result: PASS, 19 tests, 134 assertions.
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec379ManagementReportPdfSmokeTest.php --compact`
- Result: PASS, 1 test, 18 assertions.
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
- Result: PASS, 0 files changed.
- `git diff --check`
- Result: PASS.
- Local renderer proof:
- Gateway health: PASS, HTTP 200.
- Gateway render: PASS, 18,992 bytes, PDF header present, host `file` recognized 1-page PDF, Quick Look rendered a readable first-page image.
## N. Human Product Sanity
- Purpose clear: PASS for the existing Management Report PDF flow.
- One dominant next action: PASS. Review Pack detail shows unavailable/generate/open-operation/download depending on state; no duplicate new surface was introduced.
- Technical detail demotion: PASS. OperationRun/log/renderer internals remain outside customer PDF and default download path.
- Canonical labels: PASS WITH CONDITIONS. Existing UI states remain Product Surface aligned; staging PDF content must still be inspected before production enablement.
- Visible complexity: neutral. No new UI surface, route, navigation, action family, or product vocabulary was added.
- Trust result: PASS WITH CONDITIONS. Local hardening improves trust; actual staging/Dokploy proof remains required.
## O. Recommended Next Action
- Provide actual staging/Dokploy access or a production-equivalent environment and repeat the matrix rows for renderer health/conversion, queue worker, shared private storage, APP_URL/assets, generated PDF validation, signed download authorization, and logs.
- Keep `TENANTPILOT_PDF_RENDERER_RUNTIME_VALIDATED=false` outside the validation environment until that proof is complete.

View File

@ -0,0 +1,320 @@
# Implementation Plan: Spec 404 - Management Report PDF Staging Validation
**Branch**: `404-management-report-pdf-staging-validation` | **Date**: 2026-06-23 | **Spec**: `specs/404-management-report-pdf-staging-validation/spec.md`
**Input**: Feature specification from `specs/404-management-report-pdf-staging-validation/spec.md`
## Summary
Validate and, only if needed, minimally harden the existing Spec 379 Management Report PDF path under local, staging-like, and actual staging/Dokploy conditions. The implementation must prove renderer availability, queue/worker execution, private storage, signed download authorization, workspace/environment scoping, customer-safe PDF content, evidence/currentness truth, and honest failure states before production-style enablement. It must not redesign reports, add report templates, add navigation, replace the renderer, or introduce lifecycle/retention scope.
## 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, PostgreSQL, Laravel Sail, approved Gotenberg 8 Chromium internal renderer from Spec 378
**Storage**: PostgreSQL plus private Laravel `exports` disk at `storage/app/private/exports` for generated PDFs
**Testing**: Pest Unit, Feature, Filament/Livewire action tests, focused Browser/content smoke, PDF parser/render validation where available
**Validation Lanes**: fast-feedback, confidence, browser, pgsql only if schema/index behavior changes
**Target Platform**: Laravel Sail locally; Dokploy container deployment for staging/production
**Project Type**: Laravel monolith under `apps/platform`
**Performance Goals**: PDF generation remains queued and observable; no live provider calls during render/generation/download; failed renderer/storage paths fail closed
**Constraints**: no second renderer; no Graph calls during render/generation/download; no public PDF storage; no raw provider payloads/secrets/local paths in PDF/audit/log output; no production enablement without staging/Dokploy proof or explicit condition
**Scale/Scope**: one existing on-demand `customer_executive` Management Report PDF flow over current ready Review Pack/rendered-report truth
**Staging-Like / Production-Equivalent Criteria**: Local staging-like proof must exercise production-relevant runtime boundaries: separate app and queue worker execution, internal-only pinned Gotenberg access from the app/worker network, private `exports` disk write/read behavior across worker and web contexts, production-like `APP_URL`/base URL and asset behavior, validation-only runtime flags, no public renderer exposure, and no committed secrets, private URLs, generated customer data, or sensitive logs. A non-Dokploy environment counts as production-equivalent only when the implementation report names the environment and proves the same criteria.
## Repo Truth Map
Existing management-report PDF runtime from Specs 378-379:
- `docker-compose.yml` includes pinned internal `gotenberg/gotenberg:8.34.0-chromium`.
- `apps/platform/config/tenantpilot.php` defines `tenantpilot.pdf_renderer.*` and OperationRun lifecycle coverage for `report.management.generate`.
- `apps/platform/.env.example` defaults `TENANTPILOT_PDF_RENDERER_RUNTIME_VALIDATED=false`.
- `docs/deployment-checklist.md` documents Gotenberg/Dokploy controls.
- `docs/package-governance.md` approves Gotenberg with controls.
- `apps/platform/app/Services/Pdf/PdfRenderingGateway.php`
- `apps/platform/app/Services/Pdf/PdfRendererClient.php`
- `apps/platform/app/Services/ReviewPacks/ManagementReportPdfService.php`
- `apps/platform/app/Services/ReviewPacks/ManagementReportPdfRenderer.php`
- `apps/platform/app/Support/ReviewPacks/ManagementReportPdfPayloadBuilder.php`
- `apps/platform/app/Support/ReviewPacks/ManagementReportPdfRuntimeGate.php`
- `apps/platform/app/Jobs/GenerateManagementReportPdfJob.php`
- `apps/platform/app/Http/Controllers/ManagementReportPdfDownloadController.php`
- `apps/platform/routes/web.php` signed route `admin.management-report-pdfs.download`
- `apps/platform/app/Models/StoredReport.php`
- `apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php`
- `apps/platform/tests/Unit/Pdf/Spec378PdfRenderingGatewayTest.php`
- `apps/platform/tests/Unit/Support/ReviewPacks/Spec379ManagementReportPdfReadinessTest.php`
- `apps/platform/tests/Feature/ReviewPack/Spec379ManagementReportPdfTest.php`
- `apps/platform/tests/Browser/Spec379ManagementReportPdfSmokeTest.php`
- `specs/379-management-report-pdf-runtime/artifacts/runtime-validation.md`
- `specs/379-management-report-pdf-runtime/artifacts/storage-operationrun-decision.md`
Important repo-truth adjustment:
- Spec 379 implemented and locally validated the Management Report PDF flow but explicitly recorded actual staging/Dokploy runtime validation as unavailable and left the runtime gate blocked by default.
- Spec 404 must not redo Spec 379 generation work. It must validate or minimally harden the already implemented path.
## UI / Surface Guardrail Plan
- **Guardrail scope**: existing report/receipt/download/action surfaces under validation and possible minimal defect-driven hardening.
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**:
- Review Pack detail management-PDF action state.
- Signed PDF download route.
- Generated PDF customer report output.
- StoredReport artifact state.
- OperationRun proof link and terminal state.
- **No-impact class, if applicable**: N/A.
- **Native vs custom classification summary**: Review Pack owner actions stay native Filament; PDF output remains the existing report artifact output; OperationRun remains native Monitoring/Operations context.
- **Shared-family relevance**: report viewer, artifact download, status/readiness messaging, OperationRun UX, audit.
- **State layers in scope**: detail header/action state, modal confirmation, artifact state, OperationRun state, signed route, private storage, runtime/deployment validation state.
- **Audience modes in scope**: customer/read-only PDF content, operator-MSP generation/download flow, support-platform diagnostics through existing OperationRun/log surfaces.
- **Decision/diagnostic/raw hierarchy plan**: PDF is decision-first; OperationRun/logs are diagnostics; raw/support evidence stays out of default customer output.
- **Raw/support gating plan**: raw JSON, provider payloads, signed URL internals, secrets, stack traces, SQL errors, local/container paths, and storage paths are forbidden in PDF output and customer-readable default state.
- **One-primary-action / duplicate-truth control**: owner surface remains state-dependent: generate, open operation, or download. No second report center or duplicate readiness model.
- **Handling modes by drift class or surface**: hard stop for wrong-scope download, public artifact storage, raw leakage, blank/corrupt ready PDF, false evidence/currentness labels, second renderer, or production enablement without staging proof.
- **Repository-signal treatment**: update UI coverage artifacts only if runtime UI/customer output materially changes; otherwise record a checked no-update rationale in the implementation report.
- **Special surface test profiles**: high-impact artifact action, report output, signed download, OperationRun diagnostics.
- **Required tests or manual smoke**: focused Unit/Feature/Filament/Browser/PDF-validation proof.
- **Exception path and spread control**: none new. Existing bounded report/PDF output exception remains from earlier specs.
- **Active feature PR close-out entry**: Product Surface / Runtime Validation / Smoke Coverage / Deployment Gate.
- **UI/Productization coverage decision**: existing surfaces changed/validated, no new route/nav/page by default.
- **Coverage artifacts to update**: `ui-042-review-pack-detail.md`, `ui-048-stored-report-detail.md`, `ui-099-rendered-review-report.md`, route inventory, or design coverage matrix only if material runtime surface behavior changes.
- **No-impact rationale**: N/A.
- **Navigation / Filament provider-panel handling**: no navigation or panel-provider change planned. Laravel 12 panel providers remain in `apps/platform/bootstrap/providers.php`.
- **Screenshot or page-report need**: screenshot/browser proof required for focused report action/download state; page-report updates only for material UI changes.
## Product Surface Contract Plan
- **Product Surface Contract reference**: `docs/product/standards/product-surface-contract.md`.
- **No-legacy posture**: clean runtime validation gate; no local-only production claims or compatibility shims.
- **Page archetype and surface budget plan**: Report Page for PDF, Receipt Page for artifact state, Technical Annex for OperationRun/log diagnostics. Default product PDF must stay customer-safe and avoid raw technical identifiers.
- **Technical Annex and deep-link demotion plan**: OperationRun links, raw evidence, source keys, payloads, detector/fingerprint detail, renderer internals, file paths, and logs remain secondary or technical/audit only.
- **Canonical status vocabulary plan**: map product-facing labels to Ready, Needs attention, Blocked, Running, Failed, Unknown, Historical, or Superseded. Internal `queued`/`generating`/reason codes stay implementation detail unless presented through canonical labels.
- **Product Surface exceptions**: none.
- **Browser verification plan**: focused browser proof for Review Pack detail generate/blocked/ready/download state plus unauthorized/wrong-workspace blocked path where practical.
- **Human Product Sanity plan**: 5 to 15 minute review of generated PDF and owner action state, recorded in `implementation-report.md`.
- **Visible complexity outcome target**: neutral or decreased.
- **Implementation report target**: `specs/404-management-report-pdf-staging-validation/implementation-report.md`.
## Filament / Livewire / Deployment Posture
- **Livewire v4 compliance**: Livewire 4.1.4 confirmed; no Livewire v3 APIs allowed.
- **Panel provider registration location**: `apps/platform/bootstrap/providers.php`; no panel provider change planned.
- **Global search posture**: `StoredReportResource` remains non-globally-searchable unless a future spec changes it with safe View/Edit semantics. Spec 404 does not enable global search.
- **Destructive/high-impact action posture**: existing `Generate management PDF` action remains high-impact artifact creation and must use `Action::make(...)->action(...)`, `->requiresConfirmation()`, server-side `REVIEW_PACK_MANAGE`, OperationRun, audit, and tests. Download remains signed/server-authorized and audited.
- **Asset strategy**: no new Filament assets planned. Existing deployment still runs `cd apps/platform && php artisan filament:assets`; PDF output must not depend on unpublished local assets.
- **Testing plan**: existing Spec 378/379 tests plus focused Spec 404 test gaps for staging/runtime proof, PDF parser/render validation, authorization, customer-safe content, failure states, and browser/download flow.
- **Deployment impact**:
- env vars: `TENANTPILOT_PDF_RENDERER_ENABLED`, `TENANTPILOT_PDF_RENDERER_RUNTIME_VALIDATED`, `TENANTPILOT_PDF_RENDERER_BASE_URL`, renderer timeout/limit vars, `APP_URL`, queue/storage config.
- migrations: none by default.
- queues: worker must run the report job path and reach internal Gotenberg.
- scheduler: no new scheduler requirement, but queue/operation reconciliation may be relevant to proof.
- storage: private `exports` disk/volume must be writable by worker and readable by web process.
- assets: no new assets; verify existing CSS/logo/font assumptions in PDF output.
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes.
- **Systems touched**: PDF gateway, Review Pack rendered report, report profile/disclosure, StoredReport artifact storage, OperationRun, audit, customer-output gate, private storage, deployment configuration.
- **Shared abstractions reused**: `PdfRenderingGateway`, `PdfRendererClient`, `ManagementReportPdfService`, `ManagementReportPdfRenderer`, `ManagementReportPdfPayloadBuilder`, `ManagementReportPdfRuntimeGate`, `ReportProfileRegistry`, `ReportDisclosurePolicy`, `ReviewPackService`, `CustomerOutputGate`, `OperationRunService`, `OperationRunLinks`, `OperationUxPresenter`, `WorkspaceAuditLogger`.
- **New abstraction introduced? why?**: none by default. Add no new base abstraction unless a confirmed P0/P1 defect cannot be fixed safely through existing seams and the spec/plan are updated first.
- **Why the existing abstraction was sufficient or insufficient**: sufficient for generation. Insufficient only as deployed-runtime evidence until validation is performed.
- **Bounded deviation / spread control**: any hardening stays in existing PDF/report/StoredReport/ReviewPack/OperationRun/audit paths and is justified by a specific matrix defect.
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: yes through existing `report.management.generate`.
- **Central contract reused**: `OperationRunService`, `OperationRunLinks`, `OperationUxPresenter`, `OperationRunCompleted`, Monitoring -> Operations detail route.
- **Delegated UX behaviors**: queued toast, run link, artifact link, run-enqueued browser event, blocked/start-failure messaging, tenant/workspace-safe URL resolution, and terminal notification remain existing shared-path responsibilities.
- **Surface-owned behavior kept local**: report source readiness, runtime gate reason, customer-output gate reason, artifact download state.
- **Queued DB-notification policy**: no new opt-in unless existing Spec 379 path already does so and evidence is recorded.
- **Terminal notification path**: central lifecycle mechanism.
- **Exception path**: none.
## Provider Boundary & Portability Fit
- **Shared provider/platform boundary touched?**: yes, indirectly through report/evidence content.
- **Provider-owned seams**: existing provider-derived summaries already allowed by the Review Pack/customer-safe report profile.
- **Platform-core seams**: generated artifact truth, evidence basis/currentness labels, operation proof, runtime validation proof, audit metadata.
- **Neutral platform terms / contracts preserved**: management report, evidence basis, generated artifact, managed environment, workspace, runtime validation, operation.
- **Retained provider-specific semantics and why**: existing disclosure-safe Microsoft/provider summaries only; no new Graph/provider calls.
- **Bounded extraction or follow-up path**: follow-up-spec if validation uncovers broader provider/report semantic drift.
## Constitution Check
*GATE: Must pass before implementation. Re-check after runtime validation and before final report.*
- Inventory-first: PASS. Validation consumes existing stored review/report truth; no live provider state is introduced.
- Read/write separation: PASS with controls. Generation is explicit TenantPilot artifact creation with confirmation, audit, OperationRun, and tests.
- Graph contract path: PASS. No Graph calls during render/generation/download.
- Deterministic capabilities: REQUIRED. Generation/download continue to use canonical capability registry.
- RBAC-UX: REQUIRED. Wrong workspace/environment/non-member remains 404; scoped missing capability remains 403.
- Workspace/tenant isolation: REQUIRED. Source Review Pack, StoredReport, OperationRun, and download bytes re-resolve scope.
- Destructive/high-impact actions: REQUIRED. Generate PDF remains confirmed, authorized, audited, tested.
- Global search: PASS. No global-search change.
- Run observability: REQUIRED. Generation remains OperationRun-backed.
- OperationRun start UX: REQUIRED. Use central start/link/notification paths.
- Ops-UX lifecycle: REQUIRED. Status/outcome transitions through `OperationRunService`.
- Data minimization: REQUIRED. No secrets/raw payloads/paths/URLs in PDF, audit metadata, logs, or committed artifacts.
- Test governance: REQUIRED. Actual test purpose and lanes must be recorded.
- Proportionality: PASS. Runtime proof solves a current release blocker.
- No premature abstraction: PASS. Existing seams first.
- Persisted truth: PASS. No new persisted truth by default.
- Behavioral state: PASS only for defect-driven behavior-changing states/reason codes after spec/plan update.
- UI semantics: PASS. Direct mapping from report/source truth to customer output; no new status framework.
- Shared pattern first: PASS. Existing renderer/report/operation/audit paths first.
- Provider boundary: PASS. No new provider runtime semantics.
- Product Surface Contract: REQUIRED. Generated PDF/customer output and download behavior must satisfy the contract.
## Test Governance Check
- **Test purpose / classification by changed surface**:
- Unit: runtime gate decision mapping, renderer/gateway behavior, PDF payload/content sanitization.
- Feature: generation state, private storage, signed download authorization, cross-workspace denial, missing/zero-byte/failure state, audit, OperationRun proof.
- Filament/Livewire: Review Pack detail action visible/disabled/confirmed/active/ready states when runtime UI behavior changes.
- Browser: focused Review Pack detail and PDF download/open smoke.
- PDF validation: parser/text extraction/render-to-image or equivalent tooling where available.
- **Affected validation lanes**: fast-feedback, confidence, browser. PostgreSQL lane only if schema/indexes or DB-specific constraints are touched.
- **Why this lane mix is narrowest sufficient proof**: each lane proves one runtime trust boundary; no broad surface discovery is required.
- **Narrowest proving commands**:
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec404`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec379`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec378`
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec379ManagementReportPdfSmokeTest.php --compact` or a new focused Spec404 browser smoke if needed
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
- `git diff --check`
- **Fixture/helper/factory/seed/context cost risks**: use explicit PDF/report fixtures only; do not widen global seeds or default full workspace/provider context.
- **Expensive defaults or shared helper growth introduced?**: none planned.
- **Heavy-family additions, promotions, or visibility changes**: no heavy-governance family planned; browser proof is feature-local.
- **Budget/baseline/trend follow-up**: record report generation/browser runtime if materially slow.
- **Review-stop questions**: lane fit, staging proof, file/output safety, download scope, OperationRun truth, artifact readiness truth, no completed-spec rewrite.
- **Escalation path**: `document-in-feature` for contained runtime conditions; `follow-up-spec` for lifecycle/retention, data-layer hardening, report redesign, second renderer, or broader deployment architecture.
- **Active feature PR close-out entry**: Product Surface / Runtime Validation / Smoke Coverage / Deployment Gate.
## Project Structure
### Documentation (this feature)
```text
specs/404-management-report-pdf-staging-validation/
├── spec.md
├── plan.md
├── tasks.md
├── checklists/
│ └── requirements.md
└── implementation-report.md # created during implementation
```
### Source Code (implementation only if validation finds in-scope defects)
```text
apps/platform/app/Services/ReviewPacks/ManagementReportPdfService.php
apps/platform/app/Services/ReviewPacks/ManagementReportPdfRenderer.php
apps/platform/app/Support/ReviewPacks/ManagementReportPdfPayloadBuilder.php
apps/platform/app/Support/ReviewPacks/ManagementReportPdfRuntimeGate.php
apps/platform/app/Jobs/GenerateManagementReportPdfJob.php
apps/platform/app/Http/Controllers/ManagementReportPdfDownloadController.php
apps/platform/app/Models/StoredReport.php
apps/platform/app/Filament/Resources/ReviewPackResource/
apps/platform/routes/web.php
apps/platform/config/tenantpilot.php
apps/platform/config/filesystems.php
apps/platform/tests/Unit/
apps/platform/tests/Feature/
apps/platform/tests/Browser/
docs/deployment-checklist.md
```
**Structure Decision**: Keep all runtime work in existing PDF/report/ReviewPack/StoredReport/OperationRun seams. No new base folders, dependencies, report center, renderer framework, or lifecycle package.
## Implementation Phases
### Phase 0 - Preparation and Dirty State
Re-read active spec artifacts, completed context specs, product surface contract, and current code. Record branch, HEAD, dirty state, untracked files, and `git diff --check` before implementation.
### Phase 1 - Runtime Path Inventory
Map the existing path end to end:
```text
Review Pack detail action
-> ManagementReportPdfService::startGeneration()
-> ManagementReportPdfRuntimeGate
-> OperationRunService / report.management.generate
-> GenerateManagementReportPdfJob
-> ManagementReportPdfPayloadBuilder
-> ManagementReportPdfRenderer
-> PdfRenderingGateway / Gotenberg
-> private exports disk
-> StoredReport ready/failed state
-> signed ManagementReportPdfDownloadController
-> audit / OperationRun / customer-output gate
```
Record missing proof without editing runtime code.
### Phase 2 - Matrix and Existing Test Proof
Create `implementation-report.md` with the required final report skeleton and PDF Runtime Matrix. Run existing Spec 378/379 targeted tests and browser smoke to establish the local baseline. Identify gaps that Spec 404 must add or manually validate.
### Phase 3 - Local and Staging-Like PDF Proof
Generate or reuse one non-secret Management Report PDF through the existing path in local/staging-like runtime. Validate PDF bytes, text extraction, rendered first page, customer-safe content, local path/localhost absence, evidence/currentness labels, artifact metadata, OperationRun state, audit events, signed download behavior, and absence of live provider calls or external page-render resource fetches beyond the approved internal renderer/assets path.
### Phase 4 - Minimal Runtime Hardening
Apply only confirmed in-scope fixes:
- block ready state for missing/zero-byte/corrupt PDF,
- tighten download authorization/scope,
- sanitize leaked content,
- fix local-only asset/path assumptions,
- improve failed/blocked state mapping,
- fix storage/worker assumptions,
- add focused tests for proven gaps.
Stop and update spec/plan before any new renderer, report template, persisted table, lifecycle framework, queue architecture, or broad UI work.
### Phase 5 - Actual Staging/Dokploy Validation
When accessible, validate deployed staging/Dokploy:
- internal Gotenberg service and `/health`,
- renderer conversion path,
- env vars and runtime gate,
- queue worker execution,
- private storage volume read/write,
- APP_URL/base URL behavior,
- generated PDF validation,
- signed download authorization,
- logs for renderer/storage/job failures,
- customer-safe content and evidence/currentness labels.
If inaccessible, record exact missing proof and keep final result no stronger than `PASS WITH CONDITIONS`.
### Phase 6 - Product Surface, Browser, and Human Sanity Close-Out
Run focused browser proof, complete Human Product Sanity, record Product Surface exceptions (`none` unless found), visible complexity outcome, Livewire v4 posture, provider registration posture, global search posture, high-impact action posture, asset strategy, deployment impact, commands/results, unrelated failures, and next recommended action.
## Gate Evaluation
### PASS
Use only if no P0/P1 runtime proof gaps remain, actual staging/Dokploy or equivalent production-like validation passed, generated PDF is valid/renderable, download authorization/customer-safe content/evidence labels are proven, targeted tests and browser proof pass, and production enablement is no longer blocked by PDF runtime uncertainty.
### PASS WITH CONDITIONS
Use if no P0 findings remain, local and staging-like proof are strong, critical authorization/customer-safe/evidence correctness is proven, and the remaining staging/Dokploy condition is explicit and still blocks broad production/customer enablement.
### FAIL
Use if any P0 remains, generated PDF is blank/unreadable, unauthorized/cross-workspace download is possible, customer-safe PDF leaks internal data, failed generation is marked ready, evidence/currentness claims are false, renderer/storage/queue path cannot be proven, or fixes require unresolved broader product/architecture decisions.
## Stop Conditions
- Actual staging access is required for PASS and cannot be obtained.
- Validation finds a P0/P1 defect that needs new report templates, new renderer infrastructure, a new table/entity, lifecycle framework, or broad deployment architecture.
- Runtime changes would require modifying completed Specs 378, 379, 400, or 403.
- Generated proof would require committing private PDFs, credentials, URLs, or customer data.
- The worktree contains unrelated uncommitted changes before implementation begins.

View File

@ -0,0 +1,378 @@
# Feature Specification: Spec 404 - Management Report PDF Staging Validation
**Feature Branch**: `404-management-report-pdf-staging-validation`
**Created**: 2026-06-23
**Status**: Draft / Ready for implementation preparation review
**Type**: Runtime validation / deployment proof / report-output hardening spec
**Input**: User-provided "Spec 404 - Management Report PDF Staging Validation" draft from `/Users/ahmeddarrazi/.codex/attachments/3ebacb54-68bd-4099-b1b8-80ea9f0b5131/pasted-text.txt`, `docs/product/spec-candidates.md`, `docs/product/roadmap.md`, Specs 378-379 management-report PDF lineage, Spec 400 product-contract audit context, and Spec 403 evidence/currentness closure context.
## Candidate Selection Context
- **Selected candidate**: Management Report PDF Staging Validation.
- **Source location**: Direct user-provided Spec 404 draft and manual promotion backlog item `management-report-pdf-staging-runtime-validation` in `docs/product/spec-candidates.md`.
- **Why selected**: The active automatic candidate queue still states that there is no safe automatic next-best-prep target. The operator supplied a concrete manual-promotion candidate for the first manual backlog item, and Spec 403 was prepared/closed as the direct evidence/currentness predecessor. This is the next bounded gate before production-style management-report PDF enablement.
- **Roadmap relationship**: Supports the roadmap P1 "Management Report PDF runtime validation and release hardening" priority. Management-report PDF generation is repo-real through Specs 378-379, but production enablement remains blocked until staging/Dokploy runtime validation proves renderer, storage, queue, download, authorization, customer-safe content, and evidence/currentness behavior.
- **Close alternatives deferred**:
- `governance-artifact-lifecycle-retention-runtime`: broader P2 artifact lifecycle and retention semantics; not required to validate the current PDF runtime.
- `provider-readiness-onboarding-productization`: optional provider UX productization; unrelated to report renderer/storage/download proof.
- `cross-domain-indicator-runtime-follow-through`: cross-surface indicator semantics; not a runtime deployment gate for PDFs.
- `manual-system-panel-browser-fixture-or-audit-procedure`: validation procedure work for system panel, not customer report output.
- JSON-to-JSONB data-layer hardening: valid later hardening, but must not proceed as if report runtime is production-ready without this gate.
- **Completed-spec guardrail result**:
- No `specs/404-management-report-pdf-staging-validation/` package existed before this preparation run.
- `specs/378-management-report-pdf-v1/` and `specs/379-management-report-pdf-runtime/` contain checked tasks, validation artifacts, tests, browser smoke, and implementation history. They are read-only context only.
- `specs/400-product-contract-spec-completeness-audit/` and `specs/403-evidence-anchor-currentness-runtime-closure/` contain audit/checklist/implementation-context signals and must not be rewritten.
- Spec 404 builds on the existing `StoredReport`, `ManagementReportPdfService`, `GenerateManagementReportPdfJob`, `ManagementReportPdfRuntimeGate`, `ManagementReportPdfRenderer`, signed download route, OperationRun, audit, and private `exports` disk path instead of replacing them.
- **Smallest viable implementation slice**: Inventory the existing management-report PDF path; run local and staging-like runtime validation; execute actual staging/Dokploy validation when accessible; validate at least one generated PDF as non-empty, renderable, customer-safe, scoped, and authorized; apply only minimal hardening for confirmed P0/P1 runtime proof defects; record a final PDF Runtime Matrix and gate result.
- **Feature description for Spec Kit**: Prove whether TenantPilot can safely generate, store, render, authorize, and serve existing Management Report PDFs in a staging/Dokploy-like runtime without broken renderer dependencies, false evidence/currentness claims, customer-data leakage, local-only path assumptions, storage/queue failures, or cross-workspace exposure.
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
- **Problem**: TenantPilot has an implemented management-report PDF path, but production-style enablement remains blocked because actual staging/Dokploy runtime proof is incomplete.
- **Today's failure**: Local tests and a local browser smoke can pass while the deployed renderer, queue worker, private storage, asset handling, signed download path, or customer-safe report content still fails or overstates readiness in staging.
- **User-visible improvement**: Operators get a clear, evidence-backed answer on whether Management Report PDFs are ready for production-style enablement, conditionally blocked, or require bounded remediation.
- **Smallest enterprise-capable version**: A runtime validation and minimal hardening slice over the existing Spec 379 PDF path. It produces a PDF Runtime Matrix, generated-PDF proof, authorization proof, staging/Dokploy proof or explicit missing-proof condition, and a final gate result.
- **Explicit non-goals**: No new report templates, no new PDF layouts, no new charts, no new customer/admin surfaces, no new navigation, no new renderer, no report delivery center, no scheduling/email delivery, no governance artifact lifecycle/retention, no JSONB migration, no full browser/UX/runtime audit, no AI summary, no generated customer data committed to the repo.
- **Permanent complexity imported**: One spec package, focused test/validation tasks, and a future implementation report/runtime matrix. Any runtime code change must be minimal, defect-driven, and attached to existing PDF/report/StoredReport/OperationRun seams. No new persisted entity, report taxonomy, renderer framework, or lifecycle framework is approved by default.
- **Why now**: The roadmap lists management-report PDF runtime validation as the current P1 sellability gate. Specs 378-379 made the path repo-real, and Spec 403 closed the evidence/currentness predecessor needed before customer-output runtime claims are expanded.
- **Why not local**: A local-only proof cannot verify Dokploy service wiring, internal renderer network access, queue worker execution, storage volume permissions, deployed asset/base URL behavior, or production-like authorization/download behavior.
- **Approval class**: Core Enterprise.
- **Red flags triggered**: Runtime validation touches deployment, report output, authorization, OperationRun, storage, and customer-facing content. Defense: the slice is proof-first, limited to existing behavior, and forbids new report/product/lifecycle scope.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 12/12**
- **Decision**: approve as the bounded PDF runtime validation and release-hardening gate.
## Problem Statement
TenantPilot can queue and store a Management Report PDF through the existing Spec 379 path, but production-style confidence is still blocked by runtime proof. The feature must answer:
```text
Can TenantPilot generate, store, render, authorize, and serve a Management Report PDF in a staging/Dokploy-like environment without broken assets, false evidence claims, customer-data leakage, runtime errors, or cross-workspace exposure?
```
The path is valid only when a generated PDF is non-empty, renderable, customer-safe, scoped to the correct workspace and managed environment, downloadable only by entitled actors, backed by correct report/evidence receipt state, independent of local-only assumptions, and honest when generation fails.
For this spec, **staging-like** proof means a non-production validation run that exercises production-relevant runtime boundaries: separate app and queue worker execution, internal-only pinned Gotenberg access from the app/worker network, private `exports` disk writes by the worker and reads by the web process, production-like `APP_URL`/base URL and asset behavior, runtime validation flags set only in the validation environment, no public renderer exposure, and no secrets, private URLs, or sensitive logs committed. **Equivalent production-like validation** may replace actual Dokploy proof only when it explicitly proves the same criteria and names the environment used.
## Product / Business Value
- Removes the current P1 blocker before management-ready PDF output can be enabled outside local/test contexts.
- Prevents false customer-facing claims such as "current", "verified", "ready", or "generated" when the artifact or evidence does not support them.
- Gives release reviewers a concrete gate result instead of assumptions about Gotenberg, Dokploy, queue workers, storage, signed routes, and customer-safe content.
- Keeps future hardening specs smaller by separating runtime validation from report redesign, lifecycle/retention, and data-layer work.
## Primary Users / Operators
- MSP service manager validating whether a generated management report can be shared.
- TenantPilot operator or release reviewer validating staging/Dokploy readiness.
- Customer executive or IT stakeholder consuming the generated PDF.
- Support/platform operator diagnosing report-generation failures through existing OperationRun/audit/log paths.
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace plus managed-environment runtime validation over the existing management-report PDF path.
- **Primary Routes / Surfaces**:
- Review Pack detail owner surface: `apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php`.
- Signed PDF download route: `/admin/management-report-pdfs/{storedReport}/download` handled by `ManagementReportPdfDownloadController`.
- Existing rendered report source: `/admin/review-packs/{reviewPack}/report`.
- Generated PDF artifact stored through `StoredReport` and private `exports` disk.
- OperationRun detail/proof and audit trail for `report.management.generate`.
- Deployment/runtime configuration in `docker-compose.yml`, `apps/platform/config/tenantpilot.php`, `.env.example`, `docs/deployment-checklist.md`, and Dokploy/staging configuration outside the repo when accessible.
- **Data Ownership**: Generated Management Report PDF artifacts remain `StoredReport` records owned by workspace and managed environment, with source review pack, environment review, OperationRun, actor, private storage path, file size, SHA-256, status, profile, and payload metadata. No new table or source of truth is approved by default.
- **RBAC**: Generation remains `Capabilities::REVIEW_PACK_MANAGE`; download remains `Capabilities::REVIEW_PACK_VIEW` plus `CustomerOutputGate`; wrong workspace/environment/non-member access is deny-as-not-found; scoped members without capability receive 403. The system and admin planes remain separated.
For canonical or mixed-scope views:
- **Default filter behavior when environment-context is active**: PDF generation and download are anchored to the persisted source Review Pack and StoredReport, not hidden remembered context or arbitrary current environment state.
- **Explicit entitlement checks preventing cross-tenant leakage**: The download path must re-resolve `StoredReport`, source Review Pack, source review, workspace, managed environment, current-pack status, actor workspace/environment entitlement, capability, customer-output gate, and file existence before returning bytes.
## No Legacy / No Backward Compatibility Constraint *(mandatory)*
TenantPilot is pre-production for this runtime gate.
- **Compatibility posture**: canonical existing Spec 379 behavior; do not preserve local-only or pre-gate behavior that can produce false production readiness.
- **Legacy aliases, fallback readers, hidden routes, duplicate UI, old labels, or historical fixtures kept?**: no new compatibility behavior is allowed. Existing completed specs remain read-only historical evidence.
- **Why clean replacement is safe now**: PDF generation is still runtime-gated and not a fully productized production capability. Validation may tighten behavior before enablement without compatibility shims.
## 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/state added
- [x] Customer-facing surface changed
- [x] Dangerous action changed
- [x] Status/evidence/review presentation changed
- [x] Workspace/environment context presentation changed
This classification is conservative because implementation may need minimal defect-driven hardening of existing report/receipt/download/failure states. It does not authorize new surfaces, routes, navigation, or report templates.
## UI/Productization Coverage
- **Route/page/surface**: Review Pack detail management-PDF actions, signed PDF download route, generated PDF report output, StoredReport artifact metadata, OperationRun proof link, and customer-output gate state.
- **Current or new page archetype**: Report Page for generated PDF; Receipt Page for ready/failed/generated artifact state; Secondary Context for OperationRun proof.
- **Design depth**: Strategic customer-facing report output, with validation-only runtime scope.
- **Repo-truth level**: repo-verified through Spec 379 implementation; staging/Dokploy validation pending.
- **Existing pattern reused**: `ReviewPackResource`, `ManagementReportPdfService`, `ManagementReportPdfRuntimeGate`, `StoredReport`, `ManagementReportPdfDownloadController`, `PdfRenderingGateway`, `OperationRunService`, `CustomerOutputGate`, signed route, private `exports` disk, existing Spec 379 tests/browser smoke.
- **New pattern required**: none by default. If validation proves missing runtime proof, add only a spec-local implementation report/runtime matrix and minimal defect-driven tests or hardening.
- **Screenshot required**: yes when browser proof runs against existing report/receipt/download state. Existing Spec 379 screenshots are context, not sufficient staging proof.
- **Page audit required**: update or record no-update rationale for `ui-042-review-pack-detail.md`, `ui-048-stored-report-detail.md`, and `ui-099-rendered-review-report.md` only if runtime UI or customer output behavior changes materially.
- **Customer-safe review required**: yes, because generated PDF output is customer-facing.
- **Dangerous-action review required**: yes. Generation is high-impact durable artifact creation and must remain confirmation-gated, authorized, audited, and OperationRun-observable.
- **Coverage files updated or explicitly not needed**:
- [ ] `docs/ui-ux-enterprise-audit/route-inventory.md`
- [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`
- [ ] `docs/ui-ux-enterprise-audit/page-reports/...`
- [ ] `docs/ui-ux-enterprise-audit/strategic-surfaces.md`
- [ ] `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md`
- [ ] `docs/ui-ux-enterprise-audit/unresolved-pages.md`
- [ ] `N/A - no reachable UI surface impact`
- **No-impact rationale when applicable**: N/A. Runtime validation touches existing rendered report/download/customer-output behavior.
## Product Surface Impact *(mandatory)*
Reference: `docs/product/standards/product-surface-contract.md`.
- **Product Surface Contract applies?**: yes. Downloads, reports, readiness/evidence labels, customer output, and staging/runtime validation are product-facing surface concerns.
- **Page archetype**: Report Page for the generated PDF, Receipt Page for artifact state, Technical Annex only for OperationRun/log diagnostics.
- **Primary user question**: "Can I trust and safely download this management report PDF?"
- **Primary action**: Download management PDF when ready; generate management PDF only when no ready artifact exists and runtime/source gates pass.
- **Surface budget result**: must remain pass. Default product view must not expose raw identifiers, OperationRun internals, raw evidence, provider payloads, storage paths, or local URLs.
- **Technical Annex / deep-link demotion**: OperationRun, raw evidence, source keys, provider payloads, storage paths, stack traces, SQL errors, renderer internals, and log details stay out of the PDF and default customer-readable path. They may remain in authorized OperationRun/log/audit surfaces only.
- **Canonical status vocabulary**: product-facing states map to Ready, Needs attention, Blocked, Running, Failed, Unknown, Historical, or Superseded. Internal states such as `queued`, `generating`, and renderer reason codes must be translated before customer display.
- **Visible complexity impact**: neutral or decreased. Validation must not add another report surface or duplicated readiness model.
- **Product Surface exceptions**: none approved by this spec. The bounded report-canvas exception from Spec 366 remains historical context for the existing PDF output and must not spread.
## Browser Verification Plan *(mandatory)*
- **Browser proof required?**: yes.
- **No-browser rationale**: N/A.
- **Focused path when required**: authorized operator opens Review Pack detail, observes generate/blocked/ready state, generates or accesses a Management Report PDF through the existing path, downloads or opens the PDF, and validates unauthorized/wrong-workspace blocking.
- **Primary interaction to execute**: report generation/download/receipt flow and failed/unavailable state display.
- **Console, Livewire, Filament, network, and 500-error checks**: required for the focused path. Record console/runtime/network result and any unrelated failures separately.
- **Full-suite failure triage**: unrelated full-suite or browser failures must be documented as unrelated only when the focused Spec 404 path is green and evidence supports the classification.
## Human Product Sanity Check *(mandatory)*
- **Required?**: yes.
- **No-human-sanity rationale**: N/A.
- **Reviewer questions**: Does the report communicate its purpose? Is exactly one dominant next action visible? Are technical details demoted? Are status/evidence labels canonical and truthful? Does the generated PDF feel trustworthy and not more complex? Would a customer/operator trust the flow?
- **Planned result location**: `specs/404-management-report-pdf-staging-validation/implementation-report.md`.
## Product Surface Merge Gate Checklist *(mandatory)*
- [x] No-legacy posture or approved exception recorded.
- [x] Product Surface Impact is completed.
- [x] Browser proof plan is completed.
- [x] Human Product Sanity plan is completed.
- [x] Product Surface exceptions are documented as `none`.
- [x] Implementation report will state Livewire v4 compliance, provider registration location, global search posture, destructive/high-impact action posture, asset strategy, tests/browser result, deployment impact, visible complexity outcome, and no completed-spec rewrite assertion.
## Cross-Cutting / Shared Pattern Reuse
- **Cross-cutting feature?**: yes.
- **Interaction class(es)**: report output, artifact download, high-impact header action, status/readiness messaging, OperationRun link, audit, customer-safe evidence/report viewer.
- **Systems touched**: PDF renderer gateway, management-report payload/rendering path, Review Pack owner surface, StoredReport artifact substrate, private storage, signed route, OperationRun, audit, customer-output gate, deployment/runtime configuration.
- **Existing pattern(s) to extend**: Spec 379 management-report PDF implementation and Spec 378 renderer gateway.
- **Shared contract / presenter / builder / renderer to reuse**: `ManagementReportPdfService`, `ManagementReportPdfRenderer`, `ManagementReportPdfPayloadBuilder`, `ManagementReportPdfRuntimeGate`, `PdfRenderingGateway`, `PdfRendererClient`, `ReviewPackService`, `CustomerOutputGate`, `OperationRunService`, `OperationRunLinks`, `OperationUxPresenter`, `WorkspaceAuditLogger`.
- **Why the existing shared path is sufficient or insufficient**: It is sufficient for v1 generation and safety controls. It is insufficient only as proof of actual staging/Dokploy runtime until Spec 404 records validation evidence or bounded defects.
- **Allowed deviation and why**: none by default. Minimal hardening may be added only for confirmed P0/P1 runtime proof defects.
- **Consistency impact**: report state, StoredReport metadata, OperationRun status/outcome, audit events, PDF content, download route, and final runtime matrix must agree on source review pack, profile, workspace, environment, actor, file hash, and failure state.
- **Review focus**: no second renderer, no public storage, no unscoped download, no raw payloads, no stack traces, no local file paths/localhost links in output, no false `ready` state, and no production enablement without staging proof.
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: yes, through the existing `report.management.generate` operation.
- **Shared OperationRun UX contract/layer reused**: `OperationRunService`, `OperationRunLinks`, `OperationUxPresenter`, `OperationRunCompleted`, and existing Monitoring -> Operations detail paths.
- **Delegated start/completion UX behaviors**: queued toast, `Open PDF operation`, artifact/download link, run-enqueued browser event, dedupe/blocked messaging, tenant/workspace-safe URL resolution, and terminal notification remain shared-path responsibilities.
- **Local surface-owned behavior that remains**: source Review Pack readiness explanation, runtime validation blocked reason, customer-output gate blocked reason, and artifact-specific download state.
- **Queued DB-notification policy**: no new queued DB notification policy. Keep existing Spec 379 behavior unless a later spec explicitly changes it.
- **Terminal notification path**: central lifecycle mechanism.
- **Exception required?**: none.
## Provider Boundary / Platform Core Check
- **Shared provider/platform boundary touched?**: yes, indirectly through report/evidence content.
- **Boundary classification**: platform-core report artifact and operation truth; provider-owned details only where already disclosed by existing customer-safe report payloads.
- **Seams affected**: generated artifact proof, evidence basis/currentness labels, report payload, operation proof, audit metadata, deployment/runtime renderer boundary.
- **Neutral platform terms preserved or introduced**: management report, generated artifact, evidence basis, limitation, workspace, managed environment, operation, runtime validation.
- **Provider-specific semantics retained and why**: only existing customer-safe Microsoft/provider summaries already present in the source review/report truth.
- **Why this does not deepen provider coupling accidentally**: validation uses stored review/report truth and the internal renderer only; it must not call Microsoft Graph or add provider-specific runtime behavior.
- **Follow-up path**: `follow-up-spec` only if validation uncovers broader provider/report semantic debt.
## UI / Surface Guardrail Impact
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / N/A Note |
|---|---|---|---|---|---|---|
| Review Pack detail management PDF state | yes | Native Filament action surface | header action, status messaging, OperationRun link | detail header, modal/action state, artifact state | no | Existing surface only |
| Management Report PDF output | yes | Existing report/PDF output | report viewer, customer-safe disclosure | PDF artifact, report payload | none new | Existing bounded report output only |
| Signed PDF download route | yes | server-authorized route plus native action/link | artifact download, audit | URL, storage, detail state | no | Existing route only |
| OperationRun proof | yes | Native Monitoring/Operations path | operation proof link | operation detail | no | Technical Annex/secondary context |
## Decision-First Surface Role
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|---|---|---|---|---|---|---|---|
| Management Report PDF | Primary Decision Surface for customer consumption | Customer/MSP decides whether reported risks, decisions, and next actions are trustworthy | title, environment, profile, generated date, executive summary, posture, evidence basis, limitations, next actions | OperationRun/log/source details outside the PDF | Primary because the PDF is the customer-readable artifact | review/report handoff | avoids raw ZIP/report internals for management review |
| Review Pack PDF action state | Secondary Workflow Action | Operator decides whether to generate, wait, or download | current source, runtime gate, blocked/ready/running state, one action | run detail, audit, storage proof | Secondary because it starts or retrieves output | existing Review Pack owner flow | avoids a separate report center |
| OperationRun proof | Tertiary Evidence / Diagnostics Surface | Support/operator diagnoses generation failure | operation outcome, safe reason, counters | logs/audit as authorized | Not primary because customers should not need it | existing operations flow | keeps diagnostics out of customer PDF |
## Audience-Aware Disclosure
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|---|---|---|---|---|---|---|---|
| Customer PDF | customer/read-only, operator-MSP | summary, posture, evidence basis, limitations, next actions, provenance | absent or summarized safely | absent | read/share/download according to policy | raw evidence, IDs, storage paths, renderer detail | state/blockers appear once; later sections add proof |
| Review Pack detail action | operator-MSP | generate/running/download/blocked state and reason | OperationRun link | logs outside page | generate, open operation, or download based on state | raw renderer errors and storage paths | one state-dependent action |
| Download route | entitled operator/customer according to existing gate | file bytes and safe filename | audit metadata | storage internals hidden | download PDF | signed URL internals, file path | audit stores source once |
## UI/UX Surface Classification
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Review Pack detail PDF action | Record / Detail / Edit | Detail-first Operational Surface | Generate PDF, open operation, or download | existing detail page | N/A | header or grouped per existing hierarchy | N/A; high-impact generation confirmed | Review Packs | Review Pack detail | workspace and managed environment | Management report PDF | runtime gate, source readiness, artifact state | none |
| Management Report PDF | Report / Artifact | Report Page | Read/share/download | signed/server-authorized download | N/A | source/run links outside PDF | none | owner surfaces only | download route | workspace, managed environment, profile | evidence basis, generated metadata, limitations | existing report output only |
## Operator Surface Contract
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|---|---|---|---|---|---|---|---|---|---|---|
| Generate/download management PDF state | MSP operator | create or retrieve durable customer output | detail action/receipt | Is this source safe and ready to use as a PDF? | source, runtime gate, profile, readiness, blocked reason, generated state | OperationRun/log/storage detail | runtime validation, generation state, customer-output readiness | TenantPilot artifact only | Generate PDF, Open operation, Download PDF | high-impact artifact creation |
| Management Report PDF | customer stakeholder, MSP operator | understand governance posture and next actions | report artifact | What does the report say, can I trust it, and what should happen next? | executive summary, posture, evidence basis, limitations, generated metadata | none in customer profile | evidence completeness, generated/static report truth | read-only | Read/download/share | none |
## Proportionality Review
- **New source of truth?**: no new source of truth by default. The implementation report/runtime matrix is spec-local proof; generated PDFs remain existing `StoredReport` artifact truth.
- **New persisted entity/table/artifact?**: no new table or artifact family is approved. Use existing `StoredReport` and file storage.
- **New abstraction?**: no new abstraction by default. Existing services/gates/renderers should be reused.
- **New enum/state/reason family?**: no new status family by default. If validation uncovers a behavior-changing reason code gap, update spec/plan before adding it.
- **New cross-domain UI framework/taxonomy?**: no.
- **Current operator problem**: production-style PDF enablement is blocked by missing staging/Dokploy proof.
- **Existing structure is insufficient because**: Spec 379 proves local implementation and blocks runtime by default, but not actual deployed renderer/queue/storage/download behavior.
- **Narrowest correct implementation**: proof-first validation plus minimal defect fixes over existing path.
- **Ownership cost**: one implementation report, focused validation commands, possible targeted tests/hardening, staging/Dokploy proof procedure.
- **Alternative intentionally rejected**: new report system, new renderer, public storage, report redesign, lifecycle framework, local-only acceptance.
- **Release truth**: current-release runtime gate before production/customer PDF claims.
## Functional Requirements
- **FR-404-001**: The implementation MUST inventory the existing management-report PDF path before runtime edits: service, job, renderer, payload builder, runtime gate, `StoredReport`, download route/controller, Review Pack action state, storage disk, OperationRun, audit, customer-output gate, config, Docker/Dokploy deployment controls, and current tests.
- **FR-404-002**: The implementation MUST create or update `specs/404-management-report-pdf-staging-validation/implementation-report.md` with the required PDF Runtime Matrix and final gate result.
- **FR-404-003**: The PDF Runtime Matrix MUST include Generation, Renderer dependency, Asset/CSS handling, Storage/write permissions, Queue/worker, Download authorization, Workspace/environment scope, Customer-safe content, Evidence/currentness labels, Failure handling, and Staging/Dokploy validation.
- **FR-404-004**: Matrix status values MUST be one of `PASS`, `PASS WITH CONDITIONS`, `MISSING PROOF`, `DEFECT FOUND`, `DEFERRED`, or `NOT APPLICABLE`; risk values MUST be one of `P0`, `P1`, `P2`, `P3`, or `None`.
- **FR-404-005**: At least one generated Management Report PDF MUST be validated for file existence, non-zero size, parse/open success, rendered first page not blank, expected title/metadata, no debug/stack trace text, no raw provider/source data, no local absolute path, no localhost URL, customer-safe content, and truthful evidence/currentness labels.
- **FR-404-006**: If actual staging/Dokploy validation is unavailable, the final gate MUST NOT be `PASS`; it may be `PASS WITH CONDITIONS` only if local and staging-like proof is strong and the remaining production enablement condition is explicit.
- **FR-404-007**: Production/customer enablement MUST remain blocked when any P0 finding remains, generated PDF is blank/unreadable, unauthorized/cross-workspace download is possible, customer-safe PDF exposes internal-only data, failed generation is marked ready, evidence/currentness claims are false, or renderer/storage/queue path cannot be proven.
- **FR-404-008**: Download authorization MUST prove authorized access, direct signed route access, unauthorized actor denial, cross-workspace denial, customer reviewer boundary, current source Review Pack requirement, and missing file behavior.
- **FR-404-009**: Failed, missing, partial, or zero-byte generation MUST NOT produce or serve a ready/generated Management Report PDF.
- **FR-404-010**: Customer-safe PDF content MUST exclude raw provider payloads, raw evidence source keys, stack traces, internal exception messages, local/container paths, database IDs where not approved, system-only links, unsigned internal URLs, debug flags, environment variables, secrets, and renderer/storage internals.
- **FR-404-011**: Evidence/currentness labels MUST not overstate stale, missing, failed, partial, released/static, or historical proof as current, verified, live, complete, or customer-ready.
- **FR-404-012**: Queue/worker validation MUST prove the job can execute through the configured worker path where generation is queued, or record exact missing proof and keep enablement blocked.
- **FR-404-013**: Storage validation MUST prove the runtime/worker can write to the configured private `exports` disk and the web process can serve the authorized download without public path exposure.
- **FR-404-014**: Asset/CSS validation MUST prove the generated PDF does not depend on local absolute paths, developer fonts, localhost-only URLs, missing build artifacts, or public storage symlink assumptions.
- **FR-404-015**: Runtime hardening changes are allowed only for confirmed in-scope P0/P1 defects and must stay within existing PDF/report/StoredReport/ReviewPack/OperationRun/audit seams.
- **FR-404-016**: No new report template, report section, chart, customer claim, navigation item, panel provider, renderer package, queue architecture, lifecycle/retention framework, or unrelated deployment change may be introduced.
- **FR-404-017**: Any validation commands, browser proof, generated-PDF validation result, staging/Dokploy proof, skipped proof, and unrelated failures MUST be recorded in the implementation report.
- **FR-404-018**: Generated PDFs, staging credentials, private URLs, secrets, and generated customer data MUST NOT be committed.
## Non-Functional Requirements
- **Security**: no secrets, tokens, raw provider payloads, stack traces, signed URL internals, or storage paths in PDF output, audit metadata, logs, committed fixtures, or final report.
- **Performance**: generation remains queued/observable and must not add live provider calls or page-render external calls.
- **Reliability**: renderer/storage/queue failures fail closed and produce honest failed/blocked states.
- **Deployment**: staging/Dokploy validation must cover env vars, internal renderer service, queue worker, storage persistence, asset availability, and relevant logs.
- **Test governance**: use focused Unit/Feature/Filament/Browser/PDF-validation lanes; do not create a broad browser/runtime audit family.
## User Stories & Tests
### User Story 1 - Validate runtime path before enablement (P1)
As a release reviewer, I need a concrete runtime matrix for the existing Management Report PDF path so that production-style enablement is based on proof rather than local assumptions.
**Independent Test**: Inventory the existing path, run local/staging-like validation, and produce a runtime matrix with explicit result/risk/follow-up for every required area.
**Acceptance Scenarios**:
1. **Given** the current repo and Spec 379 implementation, **When** validation starts, **Then** the implementation records branch, HEAD, dirty state, relevant config/services/routes/tests, and no completed-spec rewrites.
2. **Given** local/staging-like runtime is available, **When** PDF generation is exercised, **Then** generated file existence, non-zero size, parser/render success, first page non-blank, and expected report metadata are recorded.
3. **Given** actual staging/Dokploy is unavailable, **When** the final gate is written, **Then** staging validation is `MISSING PROOF` and production enablement remains conditional or blocked.
### User Story 2 - Prove authorization and customer-safe output (P1)
As a tenant operator/customer reviewer, I need downloads and report content to be scoped and customer-safe so that generated PDFs cannot leak another workspace, raw evidence, internal diagnostics, or false currentness claims.
**Independent Test**: Exercise positive/negative download paths and inspect generated PDF content for required and forbidden strings.
**Acceptance Scenarios**:
1. **Given** a ready management report PDF, **When** an authorized actor uses the signed route, **Then** the PDF downloads and audit metadata is written without exposing storage internals.
2. **Given** a wrong-workspace or unauthorized actor, **When** they attempt the same route, **Then** the response is 404 or 403 according to workspace/capability semantics and no report bytes are returned.
3. **Given** stale, missing, partial, failed, or released evidence, **When** the PDF is generated or inspected, **Then** customer-facing labels do not claim live/current/verified/complete proof beyond the source truth.
### User Story 3 - Fail honestly when runtime breaks (P1)
As a support/platform operator, I need renderer, queue, storage, and missing-file failures to be visible as failed/blocked states so that a broken PDF is never served as ready.
**Independent Test**: Simulate or observe renderer/storage/missing-file failures and verify `StoredReport`, OperationRun, audit, and download behavior.
**Acceptance Scenarios**:
1. **Given** renderer failure, **When** the job runs, **Then** `StoredReport` is failed, OperationRun is terminal failed/blocked, audit is safe, and no ready PDF is served.
2. **Given** storage write failure or a missing stored file, **When** generation/download is attempted, **Then** the system fails closed and does not return partial or zero-byte content as a valid PDF.
3. **Given** a queued job path, **When** the worker cannot execute in staging, **Then** the runtime matrix records missing proof or defect and production enablement remains blocked.
## Edge Cases
- Actual staging/Dokploy access is unavailable from the implementation environment.
- Gotenberg health endpoint is reachable but HTML conversion fails due sandbox/file allow-list settings.
- Queue dispatch succeeds but worker process does not run or cannot reach Gotenberg.
- Web process can read storage but queue worker cannot write to the private exports disk, or vice versa.
- File exists but is zero-byte, non-PDF, corrupt, blank, or contains raw HTML as text.
- Source Review Pack is expired, not current, missing its ZIP artifact, or replaced after generation.
- Signed route is valid but actor loses capability or workspace/environment entitlement before download.
- Customer-output gate blocks a report after a queued job was created but before generation.
- APP_URL/base URL or asset paths produce localhost/local absolute URLs in the PDF.
- Actual staging proof produces private URLs or logs that must not be copied into the repo or final public report.
## Out of Scope
- New report templates, layouts, charting, copywriting, branding, or customer claims.
- New customer/admin surfaces, navigation, report center, scheduling, delivery, email, portal, AI, or billing PDF scope.
- New PDF renderer, package-governance redo, browser runtime in Laravel containers, or second renderer service.
- Governance artifact lifecycle/retention/export/delete/hold semantics.
- JSON-to-JSONB migrations, broad storage refactors, queue architecture changes, or full deployment rewrite.
- Full browser audit, broad UX/product audit, system-panel smoke procedure, or unrelated security/performance audit.
- Committing generated PDFs, staging credentials, private URLs, customer data, or secrets.
## Success Criteria
- **SC-404-001**: The implementation report answers `PASS`, `PASS WITH CONDITIONS`, or `FAIL` with concrete evidence and no hidden staging assumptions.
- **SC-404-002**: A generated Management Report PDF is proven non-empty, parseable/renderable, not blank, customer-safe, and tied to the correct source report/receipt state.
- **SC-404-003**: Authorized and unauthorized download behavior is tested and recorded, including cross-workspace/cross-environment protection.
- **SC-404-004**: Failed generation, missing files, or zero-byte/corrupt output never produce a ready/downloadable artifact.
- **SC-404-005**: Staging/Dokploy validation is either completed or explicitly recorded as the remaining blocker/condition before production enablement.
- **SC-404-006**: No new report product scope, renderer infrastructure, lifecycle framework, or unrelated deployment changes are introduced.
## Assumptions
- Spec 403 returned enough evidence/currentness closure to proceed to the PDF staging-validation gate.
- Spec 379's existing implementation remains the canonical PDF path and should be validated/hardened rather than replaced.
- `StoredReportResource` global search remains disabled unless a future spec explicitly changes it.
- Actual staging/Dokploy credentials or access may not be available to the implementation agent; missing access must be recorded honestly.
- Laravel Sail remains the preferred local validation path; Dokploy remains the staging/production deployment target.
## Risks
- Actual staging access may be unavailable, limiting final gate to `PASS WITH CONDITIONS` or `FAIL`.
- Local and Sail validation may mask Docker/Dokploy differences in service networking, worker process supervision, storage volumes, or APP_URL behavior.
- PDF render validation can become layout-polish scope creep; this spec limits visual checks to functional render validity.
- Runtime defects may require broader architecture decisions. If a second renderer, new table, new report template, or lifecycle framework seems necessary, implementation must stop and update spec/plan before continuing.
## Open Questions
- Can the implementation agent access actual staging/Dokploy logs, environment configuration, queue worker status, storage volume state, and generated report flow?
- Is a production-like staging workspace/environment fixture available for non-secret PDF validation, or must a local staging-like fixture be used?
- Which exact toolchain is available in the implementation environment for PDF parser/render validation beyond browser download proof?

View File

@ -0,0 +1,143 @@
# Tasks: Spec 404 - Management Report PDF Staging Validation
**Input**: Design documents from `specs/404-management-report-pdf-staging-validation/`
**Prerequisites**: `spec.md`, `plan.md`, `checklists/requirements.md`
## Execution Rules
- [x] No application implementation begins until T001-T012 are complete.
- [x] Do not modify completed Specs 378, 379, 400, or 403 except as read-only context.
- [x] Do not add report templates, report sections, report center/navigation, renderer packages, lifecycle/retention scope, JSONB migrations, queue architecture, or unrelated deployment changes.
- [x] Do not commit generated PDFs, staging credentials, private URLs, secrets, logs with sensitive content, or generated customer data.
- [x] Use Sail-first local commands unless actual staging/Dokploy validation is being recorded.
- [x] If actual staging/Dokploy access is unavailable, final gate cannot be `PASS`.
## Test Governance and Lane Classification
- [x] Unit purpose: runtime gate, renderer/gateway, payload sanitization, PDF validation helper behavior.
- [x] Feature purpose: generation state, storage, download authorization, cross-workspace denial, missing/zero-byte/failure state, audit, OperationRun proof.
- [x] Filament/Livewire purpose: Review Pack detail management-PDF action state if runtime UI behavior changes.
- [x] Browser purpose: focused Review Pack detail and PDF download/open smoke only.
- [x] PDF validation purpose: parser/text extraction/render-to-image or equivalent functional render proof.
- [x] Heavy-governance: N/A unless a new guard family is explicitly justified in `implementation-report.md`.
## Product Surface and Filament Contract
- [x] Review `docs/product/standards/product-surface-contract.md` before any runtime UI/report-output edit.
- [x] Preserve Livewire v4.1.4 posture; introduce no Livewire v3 APIs.
- [x] Preserve panel provider registration in `apps/platform/bootstrap/providers.php`; no panel provider change is planned.
- [x] Preserve `StoredReportResource` global-search disabled posture unless a future spec explicitly changes it.
- [x] Keep `Generate management PDF` as high-impact artifact creation with `Action::make(...)->action(...)`, `->requiresConfirmation()`, server-side authorization, OperationRun, audit, and tests.
- [x] Keep PDF download signed/server-authorized and audited.
- [x] Add no new Filament assets. If assets are registered unexpectedly, record `filament:assets` deployment impact and update spec/plan first.
## Phase 0 - Preparation and Dirty State
- [x] T001 Read `specs/404-management-report-pdf-staging-validation/spec.md`, `plan.md`, `tasks.md`, and `checklists/requirements.md`.
- [x] T002 Re-read `AGENTS.md`, `.specify/memory/constitution.md`, `.specify/README.md`, `docs/ai-coding-rules.md`, `docs/architecture-guidelines.md`, `docs/filament-guidelines.md`, `docs/security-guidelines.md`, `docs/testing-guidelines.md`, `docs/performance-guidelines.md`, and `docs/product/standards/product-surface-contract.md`.
- [x] T003 Re-read `specs/378-management-report-pdf-v1/`, `specs/379-management-report-pdf-runtime/`, `specs/400-product-contract-spec-completeness-audit/`, and `specs/403-evidence-anchor-currentness-runtime-closure/` as read-only context; preserve completed-spec history.
- [x] T004 Record current branch, HEAD, dirty state, tracked changed files, untracked files, and `git diff --check` in `specs/404-management-report-pdf-staging-validation/implementation-report.md`.
- [x] T005 Confirm no new report template, report product claim, route/navigation, renderer, package, persisted entity, lifecycle/retention scope, JSONB migration, queue architecture, broad browser audit, or generated customer fixture is in scope.
## Phase 1 - Existing Path Inventory
- [x] T006 Inventory `apps/platform/app/Services/ReviewPacks/ManagementReportPdfService.php`, including generation decision, runtime gate use, ready/active/failed report handling, OperationRun creation, dispatch, audit, and signed URL generation.
- [x] T007 Inventory `apps/platform/app/Jobs/GenerateManagementReportPdfJob.php`, including running/failed/blocked/ready state transitions, storage write, SHA-256/file metadata, OperationRun updates, and audit events.
- [x] T008 Inventory `apps/platform/app/Services/ReviewPacks/ManagementReportPdfRenderer.php`, `ManagementReportPdfPayloadBuilder.php`, `ManagementReportPdfRuntimeGate.php`, `PdfRenderingGateway.php`, and `PdfRendererClient.php` for renderer dependency, HTML/CSS output, payload sanitization, runtime gate defaults, timeouts, and safe error mapping.
- [x] T009 Inventory `apps/platform/app/Http/Controllers/ManagementReportPdfDownloadController.php`, `apps/platform/routes/web.php`, `apps/platform/app/Models/StoredReport.php`, and `apps/platform/config/filesystems.php` for signed route, status requirements, file existence checks, private `exports` disk, scope checks, customer-output gate, and audit.
- [x] T010 Inventory Review Pack owner-surface behavior in `apps/platform/app/Filament/Resources/ReviewPackResource.php` and `apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php`, including generate/open-operation/download action state.
- [x] T011 Inventory runtime/deployment controls in `docker-compose.yml`, `apps/platform/.env.example`, `apps/platform/config/tenantpilot.php`, `docs/deployment-checklist.md`, and `docs/package-governance.md`.
- [x] T012 Inventory existing tests: `Spec378PdfRenderingGatewayTest`, `Spec379ManagementReportPdfReadinessTest`, `Spec379ManagementReportPdfTest`, `Spec379ManagementReportPdfSmokeTest`, `ReviewPackDownloadTest`, and customer-output gate tests relevant to PDF/download behavior.
## Phase 2 - Implementation Report and Runtime Matrix
- [x] T013 Create `specs/404-management-report-pdf-staging-validation/implementation-report.md` using the required sections from `spec.md`.
- [x] T014 Add the PDF Runtime Matrix with rows for Generation, Renderer dependency, Asset/CSS handling, Storage/write permissions, Queue/worker, Download authorization, Workspace/environment scope, Customer-safe content, Evidence/currentness labels, Failure handling, and Staging/Dokploy validation.
- [x] T015 For each matrix row, record Runtime mechanism, Proof performed, Result, Risk, and Follow-up using the exact status/risk vocabulary from `spec.md`.
- [x] T016 Record the current runtime gate posture for `TENANTPILOT_PDF_RENDERER_ENABLED`, `TENANTPILOT_PDF_RENDERER_RUNTIME_VALIDATED`, `TENANTPILOT_PDF_RENDERER_BASE_URL`, renderer limits/timeouts, queue connection, `APP_URL`, and storage disk.
- [x] T017 Record whether actual staging/Dokploy access is available. If unavailable, pre-mark Staging/Dokploy validation as `MISSING PROOF` until a real proof is supplied.
## Phase 3 - Existing Test Baseline and Gap Closure
- [x] T018 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec379`; record result.
- [x] T019 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec378`; record result.
- [x] T020 Run existing focused download/customer-output coverage that overlaps Spec 404, including `ReviewPackDownloadTest` and any customer-output gate tests identified in T012; record result.
- [x] T021 Run existing browser smoke `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec379ManagementReportPdfSmokeTest.php --compact` if the local browser lane is available; record result and screenshots/artifacts if produced.
- [x] T022 Add or update focused Spec404 tests only for confirmed proof gaps: PDF parse/render validation, missing/zero-byte/corrupt file handling, direct signed-route authorization, cross-workspace/cross-environment denial, customer reviewer boundary, failure state truth, or local path/localhost leak checks.
- [x] T023 If tests are added, keep fixtures explicit and feature-local; do not widen global seeds, helper defaults, provider setup, workspace/session context, or browser fixture families.
## Phase 4 - Local and Staging-Like PDF Validation
- [x] T024 Start or verify the local Sail services needed for PDF validation, including Laravel app, separate queue worker execution, PostgreSQL, private `exports` storage, internal-only Gotenberg, production-like `APP_URL`/asset behavior where feasible, and validation-only runtime flags.
- [x] T025 Confirm the Gotenberg service is internal-only, pinned, reachable by the app/queue container, and not exposed publicly.
- [x] T026 Generate or reuse one non-secret ready Review Pack fixture suitable for management-report PDF generation.
- [x] T027 Temporarily set runtime validation only in the validation environment, not committed config, to exercise the generation path.
- [x] T028 Trigger Management Report PDF generation through the existing service/action path and verify the queue worker executes `GenerateManagementReportPdfJob` without live Graph/provider client calls or external page-render resource fetches beyond the approved internal renderer/assets path, using mocks, spies, logs, or focused assertions where available.
- [x] T029 Validate `StoredReport` metadata: workspace, managed environment, source review, source Review Pack, operation run, profile, status, file disk/path, file size, SHA-256, generated timestamp, and payload state.
- [x] T030 Validate generated PDF bytes: file exists, non-zero size, parser/open success, expected title/metadata, and expected profile/environment labels.
- [x] T031 Render the first page to an image or equivalent visual proof and verify it is not blank, raw HTML, overlapped beyond functional readability, or missing critical report text.
- [x] T032 Extract text or otherwise inspect content for forbidden values: raw provider payloads, raw evidence source keys, stack traces, internal exception messages, local/container paths, localhost/dev URLs, database IDs where not approved, system-only links, unsigned internal URLs, debug flags, env vars, tokens, and secrets.
- [x] T033 Validate evidence/currentness labels against the source Review Pack/review state and confirm no stale/missing/failed/partial/released evidence is overstated as live/current/verified/complete.
- [x] T034 Validate download behavior as an authorized actor through the signed route and confirm audit metadata is safe.
- [x] T035 Validate unauthorized, wrong-workspace, wrong-environment, missing-capability, expired/not-current Review Pack, missing-file, and non-ready StoredReport download behavior.
## Phase 5 - Failure and Hardening Work
- [x] T036 Simulate or observe renderer unavailable/invalid response/output-limit failure and verify `StoredReport`, OperationRun, audit, and user-facing state fail closed.
- [x] T037 Simulate or observe storage write failure and verify no ready/downloadable artifact is exposed.
- [x] T038 Simulate or observe missing/zero-byte/corrupt PDF and verify the download route does not serve it as valid output.
- [x] T039 If any P0/P1 proof defect is confirmed, add the narrowest failing test before the runtime fix where practical.
- [x] T040 Fix only confirmed in-scope defects in existing PDF/report/ReviewPack/StoredReport/OperationRun/audit seams.
- [x] T041 If a fix appears to require a new renderer, report template, table/entity, lifecycle framework, queue architecture, navigation/surface, or product claim, stop and update spec/plan before continuing.
- [x] T042 After each fix, rerun the narrow tests that prove the defect is closed and record results in the runtime matrix.
## Phase 6 - Actual Staging/Dokploy Validation
- [x] T043 Confirm whether actual staging/Dokploy access is available and what can be inspected without exposing secrets.
- [ ] T044 Validate deployed renderer env/config: `TENANTPILOT_PDF_RENDERER_ENABLED`, `TENANTPILOT_PDF_RENDERER_RUNTIME_VALIDATED`, internal `TENANTPILOT_PDF_RENDERER_BASE_URL`, timeouts, limits, Gotenberg image/tag, service network, and public-port posture.
- [ ] T045 Validate Gotenberg `/health` and HTML conversion path from the deployed app/queue network.
- [ ] T046 Validate queue worker process, queue connection, timeout/retry settings, and ability to run `GenerateManagementReportPdfJob`.
- [ ] T047 Validate storage persistence and permissions for the private `exports` disk from both worker and web/download contexts.
- [ ] T048 Validate `APP_URL`/base URL and asset/CSS/logo/font behavior in generated PDF output.
- [ ] T049 Generate a non-secret staging Management Report PDF and repeat the PDF validation checks from Phase 4.
- [ ] T050 Validate staging download authorization as authorized actor and wrong/unauthorized actor without exposing credentials or private URLs in committed artifacts.
- [ ] T051 Check staging logs for renderer, job, storage, authorization, Livewire/Filament, and 500 errors relevant to the focused path; record only non-secret summaries.
- [x] T052 If actual staging/Dokploy validation cannot be completed, record exact remaining command/user-flow/environment proof needed and keep final gate no stronger than `PASS WITH CONDITIONS`.
## Phase 7 - Browser Proof and Human Product Sanity
- [x] T053 Run focused browser proof for the authorized Review Pack detail -> generate/blocked/ready -> download/open flow.
- [x] T054 Run or manually verify a wrong-workspace/unauthorized download path and record expected/actual response.
- [x] T055 Record route/surface, actor/role, workspace/environment, report state, expected result, actual result, console/runtime/network errors, and PDF validation result for each browser proof row.
- [x] T056 Complete Human Product Sanity for the generated PDF and owner action state: purpose, one dominant next action, technical detail demotion, canonical labels, complexity outcome, trust.
- [x] T057 Update UI coverage artifacts only if runtime UI/customer-output behavior materially changed; otherwise record checked no-update rationale.
## Phase 8 - Final Validation and Close-Out
- [x] T058 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec404` if Spec404 tests were added; record result.
- [x] T059 Rerun `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec379`; record result.
- [x] T060 Rerun `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec378`; record result.
- [x] T061 Run focused browser smoke command used in T053; record result.
- [x] T062 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`; record result.
- [x] T063 Run `git diff --check`; record result.
- [x] T064 Complete implementation-report fields: active spec/slice, branch/HEAD/dirty state, files changed, runtime UI files changed yes/no, UI impact, Product Surface exceptions, browser proof, Human Product Sanity, visible complexity, no-legacy, no completed-spec rewrite assertion, tests/checks, Livewire v4, provider registration, global search, high-impact action posture, asset strategy, deployment impact, unrelated failures, follow-up candidates.
- [x] T065 Set final Candidate Gate Result to `PASS`, `PASS WITH CONDITIONS`, or `FAIL` according to `spec.md`, with explicit production enablement status.
- [x] T066 If final result is `PASS`, state whether production-style PDF enablement may proceed and exactly which env/deploy change remains.
- [x] T067 If final result is `PASS WITH CONDITIONS` or `FAIL`, list remaining P0/P1 blockers or missing proof and the smallest next action.
## Non-Goals Checklist
- [x] NT001 Do not add a new PDF renderer, renderer package, Gotenberg service, or browser binary in Laravel containers.
- [x] NT002 Do not create new report templates, charts, report sections, customer claims, report center, navigation, scheduling, email delivery, customer portal, AI summary, or billing PDF scope.
- [x] NT003 Do not add governance artifact lifecycle/retention/export/delete/hold semantics.
- [x] NT004 Do not add JSON-to-JSONB migrations or broad data-layer hardening.
- [x] NT005 Do not run or claim a full browser/UX/runtime audit.
- [x] NT006 Do not commit generated PDFs, staging credentials, private URLs, secrets, customer data, or sensitive logs.
- [x] NT007 Do not normalize, uncheck, rewrite, or strip close-out/history from completed specs.
## Dependencies
- Specs 378-379 are read-only implementation baseline.
- Spec 403 evidence/currentness closure is predecessor context.
- Actual `PASS` depends on accessible staging/Dokploy validation or an accepted production-equivalent environment.
- Production enablement remains blocked until `TENANTPILOT_PDF_RENDERER_RUNTIME_VALIDATED=true` is justified by the runtime matrix.