From a0f376ae7da491914216550019cc3fe23082e7ac Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Tue, 23 Jun 2026 20:22:51 +0200 Subject: [PATCH] feat: finish management report PDF staging validation --- .../ManagementReportPdfDownloadController.php | 22 +- apps/platform/app/Models/StoredReport.php | 6 +- .../app/Services/Pdf/PdfRendererClient.php | 3 +- .../ManagementReportPdfService.php | 35 +- .../app/Support/Pdf/PdfByteValidator.php | 20 + .../factories/StoredReportFactory.php | 40 +- .../Spec379ManagementReportPdfSmokeTest.php | 7 +- .../Spec379ManagementReportPdfTest.php | 16 +- .../Spec392CustomerOutputRouteGateTest.php | 7 +- ...nagementReportPdfRuntimeValidationTest.php | 283 +++++++++++++ .../Pdf/Spec378PdfRenderingGatewayTest.php | 12 +- .../checklists/requirements.md | 94 +++++ .../implementation-report.md | 200 +++++++++ .../plan.md | 320 +++++++++++++++ .../spec.md | 378 ++++++++++++++++++ .../tasks.md | 143 +++++++ 16 files changed, 1551 insertions(+), 35 deletions(-) create mode 100644 apps/platform/app/Support/Pdf/PdfByteValidator.php create mode 100644 apps/platform/tests/Feature/ReviewPack/Spec404ManagementReportPdfRuntimeValidationTest.php create mode 100644 specs/404-management-report-pdf-staging-validation/checklists/requirements.md create mode 100644 specs/404-management-report-pdf-staging-validation/implementation-report.md create mode 100644 specs/404-management-report-pdf-staging-validation/plan.md create mode 100644 specs/404-management-report-pdf-staging-validation/spec.md create mode 100644 specs/404-management-report-pdf-staging-validation/tasks.md diff --git a/apps/platform/app/Http/Controllers/ManagementReportPdfDownloadController.php b/apps/platform/app/Http/Controllers/ManagementReportPdfDownloadController.php index f95b2602..f7c7dd62 100644 --- a/apps/platform/app/Http/Controllers/ManagementReportPdfDownloadController.php +++ b/apps/platform/app/Http/Controllers/ManagementReportPdfDownloadController.php @@ -11,12 +11,14 @@ use App\Services\Audit\WorkspaceAuditLogger; use App\Support\Audit\AuditActionId; use App\Support\Auth\Capabilities; +use App\Support\Pdf\PdfByteValidator; use App\Support\ReviewPacks\CustomerOutputGate; use App\Support\ReviewPackStatus; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Throwable; class ManagementReportPdfDownloadController extends Controller { @@ -67,8 +69,24 @@ public function __invoke(Request $request, StoredReport $storedReport): Streamed } $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; } @@ -96,7 +114,7 @@ public function __invoke(Request $request, StoredReport $storedReport): Streamed 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', 'X-Management-Report-PDF-SHA256' => $storedReport->sha256 ?? '', ]); diff --git a/apps/platform/app/Models/StoredReport.php b/apps/platform/app/Models/StoredReport.php index 7dfa8f37..56a2abf1 100644 --- a/apps/platform/app/Models/StoredReport.php +++ b/apps/platform/app/Models/StoredReport.php @@ -103,8 +103,10 @@ public function isReadyManagementPdf(): bool return $this->report_type === self::REPORT_TYPE_MANAGEMENT_REPORT_PDF && $this->report_format === self::FORMAT_PDF && $this->status === self::STATUS_READY - && filled($this->file_disk) - && filled($this->file_path); + && $this->file_disk === 'exports' + && filled($this->file_path) + && (int) $this->file_size > 0 + && filled($this->sha256); } public function artifactSourceDescriptor(): ArtifactSourceDescriptor diff --git a/apps/platform/app/Services/Pdf/PdfRendererClient.php b/apps/platform/app/Services/Pdf/PdfRendererClient.php index 4ba2a02d..4e04090c 100644 --- a/apps/platform/app/Services/Pdf/PdfRendererClient.php +++ b/apps/platform/app/Services/Pdf/PdfRendererClient.php @@ -4,6 +4,7 @@ namespace App\Services\Pdf; +use App\Support\Pdf\PdfByteValidator; use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\Response; use Illuminate\Support\Facades\Http; @@ -87,7 +88,7 @@ private function resultFromResponse(Response $response, ?string $fallbackCorrela $body = $response->body(); $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( PdfRenderResult::INVALID_RESPONSE, 'PDF renderer returned an invalid PDF response.', diff --git a/apps/platform/app/Services/ReviewPacks/ManagementReportPdfService.php b/apps/platform/app/Services/ReviewPacks/ManagementReportPdfService.php index 64cf23bc..aebb823d 100644 --- a/apps/platform/app/Services/ReviewPacks/ManagementReportPdfService.php +++ b/apps/platform/app/Services/ReviewPacks/ManagementReportPdfService.php @@ -16,6 +16,7 @@ use App\Support\Auth\Capabilities; use App\Support\OperationRunStatus; use App\Support\OperationRunType; +use App\Support\Pdf\PdfByteValidator; use App\Support\ReviewPacks\ManagementReportPdfPayloadBuilder; use App\Support\ReviewPacks\ManagementReportPdfRuntimeGate; use App\Support\ReviewPacks\ReportProfileRegistry; @@ -23,6 +24,7 @@ use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\URL; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Throwable; final class ManagementReportPdfService { @@ -297,11 +299,14 @@ public function findReadyReport(ReviewPack $reviewPack): ?StoredReport ->where('report_format', StoredReport::FORMAT_PDF) ->where('profile', ReportProfileRegistry::CUSTOMER_EXECUTIVE) ->where('status', StoredReport::STATUS_READY) - ->whereNotNull('file_disk') + ->where('file_disk', 'exports') ->whereNotNull('file_path') + ->where('file_size', '>', 0) + ->whereNotNull('sha256') ->latest('generated_at') ->latest('id') - ->first(); + ->get() + ->first(fn (StoredReport $report): bool => $this->hasReadablePdfArtifact($report)); } public function findActiveReport(ReviewPack $reviewPack): ?StoredReport @@ -343,6 +348,32 @@ private function findRetryableReport(ReviewPack $reviewPack, string $fingerprint ->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 { return hash('sha256', json_encode([ diff --git a/apps/platform/app/Support/Pdf/PdfByteValidator.php b/apps/platform/app/Support/Pdf/PdfByteValidator.php new file mode 100644 index 00000000..38fa6cd9 --- /dev/null +++ b/apps/platform/app/Support/Pdf/PdfByteValidator.php @@ -0,0 +1,20 @@ +state(fn (): array => [ - 'report_type' => StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF, - 'report_format' => StoredReport::FORMAT_PDF, - 'status' => StoredReport::STATUS_READY, - 'profile' => 'customer_executive', - 'payload' => array_replace_recursive([ - 'title' => 'Management report', + return $this->state(function () use ($payload): array { + $pdfBytes = "%PDF-1.7\n1 0 obj\n<< /Type /Catalog /Title (test) >>\nendobj\nstartxref\n0\n%%EOF"; + + return [ + 'report_type' => StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF, + 'report_format' => StoredReport::FORMAT_PDF, + 'status' => StoredReport::STATUS_READY, 'profile' => 'customer_executive', - 'chapters' => [], - 'provenance' => [ - 'generated_at' => now()->toIso8601String(), - ], - ], $payload), - 'file_disk' => 'exports', - 'file_path' => 'management-reports/test/report.pdf', - 'file_size' => 128, - 'sha256' => hash('sha256', '%PDF-1.7 test'), - 'generated_at' => now(), - ]); + 'payload' => array_replace_recursive([ + 'title' => 'Management report', + 'profile' => 'customer_executive', + 'chapters' => [], + 'provenance' => [ + 'generated_at' => now()->toIso8601String(), + ], + ], $payload), + 'file_disk' => 'exports', + 'file_path' => 'management-reports/test/report.pdf', + 'file_size' => strlen($pdfBytes), + 'sha256' => hash('sha256', $pdfBytes), + 'generated_at' => now(), + ]; + }); } } diff --git a/apps/platform/tests/Browser/Spec379ManagementReportPdfSmokeTest.php b/apps/platform/tests/Browser/Spec379ManagementReportPdfSmokeTest.php index 88617acb..ff62457e 100644 --- a/apps/platform/tests/Browser/Spec379ManagementReportPdfSmokeTest.php +++ b/apps/platform/tests/Browser/Spec379ManagementReportPdfSmokeTest.php @@ -122,7 +122,7 @@ function spec379BrowserCurrentReadyPack(): array function spec379BrowserReadyManagementPdf(ReviewPack $pack): StoredReport { $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()); 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 { $tempFile = tempnam(sys_get_temp_dir(), 'spec379-browser-pack-'); diff --git a/apps/platform/tests/Feature/ReviewPack/Spec379ManagementReportPdfTest.php b/apps/platform/tests/Feature/ReviewPack/Spec379ManagementReportPdfTest.php index 02cde080..d3a97df5 100644 --- a/apps/platform/tests/Feature/ReviewPack/Spec379ManagementReportPdfTest.php +++ b/apps/platform/tests/Feature/ReviewPack/Spec379ManagementReportPdfTest.php @@ -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 $summaryOverrides * @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 { - $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()); Storage::disk('exports')->put($filePath, $pdfBytes); @@ -259,6 +264,7 @@ function spec379ReadyManagementPdf(ReviewPack $pack): StoredReport $report = $result['report']; /** @var OperationRun $run */ $run = $result['operation_run']; + $pdfBytes = spec379PdfBytes('rendered spec379'); expect($run->type)->toBe(OperationRunType::ManagementReportGenerate->value) ->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()); 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', 'Gotenberg-Trace' => 'spec379-render', ]), @@ -283,7 +289,7 @@ function spec379ReadyManagementPdf(ReviewPack $pack): StoredReport expect($report->status)->toBe(StoredReport::STATUS_READY) ->and($report->file_disk)->toBe('exports') ->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($run->status)->toBe(OperationRunStatus::Completed->value) ->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value) @@ -442,7 +448,7 @@ function spec379ReadyManagementPdf(ReviewPack $pack): StoredReport ->push('renderer failed', 500, [ 'Content-Type' => 'text/plain', ]) - ->push('%PDF-1.7 rendered spec379 retry', 200, [ + ->push(spec379PdfBytes('rendered spec379 retry'), 200, [ 'Content-Type' => 'application/pdf', 'Gotenberg-Trace' => 'spec379-renderer-retry', ]), @@ -506,7 +512,7 @@ function spec379ReadyManagementPdf(ReviewPack $pack): StoredReport $run = $result['operation_run']; 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', 'Gotenberg-Trace' => 'spec379-storage-failure', ]), diff --git a/apps/platform/tests/Feature/ReviewPack/Spec392CustomerOutputRouteGateTest.php b/apps/platform/tests/Feature/ReviewPack/Spec392CustomerOutputRouteGateTest.php index 87ff1906..07f77956 100644 --- a/apps/platform/tests/Feature/ReviewPack/Spec392CustomerOutputRouteGateTest.php +++ b/apps/platform/tests/Feature/ReviewPack/Spec392CustomerOutputRouteGateTest.php @@ -84,7 +84,7 @@ function spec392RouteCurrentReviewPack(bool $customerSafeReady = true): array 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()); 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 { [$owner, $tenant, $review, $pack] = spec392RouteCurrentReviewPack(customerSafeReady: false); [$readonly] = createUserWithTenant( diff --git a/apps/platform/tests/Feature/ReviewPack/Spec404ManagementReportPdfRuntimeValidationTest.php b/apps/platform/tests/Feature/ReviewPack/Spec404ManagementReportPdfRuntimeValidationTest.php new file mode 100644 index 00000000..319108f8 --- /dev/null +++ b/apps/platform/tests/Feature/ReviewPack/Spec404ManagementReportPdfRuntimeValidationTest.php @@ -0,0 +1,283 @@ +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); +}); diff --git a/apps/platform/tests/Unit/Pdf/Spec378PdfRenderingGatewayTest.php b/apps/platform/tests/Unit/Pdf/Spec378PdfRenderingGatewayTest.php index f769b826..c07352af 100644 --- a/apps/platform/tests/Unit/Pdf/Spec378PdfRenderingGatewayTest.php +++ b/apps/platform/tests/Unit/Pdf/Spec378PdfRenderingGatewayTest.php @@ -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 { configureSpec378PdfRenderer(['enabled' => false]); @@ -113,9 +118,10 @@ function configureSpec378PdfRenderer(array $overrides = []): void it('renders server generated HTML through the configured Gotenberg route', function (): void { configureSpec378PdfRenderer(); + $pdfBytes = spec378PdfBytes('rendered'); 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-Disposition' => 'attachment; filename=management-report.pdf', 'Gotenberg-Trace' => 'spec378-render', @@ -133,7 +139,7 @@ function configureSpec378PdfRenderer(array $overrides = []): void )); 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->outputFilename)->toBe('management-report.pdf') ->and($result->correlationId)->toBe('spec378-render'); @@ -215,7 +221,7 @@ function configureSpec378PdfRenderer(array $overrides = []): void ]); 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', 'Gotenberg-Trace' => 'spec378-output', ]), diff --git a/specs/404-management-report-pdf-staging-validation/checklists/requirements.md b/specs/404-management-report-pdf-staging-validation/checklists/requirements.md new file mode 100644 index 00000000..313d83b5 --- /dev/null +++ b/specs/404-management-report-pdf-staging-validation/checklists/requirements.md @@ -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. diff --git a/specs/404-management-report-pdf-staging-validation/implementation-report.md b/specs/404-management-report-pdf-staging-validation/implementation-report.md new file mode 100644 index 00000000..67c8ade7 --- /dev/null +++ b/specs/404-management-report-pdf-staging-validation/implementation-report.md @@ -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. diff --git a/specs/404-management-report-pdf-staging-validation/plan.md b/specs/404-management-report-pdf-staging-validation/plan.md new file mode 100644 index 00000000..752764f5 --- /dev/null +++ b/specs/404-management-report-pdf-staging-validation/plan.md @@ -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. diff --git a/specs/404-management-report-pdf-staging-validation/spec.md b/specs/404-management-report-pdf-staging-validation/spec.md new file mode 100644 index 00000000..0dcd6777 --- /dev/null +++ b/specs/404-management-report-pdf-staging-validation/spec.md @@ -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? diff --git a/specs/404-management-report-pdf-staging-validation/tasks.md b/specs/404-management-report-pdf-staging-validation/tasks.md new file mode 100644 index 00000000..b3ccce50 --- /dev/null +++ b/specs/404-management-report-pdf-staging-validation/tasks.md @@ -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.