From d43ebcb4ee84b61be2d4f42fe39307897c50a9a7 Mon Sep 17 00:00:00 2001 From: ahmido Date: Sun, 14 Jun 2026 18:36:07 +0000 Subject: [PATCH] feat(report): implement management report pdf v1 (#449) Added PDF generation service for management reports as per Spec 378, including Gotenberg integration in docker-compose and configuration updates. Co-authored-by: Ahmed Darrazi Reviewed-on: https://git.cloudarix.de/ahmido/TenantAtlas/pulls/449 --- apps/platform/.env.example | 16 + .../app/Services/Pdf/PdfRenderRequest.php | 20 + .../app/Services/Pdf/PdfRenderResult.php | 84 ++++ .../app/Services/Pdf/PdfRendererClient.php | 201 ++++++++ .../app/Services/Pdf/PdfRenderingGateway.php | 114 +++++ apps/platform/config/tenantpilot.php | 15 + .../Pdf/Spec378PdfRenderingGatewayTest.php | 231 ++++++++++ docker-compose.yml | 32 ++ docs/deployment-checklist.md | 13 + docs/package-governance.md | 19 +- ...ec378-constitution-renderer-gate-review.md | 105 +++++ .../spec378-gotenberg-security-controls.md | 87 ++++ .../spec378-pdf-renderer-decision-matrix.md | 83 ++++ .../checklists/requirements.md | 59 +++ specs/378-management-report-pdf-v1/plan.md | 434 ++++++++++++++++++ specs/378-management-report-pdf-v1/spec.md | 421 +++++++++++++++++ specs/378-management-report-pdf-v1/tasks.md | 221 +++++++++ 17 files changed, 2154 insertions(+), 1 deletion(-) create mode 100644 apps/platform/app/Services/Pdf/PdfRenderRequest.php create mode 100644 apps/platform/app/Services/Pdf/PdfRenderResult.php create mode 100644 apps/platform/app/Services/Pdf/PdfRendererClient.php create mode 100644 apps/platform/app/Services/Pdf/PdfRenderingGateway.php create mode 100644 apps/platform/tests/Unit/Pdf/Spec378PdfRenderingGatewayTest.php create mode 100644 specs/378-management-report-pdf-v1/artifacts/spec378-constitution-renderer-gate-review.md create mode 100644 specs/378-management-report-pdf-v1/artifacts/spec378-gotenberg-security-controls.md create mode 100644 specs/378-management-report-pdf-v1/artifacts/spec378-pdf-renderer-decision-matrix.md create mode 100644 specs/378-management-report-pdf-v1/checklists/requirements.md create mode 100644 specs/378-management-report-pdf-v1/plan.md create mode 100644 specs/378-management-report-pdf-v1/spec.md create mode 100644 specs/378-management-report-pdf-v1/tasks.md diff --git a/apps/platform/.env.example b/apps/platform/.env.example index 4759a485..76cb5a72 100644 --- a/apps/platform/.env.example +++ b/apps/platform/.env.example @@ -59,6 +59,22 @@ MAIL_PASSWORD=null MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_NAME="${APP_NAME}" +# Internal PDF renderer (Spec 378) +TENANTPILOT_PDF_RENDERER_ENABLED=true +TENANTPILOT_PDF_RENDERER_BASE_URL=http://gotenberg:3000 +TENANTPILOT_PDF_RENDERER_TIMEOUT_SECONDS=30 +TENANTPILOT_PDF_RENDERER_CONNECT_TIMEOUT_SECONDS=5 +TENANTPILOT_PDF_RENDERER_MAX_HTML_BYTES=1048576 +TENANTPILOT_PDF_RENDERER_MAX_ASSET_BYTES=2097152 +TENANTPILOT_PDF_RENDERER_MAX_OUTPUT_BYTES=10485760 +TENANTPILOT_PDF_RENDERER_CORRELATION_HEADER=Gotenberg-Trace +GOTENBERG_API_TIMEOUT=30s +GOTENBERG_API_BODY_LIMIT=10MB +GOTENBERG_API_CORRELATION_ID_HEADER=Gotenberg-Trace +GOTENBERG_CHROMIUM_START_TIMEOUT=20s +GOTENBERG_CHROMIUM_MAX_QUEUE_SIZE=10 +GOTENBERG_CHROMIUM_MAX_CONCURRENCY=2 + SUPPORT_DESK_ENABLED=false SUPPORT_DESK_NAME="External support desk" SUPPORT_DESK_CREATE_URL= diff --git a/apps/platform/app/Services/Pdf/PdfRenderRequest.php b/apps/platform/app/Services/Pdf/PdfRenderRequest.php new file mode 100644 index 00000000..f38e3f48 --- /dev/null +++ b/apps/platform/app/Services/Pdf/PdfRenderRequest.php @@ -0,0 +1,20 @@ + $assets + * @param array $options + */ + public function __construct( + public readonly string $html, + public readonly array $assets = [], + public readonly array $options = [], + public readonly ?string $correlationId = null, + public readonly ?string $outputFilename = null, + ) {} +} diff --git a/apps/platform/app/Services/Pdf/PdfRenderResult.php b/apps/platform/app/Services/Pdf/PdfRenderResult.php new file mode 100644 index 00000000..da5254d6 --- /dev/null +++ b/apps/platform/app/Services/Pdf/PdfRenderResult.php @@ -0,0 +1,84 @@ +successful; + } + + public function failed(): bool + { + return ! $this->successful; + } +} diff --git a/apps/platform/app/Services/Pdf/PdfRendererClient.php b/apps/platform/app/Services/Pdf/PdfRendererClient.php new file mode 100644 index 00000000..4ba2a02d --- /dev/null +++ b/apps/platform/app/Services/Pdf/PdfRendererClient.php @@ -0,0 +1,201 @@ +timeoutSeconds()) + ->connectTimeout($this->connectTimeoutSeconds()) + ->withHeaders($this->correlationHeaders($correlationId)) + ->get($this->url($this->configString('health_path', '/health'))); + } catch (ConnectionException) { + return PdfRenderResult::failure( + PdfRenderResult::RENDERER_UNAVAILABLE, + 'PDF renderer is unavailable.', + correlationId: $correlationId, + ); + } + + if ($response->successful()) { + return PdfRenderResult::success( + pdfBytes: '', + contentType: (string) $response->header('Content-Type', 'text/plain'), + correlationId: $this->responseCorrelationId($response, $correlationId), + statusCode: $response->status(), + ); + } + + return PdfRenderResult::failure( + PdfRenderResult::RENDERER_UNAVAILABLE, + 'PDF renderer health check failed.', + statusCode: $response->status(), + correlationId: $this->responseCorrelationId($response, $correlationId), + ); + } + + public function render(PdfRenderRequest $request): PdfRenderResult + { + try { + $pending = Http::timeout($this->timeoutSeconds()) + ->connectTimeout($this->connectTimeoutSeconds()) + ->accept('application/pdf') + ->withHeaders(array_filter(array_merge( + $this->correlationHeaders($request->correlationId), + [ + 'Gotenberg-Output-Filename' => $request->outputFilename + ?? $this->configString('output_filename', 'tenantpilot-management-report'), + ], + ))); + + $pending->attach('files', $request->html, 'index.html', [ + 'Content-Type' => 'text/html; charset=UTF-8', + ]); + + foreach ($request->assets as $filename => $contents) { + $pending->attach('files', $contents, $filename); + } + + $response = $pending->post( + $this->url($this->configString('html_route', '/forms/chromium/convert/html')), + $this->stringFormOptions($request->options), + ); + } catch (ConnectionException) { + return PdfRenderResult::failure( + PdfRenderResult::RENDERER_UNAVAILABLE, + 'PDF renderer did not respond before the configured timeout or was unavailable.', + correlationId: $request->correlationId, + ); + } + + return $this->resultFromResponse($response, $request->correlationId); + } + + private function resultFromResponse(Response $response, ?string $fallbackCorrelationId): PdfRenderResult + { + $correlationId = $this->responseCorrelationId($response, $fallbackCorrelationId); + + if ($response->successful()) { + $body = $response->body(); + $contentType = (string) $response->header('Content-Type', 'application/pdf'); + + if ($body === '' || (! str_contains(strtolower($contentType), 'pdf') && ! str_starts_with($body, '%PDF'))) { + return PdfRenderResult::failure( + PdfRenderResult::INVALID_RESPONSE, + 'PDF renderer returned an invalid PDF response.', + statusCode: $response->status(), + correlationId: $correlationId, + ); + } + + return PdfRenderResult::success( + pdfBytes: $body, + contentType: $contentType, + outputFilename: $this->filenameFromDisposition((string) $response->header('Content-Disposition', '')), + correlationId: $correlationId, + statusCode: $response->status(), + ); + } + + if ($response->status() === 400) { + return PdfRenderResult::failure( + PdfRenderResult::INVALID_REQUEST, + 'PDF renderer rejected the HTML render request.', + statusCode: $response->status(), + correlationId: $correlationId, + ); + } + + if ($response->status() === 503) { + return PdfRenderResult::failure( + PdfRenderResult::RENDERER_TIMEOUT, + 'PDF renderer did not complete within the configured timeout.', + statusCode: $response->status(), + correlationId: $correlationId, + ); + } + + return PdfRenderResult::failure( + PdfRenderResult::RENDERER_FAILED, + 'PDF renderer failed to create the document.', + statusCode: $response->status(), + correlationId: $correlationId, + ); + } + + /** + * @param array $options + * @return array + */ + private function stringFormOptions(array $options): array + { + $formatted = []; + + foreach ($options as $key => $value) { + $formatted[$key] = is_bool($value) ? ($value ? 'true' : 'false') : (string) $value; + } + + return $formatted; + } + + /** + * @return array + */ + private function correlationHeaders(?string $correlationId): array + { + if ($correlationId === null || trim($correlationId) === '') { + return []; + } + + return [ + $this->configString('correlation_header', 'Gotenberg-Trace') => trim($correlationId), + ]; + } + + private function responseCorrelationId(Response $response, ?string $fallback): ?string + { + $header = $this->configString('correlation_header', 'Gotenberg-Trace'); + $value = $response->header($header); + + return is_string($value) && trim($value) !== '' ? trim($value) : $fallback; + } + + private function url(string $path): string + { + return rtrim($this->configString('base_url', 'http://gotenberg:3000'), '/').'/'.ltrim($path, '/'); + } + + private function timeoutSeconds(): int + { + return max(1, (int) config('tenantpilot.pdf_renderer.timeout_seconds', 30)); + } + + private function connectTimeoutSeconds(): int + { + return max(1, (int) config('tenantpilot.pdf_renderer.connect_timeout_seconds', 5)); + } + + private function configString(string $key, string $default): string + { + $value = config('tenantpilot.pdf_renderer.'.$key, $default); + + return is_string($value) && trim($value) !== '' ? trim($value) : $default; + } + + private function filenameFromDisposition(string $disposition): ?string + { + if (preg_match('/filename="?([^";]+)"?/i', $disposition, $matches) !== 1) { + return null; + } + + return basename($matches[1]); + } +} diff --git a/apps/platform/app/Services/Pdf/PdfRenderingGateway.php b/apps/platform/app/Services/Pdf/PdfRenderingGateway.php new file mode 100644 index 00000000..2245a996 --- /dev/null +++ b/apps/platform/app/Services/Pdf/PdfRenderingGateway.php @@ -0,0 +1,114 @@ +isEnabled()) { + return PdfRenderResult::failure( + PdfRenderResult::RENDERER_DISABLED, + 'PDF renderer is disabled.', + correlationId: $correlationId, + ); + } + + return $this->client->healthCheck($correlationId); + } + + public function renderHtml(PdfRenderRequest $request): PdfRenderResult + { + if (! $this->isEnabled()) { + return PdfRenderResult::failure( + PdfRenderResult::RENDERER_DISABLED, + 'PDF renderer is disabled.', + correlationId: $request->correlationId, + ); + } + + if (trim($request->html) === '') { + return PdfRenderResult::failure( + PdfRenderResult::INVALID_REQUEST, + 'PDF renderer requires server-generated HTML.', + correlationId: $request->correlationId, + ); + } + + if (strlen($request->html) > $this->maxHtmlBytes()) { + return PdfRenderResult::failure( + PdfRenderResult::PAYLOAD_TOO_LARGE, + 'PDF HTML payload exceeds the configured size limit.', + correlationId: $request->correlationId, + ); + } + + foreach ($request->assets as $filename => $contents) { + if (! $this->isFlatAssetFilename((string) $filename)) { + return PdfRenderResult::failure( + PdfRenderResult::INVALID_ASSET, + 'PDF assets must use flat relative filenames.', + correlationId: $request->correlationId, + ); + } + + if (strlen($contents) > $this->maxAssetBytes()) { + return PdfRenderResult::failure( + PdfRenderResult::PAYLOAD_TOO_LARGE, + 'PDF asset payload exceeds the configured size limit.', + correlationId: $request->correlationId, + ); + } + } + + $result = $this->client->render($request); + + if ($result->successful() && $result->pdfBytes !== null && strlen($result->pdfBytes) > $this->maxOutputBytes()) { + return PdfRenderResult::failure( + PdfRenderResult::OUTPUT_TOO_LARGE, + 'PDF renderer output exceeds the configured size limit.', + statusCode: $result->statusCode, + correlationId: $result->correlationId, + ); + } + + return $result; + } + + private function isEnabled(): bool + { + return (bool) config('tenantpilot.pdf_renderer.enabled', false) + && config('tenantpilot.pdf_renderer.driver', 'gotenberg') === 'gotenberg'; + } + + private function maxHtmlBytes(): int + { + return max(1, (int) config('tenantpilot.pdf_renderer.max_html_bytes', 1024 * 1024)); + } + + private function maxAssetBytes(): int + { + return max(1, (int) config('tenantpilot.pdf_renderer.max_asset_bytes', 2 * 1024 * 1024)); + } + + private function maxOutputBytes(): int + { + return max(1, (int) config('tenantpilot.pdf_renderer.max_output_bytes', 10 * 1024 * 1024)); + } + + private function isFlatAssetFilename(string $filename): bool + { + $filename = trim($filename); + + return $filename !== '' + && $filename === basename($filename) + && ! str_contains($filename, "\0") + && $filename !== 'index.html'; + } +} diff --git a/apps/platform/config/tenantpilot.php b/apps/platform/config/tenantpilot.php index 41073397..bc8a1438 100644 --- a/apps/platform/config/tenantpilot.php +++ b/apps/platform/config/tenantpilot.php @@ -10,6 +10,21 @@ 'max_ttl_minutes' => (int) env('TENANTPILOT_SUPPORT_ACCESS_MAX_TTL_MINUTES', 240), ], + 'pdf_renderer' => [ + 'enabled' => (bool) env('TENANTPILOT_PDF_RENDERER_ENABLED', false), + 'driver' => env('TENANTPILOT_PDF_RENDERER_DRIVER', 'gotenberg'), + 'base_url' => env('TENANTPILOT_PDF_RENDERER_BASE_URL', 'http://gotenberg:3000'), + 'health_path' => env('TENANTPILOT_PDF_RENDERER_HEALTH_PATH', '/health'), + 'html_route' => env('TENANTPILOT_PDF_RENDERER_HTML_ROUTE', '/forms/chromium/convert/html'), + 'timeout_seconds' => (int) env('TENANTPILOT_PDF_RENDERER_TIMEOUT_SECONDS', 30), + 'connect_timeout_seconds' => (int) env('TENANTPILOT_PDF_RENDERER_CONNECT_TIMEOUT_SECONDS', 5), + 'max_html_bytes' => (int) env('TENANTPILOT_PDF_RENDERER_MAX_HTML_BYTES', 1024 * 1024), + 'max_asset_bytes' => (int) env('TENANTPILOT_PDF_RENDERER_MAX_ASSET_BYTES', 2 * 1024 * 1024), + 'max_output_bytes' => (int) env('TENANTPILOT_PDF_RENDERER_MAX_OUTPUT_BYTES', 10 * 1024 * 1024), + 'correlation_header' => env('TENANTPILOT_PDF_RENDERER_CORRELATION_HEADER', 'Gotenberg-Trace'), + 'output_filename' => env('TENANTPILOT_PDF_RENDERER_OUTPUT_FILENAME', 'tenantpilot-management-report'), + ], + 'system_console' => [ 'stuck_thresholds' => [ 'queued_minutes' => (int) env('TENANTPILOT_SYSTEM_CONSOLE_STUCK_QUEUED_MINUTES', 15), diff --git a/apps/platform/tests/Unit/Pdf/Spec378PdfRenderingGatewayTest.php b/apps/platform/tests/Unit/Pdf/Spec378PdfRenderingGatewayTest.php new file mode 100644 index 00000000..15680b66 --- /dev/null +++ b/apps/platform/tests/Unit/Pdf/Spec378PdfRenderingGatewayTest.php @@ -0,0 +1,231 @@ + array_merge([ + 'enabled' => 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, + 'max_asset_bytes' => 512, + 'max_output_bytes' => 2048, + 'correlation_header' => 'Gotenberg-Trace', + 'output_filename' => 'tenantpilot-management-report', + ], $overrides), + ]); +} + +it('keeps the renderer disabled without issuing outbound HTTP', function (): void { + configureSpec378PdfRenderer(['enabled' => false]); + + Http::fake(); + + $result = app(PdfRenderingGateway::class)->renderHtml(new PdfRenderRequest( + html: 'Management report', + correlationId: 'spec378-disabled', + )); + + expect($result->failed())->toBeTrue() + ->and($result->failureCode)->toBe(PdfRenderResult::RENDERER_DISABLED) + ->and($result->safeMessage)->toBe('PDF renderer is disabled.') + ->and($result->correlationId)->toBe('spec378-disabled'); + + Http::assertNothingSent(); +}); + +it('checks Gotenberg health with the configured correlation header', function (): void { + configureSpec378PdfRenderer(); + + Http::fake([ + 'gotenberg.test/health' => Http::response('OK', 200, [ + 'Content-Type' => 'text/plain', + 'Gotenberg-Trace' => 'spec378-health', + ]), + ]); + + $result = app(PdfRenderingGateway::class)->healthCheck('spec378-health'); + + expect($result->successful())->toBeTrue() + ->and($result->correlationId)->toBe('spec378-health'); + + Http::assertSent(fn (Request $request): bool => $request->method() === 'GET' + && $request->url() === 'http://gotenberg.test/health' + && (($request->headers()['Gotenberg-Trace'][0] ?? null) === 'spec378-health')); +}); + +it('keeps the Sail Gotenberg service pinned and internal only', function (): void { + $compose = file_get_contents(repo_path('docker-compose.yml')); + $compose = is_string($compose) ? $compose : ''; + $gotenbergBlock = Str::between($compose, " gotenberg:\n", "\n pgsql:"); + + expect($gotenbergBlock)->toContain("image: 'gotenberg/gotenberg:8.34.0-chromium'") + ->and($gotenbergBlock)->toContain("API_DISABLE_DOWNLOAD_FROM: 'true'") + ->and($gotenbergBlock)->toContain("WEBHOOK_DISABLE: 'true'") + ->and($gotenbergBlock)->toContain("CHROMIUM_ALLOW_LIST: '^$'") + ->and($gotenbergBlock)->toContain("CHROMIUM_DENY_PRIVATE_IPS: 'true'") + ->and($gotenbergBlock)->toContain("CHROMIUM_DENY_PUBLIC_IPS: 'true'") + ->and($gotenbergBlock)->not->toContain("\n ports:"); +}); + +it('rejects oversized HTML and unsafe asset names before the renderer call', function (): void { + configureSpec378PdfRenderer([ + 'max_html_bytes' => 8, + ]); + + Http::fake(); + + $tooLarge = app(PdfRenderingGateway::class)->renderHtml(new PdfRenderRequest( + html: 'too large', + correlationId: 'spec378-size', + )); + + expect($tooLarge->failed())->toBeTrue() + ->and($tooLarge->failureCode)->toBe(PdfRenderResult::PAYLOAD_TOO_LARGE); + + configureSpec378PdfRenderer(); + + $invalidAsset = app(PdfRenderingGateway::class)->renderHtml(new PdfRenderRequest( + html: 'Management report', + assets: ['assets/logo.png' => 'image-bytes'], + correlationId: 'spec378-asset', + )); + + expect($invalidAsset->failed())->toBeTrue() + ->and($invalidAsset->failureCode)->toBe(PdfRenderResult::INVALID_ASSET); + + Http::assertNothingSent(); +}); + +it('renders server generated HTML through the configured Gotenberg route', function (): void { + configureSpec378PdfRenderer(); + + Http::fake([ + 'gotenberg.test/forms/chromium/convert/html' => Http::response('%PDF-1.7 rendered', 200, [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => 'attachment; filename=management-report.pdf', + 'Gotenberg-Trace' => 'spec378-render', + ]), + ]); + + $result = app(PdfRenderingGateway::class)->renderHtml(new PdfRenderRequest( + html: 'Management report', + assets: ['logo.png' => 'image-bytes'], + options: [ + 'printBackground' => true, + 'paperWidth' => '8.27', + ], + correlationId: 'spec378-render', + )); + + expect($result->successful())->toBeTrue() + ->and($result->pdfBytes)->toBe('%PDF-1.7 rendered') + ->and($result->contentType)->toBe('application/pdf') + ->and($result->outputFilename)->toBe('management-report.pdf') + ->and($result->correlationId)->toBe('spec378-render'); + + Http::assertSent(fn (Request $request): bool => $request->method() === 'POST' + && $request->url() === 'http://gotenberg.test/forms/chromium/convert/html' + && (($request->headers()['Gotenberg-Trace'][0] ?? null) === 'spec378-render') + && (($request->headers()['Gotenberg-Output-Filename'][0] ?? null) === 'tenantpilot-management-report')); +}); + +it('maps renderer unavailable failures to safe results', function (): void { + configureSpec378PdfRenderer([ + 'base_url' => 'http://gotenberg-unavailable.test', + ]); + + Http::fake([ + 'gotenberg-unavailable.test/forms/chromium/convert/html' => Http::failedConnection(), + ]); + + $unavailable = app(PdfRenderingGateway::class)->renderHtml(new PdfRenderRequest( + html: 'Management report', + correlationId: 'spec378-unavailable', + )); + + expect($unavailable->failed())->toBeTrue() + ->and($unavailable->failureCode)->toBe(PdfRenderResult::RENDERER_UNAVAILABLE) + ->and($unavailable->safeMessage)->not->toContain('SQLSTATE', 'access token', 'client secret', 'stack trace'); +}); + +it('maps invalid renderer responses to safe results', function (): void { + configureSpec378PdfRenderer([ + 'base_url' => 'http://gotenberg-invalid.test', + ]); + + Http::fake([ + 'gotenberg-invalid.test/forms/chromium/convert/html' => Http::response('not a pdf', 200, [ + 'Content-Type' => 'text/plain', + 'Gotenberg-Trace' => 'spec378-invalid', + ]), + ]); + + $invalid = app(PdfRenderingGateway::class)->renderHtml(new PdfRenderRequest( + html: 'Management report', + correlationId: 'spec378-invalid', + )); + + expect($invalid->failed())->toBeTrue() + ->and($invalid->failureCode)->toBe(PdfRenderResult::INVALID_RESPONSE) + ->and($invalid->correlationId)->toBe('spec378-invalid'); +}); + +it('maps renderer HTTP failures and output limits to safe result codes', function (int $status, string $expectedCode): void { + configureSpec378PdfRenderer(); + + Http::fake([ + 'gotenberg.test/forms/chromium/convert/html' => Http::response('SQLSTATE raw renderer body', $status, [ + 'Content-Type' => 'text/plain', + 'Gotenberg-Trace' => 'spec378-failure', + ]), + ]); + + $result = app(PdfRenderingGateway::class)->renderHtml(new PdfRenderRequest( + html: 'Management report', + correlationId: 'spec378-failure', + )); + + expect($result->failed())->toBeTrue() + ->and($result->failureCode)->toBe($expectedCode) + ->and($result->safeMessage)->not->toContain('SQLSTATE'); +})->with([ + 'invalid request' => [400, PdfRenderResult::INVALID_REQUEST], + 'timeout' => [503, PdfRenderResult::RENDERER_TIMEOUT], + 'unexpected renderer failure' => [500, PdfRenderResult::RENDERER_FAILED], +]); + +it('rejects oversized PDF output after a successful renderer response', function (): void { + configureSpec378PdfRenderer([ + 'max_output_bytes' => 10, + ]); + + Http::fake([ + 'gotenberg.test/forms/chromium/convert/html' => Http::response('%PDF-1.7 document body', 200, [ + 'Content-Type' => 'application/pdf', + 'Gotenberg-Trace' => 'spec378-output', + ]), + ]); + + $result = app(PdfRenderingGateway::class)->renderHtml(new PdfRenderRequest( + html: 'Management report', + correlationId: 'spec378-output', + )); + + expect($result->failed())->toBeTrue() + ->and($result->failureCode)->toBe(PdfRenderResult::OUTPUT_TOO_LARGE) + ->and($result->correlationId)->toBe('spec378-output'); +}); diff --git a/docker-compose.yml b/docker-compose.yml index cd60f661..3548af92 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: depends_on: - pgsql - redis + - gotenberg queue: build: @@ -62,8 +63,39 @@ services: - laravel.test - pgsql - redis + - gotenberg command: php artisan queue:listen --tries=3 --timeout=300 --sleep=3 + gotenberg: + image: 'gotenberg/gotenberg:8.34.0-chromium' + environment: + API_TIMEOUT: '${GOTENBERG_API_TIMEOUT:-30s}' + API_BODY_LIMIT: '${GOTENBERG_API_BODY_LIMIT:-10MB}' + API_CORRELATION_ID_HEADER: '${GOTENBERG_API_CORRELATION_ID_HEADER:-Gotenberg-Trace}' + API_DISABLE_DOWNLOAD_FROM: 'true' + API_ENABLE_DEBUG_ROUTE: 'false' + CHROMIUM_AUTO_START: 'true' + CHROMIUM_START_TIMEOUT: '${GOTENBERG_CHROMIUM_START_TIMEOUT:-20s}' + CHROMIUM_MAX_QUEUE_SIZE: '${GOTENBERG_CHROMIUM_MAX_QUEUE_SIZE:-10}' + CHROMIUM_MAX_CONCURRENCY: '${GOTENBERG_CHROMIUM_MAX_CONCURRENCY:-2}' + CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES: 'false' + CHROMIUM_ALLOW_LIST: '^$' + CHROMIUM_DENY_PRIVATE_IPS: 'true' + CHROMIUM_DENY_PUBLIC_IPS: 'true' + WEBHOOK_DISABLE: 'true' + WEBHOOK_DENY_PRIVATE_IPS: 'true' + WEBHOOK_DENY_PUBLIC_IPS: 'true' + API_DOWNLOAD_FROM_DENY_PRIVATE_IPS: 'true' + API_DOWNLOAD_FROM_DENY_PUBLIC_IPS: 'true' + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/health >/dev/null 2>&1 || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + networks: + - sail + pgsql: image: 'postgres:16' ports: diff --git a/docs/deployment-checklist.md b/docs/deployment-checklist.md index 3521a888..f4960ab6 100644 --- a/docs/deployment-checklist.md +++ b/docs/deployment-checklist.md @@ -52,6 +52,19 @@ ## Queue Worker Checklist - Track queue depth and failed jobs. - Run destructive restore/backups in separate queues when volume grows. +## PDF Renderer Checklist + +Spec 378 approves Gotenberg 8 Chromium only as an internal renderer service for report-style PDFs. + +- Use a pinned `gotenberg/gotenberg:8.34.0-chromium` image or an explicitly reviewed immutable digest. +- Keep the renderer on the internal Dokploy/container network only; do not publish a public port. +- Configure `/health` monitoring for the renderer service. +- Set `TENANTPILOT_PDF_RENDERER_BASE_URL` to the internal service URL, for example `http://gotenberg:3000`. +- Set explicit renderer timeouts and limits: `TENANTPILOT_PDF_RENDERER_TIMEOUT_SECONDS`, `TENANTPILOT_PDF_RENDERER_CONNECT_TIMEOUT_SECONDS`, `TENANTPILOT_PDF_RENDERER_MAX_HTML_BYTES`, `TENANTPILOT_PDF_RENDERER_MAX_ASSET_BYTES`, and `TENANTPILOT_PDF_RENDERER_MAX_OUTPUT_BYTES`. +- Set matching Gotenberg service controls: `API_TIMEOUT`, `API_BODY_LIMIT`, `API_CORRELATION_ID_HEADER`, `CHROMIUM_START_TIMEOUT`, `CHROMIUM_MAX_QUEUE_SIZE`, and `CHROMIUM_MAX_CONCURRENCY`. +- Keep `API_DISABLE_DOWNLOAD_FROM=true`, `WEBHOOK_DISABLE=true`, `CHROMIUM_ALLOW_LIST=^$`, `CHROMIUM_DENY_PRIVATE_IPS=true`, and `CHROMIUM_DENY_PUBLIC_IPS=true` unless a later spec approves external asset fetches. +- Do not install Node, Puppeteer, Chrome, Chromium, or browser binaries in Laravel web or queue containers for production PDF rendering. + ## Scheduler Checklist - One scheduler instance per environment. diff --git a/docs/package-governance.md b/docs/package-governance.md index 8d33005c..cfa35a50 100644 --- a/docs/package-governance.md +++ b/docs/package-governance.md @@ -1,6 +1,6 @@ # TenantPilot Package Governance -Status: 2026-05-15 +Status: 2026-06-14 Applies to: Composer, pnpm workspace, Filament plugins, Laravel packages, frontend tooling. ## Policy @@ -40,6 +40,23 @@ ## Approved Packages - Tailwind CSS v4 and `@tailwindcss/vite`. - Drizzle tooling for local PostgreSQL workflows when repo scripts require it. +## Approved Runtime Services / PDF Rendering + +### Gotenberg 8 Chromium internal service + +- **Decision**: approved with controls for Spec 378 and future report-style PDF rendering. +- **Scope**: internal PDF rendering infrastructure for server-generated report documents. +- **Runtime model**: separate Docker service reachable only from TenantPilot application/worker services over the internal deployment network. +- **Required integration boundary**: Laravel must call the service through a narrow `PdfRenderingGateway` / `PdfRendererClient`; production PDF generation must not install or execute Node, Puppeteer, Chrome, Chromium, or browser binaries in the Laravel app/queue containers. +- **Required image policy**: pin an explicit Gotenberg 8 Chromium image tag or immutable digest; never use `latest`. +- **Required controls**: internal network only, no public port, health check, explicit timeouts, request/output size limits, no user-provided URL rendering in v1, server-generated HTML payloads only, no signed URLs/secrets/raw provider payloads in HTML, outbound URL access denied or tightly restricted, structured renderer error mapping, and safe OperationRun/audit correlation. +- **Not approved for**: legal invoice generation, German B2B e-invoicing, XRechnung, ZUGFeRD/Factur-X, GoBD archival, tax calculation, invoice numbering, or billing compliance. +- **Owner**: Platform/runtime governance. +- **Rationale**: keeps Chromium isolated from Laravel runtime containers while preserving modern HTML/CSS rendering quality for customer-facing management reports. +- **Review date**: TODO before first production deployment that enables PDF generation, and at least once per major/minor Gotenberg upgrade. +- **Upgrade/patching expectation**: patch within the approved Gotenberg 8 line on security or Chromium base-image advisories; major-version upgrades require a spec/plan update, renderer smoke tests, and staging validation. +- **Spec decision record**: `specs/378-management-report-pdf-v1/artifacts/spec378-pdf-renderer-decision-matrix.md` and `specs/378-management-report-pdf-v1/artifacts/spec378-gotenberg-security-controls.md`. + ## Packages Under Review - `socialiteproviders/microsoft-azure` 4.x to 5.x. diff --git a/specs/378-management-report-pdf-v1/artifacts/spec378-constitution-renderer-gate-review.md b/specs/378-management-report-pdf-v1/artifacts/spec378-constitution-renderer-gate-review.md new file mode 100644 index 00000000..986a4f7c --- /dev/null +++ b/specs/378-management-report-pdf-v1/artifacts/spec378-constitution-renderer-gate-review.md @@ -0,0 +1,105 @@ +# Spec 378 Constitution Renderer Gate Review + +Date: 2026-06-14 +Review type: renderer/package/runtime governance gate. +Decision: renderer gate approved with controls; implementation may proceed after a scoped runtime config task. + +## Repo Safety + +- Branch: `378-management-report-pdf-v1` +- HEAD: `f1eadadf docs: add spec 377 post-productization browser reaudit closeout gate (#448)` +- Dirty files before governance update: untracked `specs/378-management-report-pdf-v1/` +- Staged files before governance update: none observed +- Untracked files before governance update: `specs/378-management-report-pdf-v1/` +- `git diff --name-only` before governance update: no tracked-file diff +- Spec 378 files already changed before governance update: yes, active Spec 378 directory existed as untracked content +- Other specs affected before governance update: none observed + +## Runtime Surface Read + +- `docker-compose.yml`: present at repo root; contains `laravel.test`, `queue`, `pgsql`, `redis`, and a `sail` bridge network. +- `apps/platform/docker-compose.yml`: not present. +- `apps/platform/Dockerfile`: not present. +- `apps/platform/docker/`: not present. +- `apps/platform/composer.json`: no runtime PDF renderer package. +- `apps/platform/package.json`: `playwright` appears under `devDependencies`; no production PDF runtime is declared. + +## Gate State + +Result: gate can be updated with approved renderer. + +- Previous gate stop was valid. +- Missing renderer evidence was documented in Spec 378. +- Downstream implementation tasks remain open. +- No tracked app-code, runtime compose, Composer, npm, StoredReport, OperationRun, UI, or template implementation was started. +- Gotenberg is not currently deployed or configured in the repo. +- This review approves the renderer architecture only; runtime config remains pending. + +## Constitution Alignment + +| Area | Result | Review | +|---|---|---| +| Package Governance | PASS with controls | Gotenberg is a runtime service, not a Composer/npm package. The decision is documented in Spec 378 artifacts and `docs/package-governance.md`, with pinning, security, maintenance, and review-date controls. | +| Security | PASS with controls | The approved model keeps the browser runtime out of Laravel containers, internal-only, no public port, server-generated HTML only, no signed URLs/secrets in HTML, outbound access restricted, and safe error mapping required. | +| RBAC / Isolation | PASS | Renderer selection does not weaken RBAC. Generation/download still require workspace, tenant, managed-environment, and capability checks before renderer invocation or artifact return. | +| Auditability | PASS | The renderer gateway must pass correlation/OperationRun metadata and map success/failure into OperationRun and audit records without raw renderer errors. | +| Proportionality / BLOAT | PASS | A separate internal renderer service plus narrow Laravel gateway is justified by customer-facing PDF artifact quality, queue failure isolation, and avoiding browser binaries in the app container. It is narrower than a generic report engine. | +| No Premature Abstraction | PASS with controls | Only `PdfRenderingGateway` / `PdfRendererClient` is approved as shared infrastructure. Domain generators remain separate; no report center, billing framework, or generic document engine is approved. | +| UI-COV | PASS | Renderer governance changes no reachable UI by itself. Spec 378 still records that implementation will change report/action/download surfaces and must update or close out UI coverage then. | +| Test Governance | PASS | This docs-only gate requires no PHP tests. Future runtime implementation must cover gateway, failure mapping, authorization-before-render, artifact storage, OperationRun, audit, and content smoke. | +| Operational Readiness | PASS with controls | Docker/Dokploy fit is strong, but runtime service config, health check, env/config, timeouts, request limits, logs, and image pinning remain required implementation tasks. | +| Billing / Invoice Boundary | PASS | Approval is explicitly limited to report-style PDF rendering. BillingDocument, invoice numbering, tax/e-invoice XML, and invoice archival remain separate future domains. | + +## SaaS Invoice Future Boundary + +PDF Rendering Infrastructure is shared. +Report Domain is separate. +Billing / Invoice Domain is separate. +E-Invoice XML Compliance is separate. + +Target architecture: + +```text +Shared PDF Rendering Infrastructure + -> Gotenberg / Chromium Renderer + -> PdfRenderingGateway + -> Storage + -> Audit + -> OperationRun + +Domain-specific generators + -> ManagementReportGenerator + -> EvidenceReportGenerator + -> InvoiceDocumentGenerator + -> CreditNoteDocumentGenerator +``` + +Identity boundaries: + +- `StoredReport` is not `BillingDocument`. +- `Report ID` is not `Invoice Number`. +- `OperationRun ID` is not `Invoice Number`. +- `StoredReport ID` is not `Invoice Number`. + +Future billing specs must define these separately before implementation: + +- `BillingDocument` +- `InvoiceNumberAllocator` +- `InvoiceTaxCalculator` +- `EInvoiceXmlBuilder` +- `ZugferdFacturXBuilder` +- `XRechnungBuilder` +- `InvoicePdfRenderer via PdfRenderingGateway` +- `BillingDocumentArchive` +- `BillingAuditTrail` + +## Final Gate Result + +renderer gate approved - implementation may proceed after runtime config task. + +Required next scoped task before report runtime implementation: + +- Add and validate Gotenberg runtime service configuration for Sail/Dokploy with the controls from `spec378-gotenberg-security-controls.md`. +- Add config keys for renderer base URL, timeout, body/output size limits, and disabled/unavailable behavior. +- Implement `PdfRenderingGateway` / `PdfRendererClient` only after the runtime config task is scoped. +- Do not implement management report payload, StoredReport integration, OperationRun integration, UI action, PDF templates, or report generation before that runtime path exists. diff --git a/specs/378-management-report-pdf-v1/artifacts/spec378-gotenberg-security-controls.md b/specs/378-management-report-pdf-v1/artifacts/spec378-gotenberg-security-controls.md new file mode 100644 index 00000000..cd4321c8 --- /dev/null +++ b/specs/378-management-report-pdf-v1/artifacts/spec378-gotenberg-security-controls.md @@ -0,0 +1,87 @@ +# Spec 378 Gotenberg Security Controls + +Date: 2026-06-14 +Decision: Gotenberg 8 Chromium is approved with controls as an internal production PDF rendering service for report-style documents. + +These are required future implementation controls. This artifact does not add runtime config and does not claim that Gotenberg is already deployed in the repo. + +## Required Runtime Boundary + +- Gotenberg runs as a separate internal Docker service. +- The Laravel app and queue containers do not install or execute Node, Puppeteer, Chrome, Chromium, or a browser binary for production PDF rendering. +- Laravel accesses the service only through a narrow `PdfRenderingGateway` / `PdfRendererClient`. +- The gateway exposes only server-generated HTML-to-PDF rendering required by report generators. +- No user-provided URL rendering is allowed in Spec 378 v1. +- No public port is exposed for the renderer in Sail or Dokploy. +- The service is reachable only over the internal Docker/Dokploy network. + +## Required Image And Deployment Controls + +- Use a pinned Gotenberg 8 Chromium image version or immutable digest; never `latest`. +- Prefer the Chromium-only Gotenberg 8 image variant for v1 unless office conversion is explicitly approved later. +- Record the exact image tag/digest in the implementation PR. +- Add a health check using Gotenberg `/health`. +- Configure startup/readiness behavior so unavailable renderer state fails generation before artifact exposure. +- Configure resource limits appropriate for PDF generation workers. +- Configure request timeout (`API_TIMEOUT`) and Chromium startup timeout (`CHROMIUM_START_TIMEOUT`) explicitly. +- Configure multipart request size limit (`API_BODY_LIMIT`) explicitly. +- Configure Chromium queue/concurrency limits (`CHROMIUM_MAX_QUEUE_SIZE`, `CHROMIUM_MAX_CONCURRENCY`) explicitly. +- Disable unused webhook behavior unless a future spec explicitly approves it. +- Disable or do not use `downloadFrom` in v1. + +## Required Request Controls + +- Send HTML as a server-generated `index.html` multipart payload. +- Send only local bundled assets alongside the HTML payload. +- Reference assets by relative filename only. +- Do not send signed URLs inside HTML. +- Do not send access tokens, refresh tokens, client secrets, provider credentials, raw provider payloads, SQL errors, stack traces, serialized job context, or internal MSP-only notes inside HTML. +- Do not render arbitrary operator-provided HTML. +- Do not render arbitrary user-provided remote URLs. +- Apply max HTML payload size, max asset size, and max PDF output size in the Laravel gateway before calling Gotenberg. +- Add a correlation id / OperationRun id to request metadata, using the configured Gotenberg correlation header. + +## Required Network And File-Access Controls + +- Keep Gotenberg internal-only with no public listener. +- No direct user-provided URL rendering in v1. +- Deny or tightly restrict outbound URL access. +- For Spec 378, default to no external outbound fetches: use bundled local assets only. +- Set Chromium outbound restrictions to reject public and private external HTTP destinations unless a later approved asset host allow-list is introduced. +- Do not enable file access beyond Gotenberg's per-request working directory defaults. +- Keep Chromium file access from files disabled. +- Do not mount application storage, secrets, `.env`, or host paths into the Gotenberg container. +- If custom fonts are required later, use a dedicated reviewed font image or readonly font mount with no secrets. + +## Required Error And Audit Controls + +- Renderer unavailable must map to OperationRun failed/blocked with safe reason. +- Renderer timeout must map to OperationRun failed/blocked with safe reason. +- Renderer 400/503 responses must map to structured internal error classes. +- Do not expose raw renderer errors to customers. +- No ghost report: failed generation must not expose a ready artifact, stale download URL, or partially written PDF. +- Renderer logs must not contain secrets because request HTML and metadata must be pre-sanitized. +- Audit generation attempts and successful downloads with safe metadata only. +- Log correlation id, source review/pack id, operation run id, profile, format, status, and safe renderer outcome. + +## Required Testing And Validation Controls + +- Unit coverage for renderer client request building and size-limit rejection. +- Feature coverage for renderer unavailable, timeout, invalid payload, and successful artifact response mapping. +- Feature coverage proving no ready artifact is exposed on renderer/storage failure. +- Feature coverage proving denied generation never calls the renderer. +- Browser/content smoke only after runtime service and generation flow are implemented. +- Deployment validation must include health check behavior and queue worker failure handling. + +## Explicit Non-Approval + +Gotenberg is approved as shared internal PDF rendering infrastructure for report-style documents. + +This approval does not approve legal invoice generation, German B2B e-invoicing, XRechnung, ZUGFeRD/Factur-X, GoBD archival, tax calculation, invoice numbering, or billing compliance. + +## Source Notes + +- Gotenberg supports Docker and Docker Compose service use, with other services able to reach it on the compose network. +- Gotenberg exposes a Chromium HTML-to-PDF route accepting `index.html` plus optional assets. +- Gotenberg exposes `/health` for liveness/monitoring. +- Gotenberg has timeout, body limit, correlation id, Chromium concurrency, queue, file access, and outbound URL filtering settings that must be set deliberately. diff --git a/specs/378-management-report-pdf-v1/artifacts/spec378-pdf-renderer-decision-matrix.md b/specs/378-management-report-pdf-v1/artifacts/spec378-pdf-renderer-decision-matrix.md new file mode 100644 index 00000000..e5899476 --- /dev/null +++ b/specs/378-management-report-pdf-v1/artifacts/spec378-pdf-renderer-decision-matrix.md @@ -0,0 +1,83 @@ +# Spec 378 PDF Renderer Decision Matrix + +Date: 2026-06-14 +Scope: Management Report PDF v1 renderer/runtime governance only. +Outcome: approved with controls for Gotenberg 8 Chromium as an internal Docker service. + +## Repo Safety Baseline + +- Branch: `378-management-report-pdf-v1` +- HEAD: `f1eadadf docs: add spec 377 post-productization browser reaudit closeout gate (#448)` +- `git status --short --branch`: branch `378-management-report-pdf-v1`; untracked `specs/378-management-report-pdf-v1/` +- `git diff --name-only`: no tracked-file diff before this governance update +- Staged files before this governance update: none observed +- Dirty/untracked files before this governance update: `specs/378-management-report-pdf-v1/` +- Spec 378 files already changed before this governance update: present as an untracked active spec directory +- Other specs affected before this governance update: none observed +- Existing runtime PDF renderer in `apps/platform/composer.json`: none +- Existing PDF runtime in `apps/platform/package.json`: none; `playwright` is listed only under `devDependencies` +- Docker infrastructure: root `docker-compose.yml` exists with `laravel.test`, `queue`, `pgsql`, and `redis`; `apps/platform/docker-compose.yml`, `apps/platform/Dockerfile`, and `apps/platform/docker/` are not present + +## Gate Verification + +Result: gate can be updated with approved renderer. + +- Spec 378 was correctly blocked at the renderer/package gate. +- The missing approved production-safe PDF renderer was documented in `spec.md`, `plan.md`, `tasks.md`, and `checklists/requirements.md`. +- Downstream implementation tasks remain open. +- No runtime implementation was started in tracked app/runtime files. +- Playwright is dev/browser tooling only in `apps/platform/package.json`. +- No Composer PDF renderer is present in `apps/platform/composer.json`. +- Root Docker Compose already has an internal service network, so an internal renderer service can fit the deployment model, but no Gotenberg service exists yet. + +## Decision Matrix + +| Candidate | Runtime model | Deployment impact | Security posture | Maintenance posture | Layout quality | Laravel integration complexity | Docker/Dokploy fit | License/commercial concern | Enterprise suitability | Decision | Rationale | +|---|---|---|---|---|---|---|---|---|---|---|---| +| Gotenberg 8 Chromium internal service | Separate Docker HTTP service using Chromium HTML-to-PDF route; Laravel calls it through a narrow `PdfRenderingGateway` / `PdfRendererClient` | Requires adding a pinned internal service, health check, env config, timeouts, request/output limits, and Dokploy service wiring | Strongest option when kept internal-only with no public port, no user-provided URL rendering, outbound restrictions, server-generated HTML, and structured failure mapping | Operationally patchable as a service image; avoids embedding Chrome/Node in the app container; image must be pinned to an explicit 8.x Chromium tag or digest | High for modern HTML/CSS report layouts, page breaks, headers/footers, and branded management reports | Moderate; HTTP client and gateway contract are needed, but no Composer PDF package or browser binary in Laravel | Strong; repo is Sail/Docker/Dokploy-oriented and already uses a compose network | Open-source service; no per-document commercial licensing identified for report rendering; production image/version governance still required | Best fit for enterprise SaaS report PDFs under controls | approved with controls | Matches Docker-first deployment, isolates browser runtime from Laravel, supports Chromium-quality rendering, and can be governed with network, timeout, egress, and artifact controls. | +| Spatie Browsershot / Spatie Laravel PDF with Browsershot | Laravel package invokes Puppeteer/Chrome through Node | Adds Composer package plus Node/Puppeteer/Chrome runtime or custom image changes near the app container | Larger app-container attack/ops surface; browser process ownership, temp files, sandboxing, and binary path management move into Laravel runtime | Actively used but coupled to Node/Puppeteer/Chrome versions and host dependencies | High Chromium quality | Low at code level but high at runtime; needs Node 22+, Puppeteer, Chrome, binary paths | Weaker for this repo because it pulls browser runtime into app/queue containers | MIT-style package ecosystem, but operational dependency surface is larger | Good for smaller apps; not preferred here | rejected | It solves layout quality but violates the preferred isolation boundary: no direct Chromium/Node/browser runtime in Laravel app or queue containers for v1. | +| dompdf / barryvdh/laravel-dompdf | Pure PHP package inside Laravel | Simple Composer install and no external service | Smaller infrastructure surface, but renderer runs in app process and remote resource/file options require careful hardening | Mature PHP package, but HTML/CSS engine is intentionally limited | Not sufficient for modern enterprise report layouts; limited CSS, no JavaScript, weak repeating header/footer support | Low | Good deployment simplicity | LGPL/transitive license review required; no commercial blocker | Suitable for simple PDFs only | rejected | Simplicity is outweighed by CSS/layout limitations for branded management reports with reliable page breaks, tables, and headers/footers. | +| wkhtmltopdf / Snappy | Native binary or PHP wrapper around wkhtmltopdf using Qt WebKit | Requires binary installation, container image changes, and runtime process management | Higher maintenance and security risk due to old browser engine lineage | Poor; upstream wkhtmltopdf repository is archived/read-only and organization is marked unmaintained | Historically useful but outdated rendering engine | Moderate; wrappers exist but depend on external binary | Weak for new platform work | LGPL-style tooling; main issue is maintenance, not license | Not acceptable for new enterprise SaaS renderer | rejected | The upstream status and Qt WebKit legacy make it a poor default for a new security-sensitive SaaS runtime. | +| PrinceXML | Commercial print/Paged Media engine | Requires licensed binary/service integration and procurement | Strong if licensed and isolated correctly | Strong commercial vendor posture | Highest print/Paged Media quality | Moderate; custom client/integration required | Good if packaged as service or sidecar | Commercial/OEM license and cost review required | Excellent for premium publishing/compliance documents | premium future option | Overkill for v1 management reports, but worth revisiting for premium print workflows or legally constrained document classes if licensing is approved. | + +## Selected Renderer + +Decision: approved with controls. + +Approved renderer family: Gotenberg 8 Chromium internal PDF rendering service. + +Production pinning rule: use an explicit Gotenberg 8 Chromium image tag or immutable digest during the runtime config task. Do not use `latest`. Do not treat the unpinned major tag as sufficient for production promotion unless the deployment governance explicitly accepts that risk. + +Approved scope: + +- Internal PDF rendering infrastructure for TenantPilot report-style documents. +- Server-generated HTML-to-PDF using the Chromium conversion route. +- Use by Spec 378 Management Report PDF v1 through a Laravel gateway/client abstraction. + +Not approved: + +- Legal invoice generation. +- German B2B e-invoicing. +- XRechnung, ZUGFeRD, or Factur-X compliance. +- GoBD archival. +- Tax calculation. +- Invoice numbering. +- Billing compliance. +- Public renderer exposure. +- User-supplied URL-to-PDF rendering in v1. +- Direct Node/Chromium/browser runtime inside the Laravel app or queue containers. + +## Consulted Primary Sources + +- Gotenberg installation and image variants: https://gotenberg.dev/docs/getting-started/installation +- Gotenberg Chromium HTML-to-PDF route: https://gotenberg.dev/docs/convert-with-chromium/convert-html-to-pdf +- Gotenberg configuration flags: https://gotenberg.dev/docs/configuration +- Gotenberg outbound URL filtering: https://gotenberg.dev/docs/outbound-url-filtering +- Gotenberg health check: https://gotenberg.dev/docs/system/get-health-check +- Spatie Browsershot requirements: https://spatie.be/docs/browsershot/v4/requirements +- Dompdf README: https://github.com/dompdf/dompdf +- Spatie Laravel PDF DOMPDF driver limitations: https://spatie.be/docs/laravel-pdf/v2/drivers/using-the-dompdf-driver +- wkhtmltopdf status: https://wkhtmltopdf.org/status.html +- wkhtmltopdf archived repository evidence: https://github.com/wkhtmltopdf/wkhtmltopdf/issues/5160 +- Snappy wrapper dependency: https://github.com/KnpLabs/snappy/ +- PrinceXML product and licensing: https://www.princexml.com/ and https://www.princexml.com/purchase/ diff --git a/specs/378-management-report-pdf-v1/checklists/requirements.md b/specs/378-management-report-pdf-v1/checklists/requirements.md new file mode 100644 index 00000000..8f87af41 --- /dev/null +++ b/specs/378-management-report-pdf-v1/checklists/requirements.md @@ -0,0 +1,59 @@ +# Requirements Checklist: Spec 378 - Management Report PDF v1 + +**Purpose**: Preparation readiness review for Spec 378 before application implementation. +**Created**: 2026-06-14 +**Feature**: `specs/378-management-report-pdf-v1/spec.md` + +## Candidate And Scope + +- [x] CHK001 The selected candidate is directly user-provided and not invented from the automatic queue. +- [x] CHK002 Related completed specs are treated as historical context only and are not rewritten. +- [x] CHK003 The smallest v1 slice is one `customer_executive` management PDF artifact from existing ready/current review-pack truth. +- [x] CHK004 Technical/Auditor Evidence Report, Delivery Center, scheduled delivery, portal, AI, and raw JSON appendix are out of scope. +- [x] CHK005 The spec records close alternatives and follow-up candidates instead of hiding them inside v1. + +## Repo Truth And Dependencies + +- [x] CHK006 The spec reuses the existing rendered-report route/controller/view family from Specs 356, 357, and 366. +- [x] CHK007 The spec requires `ReportProfileRegistry` and `ReportDisclosurePolicy` rather than inventing parallel profile/disclosure rules. +- [x] CHK008 The plan records that no native PDF runtime package is currently approved in `apps/platform/composer.json`. +- [x] CHK009 The tasks include a hard package/renderer governance gate before runtime implementation. +- [x] CHK010 The plan records that current `StoredReport` is narrow and may need a bounded substrate extension. + +## Security, RBAC, And Isolation + +- [x] CHK011 Workspace, tenant, and managed-environment scope are explicit for generation, storage, lookup, and download. +- [x] CHK012 Unauthorized non-member or wrong-scope access uses deny-as-not-found semantics. +- [x] CHK013 Member-without-capability handling is specified as 403 after scope is established. +- [x] CHK014 The PDF and audit metadata forbid secrets, signed URLs, raw provider payloads, raw operation context, SQL errors, stack traces, and serialized jobs. +- [x] CHK015 Download is required to be signed and/or server-authorized and to re-resolve scope server-side. + +## OperationRun, Audit, And Artifact Truth + +- [x] CHK016 The preferred implementation creates or reuses an OperationRun for generation. +- [x] CHK017 The spec requires safe OperationRun outcomes for success, renderer failure, storage failure, and blocked source/readiness cases. +- [x] CHK018 Generation audit and download audit metadata are specified. +- [x] CHK019 Artifact truth is required to carry workspace, tenant scope, managed environment, source review/pack, profile, format, actor, generated time, and operation-run provenance. +- [x] CHK020 A new artifact entity is not approved; implementation must stop and update spec/plan if one is required. + +## UI And Productization Coverage + +- [x] CHK021 UI Surface Impact is marked as changed reachable surfaces, not `No UI surface impact`. +- [x] CHK022 The affected surfaces are bounded to existing rendered report, Environment Review/Review Pack owner actions, optional download route, and StoredReport only if reused. +- [x] CHK023 The plan defines deterministic UI coverage update rules for UI-099/UI-042/UI-048, route inventory, and design coverage artifacts. +- [x] CHK024 The generate action is classified as high-impact artifact creation, not destructive Microsoft-tenant mutation, and requires explicit confirmation. +- [x] CHK025 Filament v5 / Livewire v4 compliance, provider registration, global-search posture, action safety, asset strategy, and testing plan are called out for implementation close-out. + +## Testing And Validation + +- [x] CHK026 Unit tests are required for payload, readiness, disclosure, and renderer failure mapping. +- [x] CHK027 Feature tests are required for generation, storage, OperationRun, audit, authorization, and download. +- [x] CHK028 Filament/Livewire action tests are required for action visibility/disabled state on the chosen owner surface. +- [x] CHK029 Browser/content smoke is required for customer-facing PDF content and leakage boundaries. +- [x] CHK030 PostgreSQL lane is required if migrations/indexes/schema constraints are introduced. + +## Review Outcome + +- [x] CHK031 Review outcome class: `acceptable-special-case` for preparation, with renderer/package gate preserved for implementation. +- [x] CHK032 Workflow outcome: `keep` for the prepared scope, with follow-up specs separated. +- [x] CHK033 No application implementation was performed during preparation. diff --git a/specs/378-management-report-pdf-v1/plan.md b/specs/378-management-report-pdf-v1/plan.md new file mode 100644 index 00000000..dcefcaa4 --- /dev/null +++ b/specs/378-management-report-pdf-v1/plan.md @@ -0,0 +1,434 @@ +# Implementation Plan: Spec 378 - Management Report PDF v1 + +**Branch**: `378-management-report-pdf-v1` | **Date**: 2026-06-14 | **Spec**: `specs/378-management-report-pdf-v1/spec.md` +**Input**: Feature specification from `specs/378-management-report-pdf-v1/spec.md` + +## Summary + +Implement the first durable customer-executive management PDF artifact over the existing Review Pack rendered-report family. The PDF must reuse the current `ReviewPackRenderedReportController` content/profile/disclosure truth where possible, produce a stored or canonically referenced private artifact, expose an authorized download path, and record OperationRun plus audit evidence for generation and download. + +This is not a new report engine. It is the smallest PDF artifact productization slice after Specs 356, 357, and 366. + +## 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, approved Gotenberg 8 Chromium internal PDF rendering service (Sail/local service and Laravel gateway configured; staging/Dokploy controls pending), existing Playwright dev/browser tooling only +**Storage**: PostgreSQL plus private Laravel storage disk for generated PDF artifacts if stored file output is implemented +**Testing**: Pest Unit, Feature/Filament action, and Browser/content smoke +**Validation Lanes**: fast-feedback, confidence, browser; PostgreSQL lane if migrations/indexes are introduced +**Target Platform**: Laravel Sail locally, Dokploy container deployment for staging/production +**Project Type**: Laravel monolith under `apps/platform` +**Performance Goals**: PDF generation is queue-safe and idempotent; report render/download stays DB/storage-only with no live provider calls +**Constraints**: no Graph calls during render/generation/download; Gotenberg 8 Chromium only through a Laravel gateway/client; no direct browser runtime in Laravel app/queue containers; no new runtime package without package governance; workspace/managed-environment isolation; profile/disclosure fail-closed; private artifact storage; no raw technical leakage +**Scale/Scope**: one customer-executive PDF profile, one current ready Review Pack source, one artifact output path, focused report/audit/OperationRun tests + +## Repo Truth Map + +Current runtime/report foundations: + +- `apps/platform/app/Http/Controllers/ReviewPackRenderedReportController.php` builds the existing rendered report state. +- `apps/platform/resources/views/review-packs/rendered-report.blade.php` renders the management-style HTML/print report. +- `apps/platform/app/Support/ReviewPacks/ReportProfileRegistry.php` defines `customer_executive`, technical, internal, auditor, and framework profiles. +- `apps/platform/app/Support/ReviewPacks/ReportDisclosurePolicy.php` evaluates mandatory disclosures and appendix/technical visibility. +- `apps/platform/app/Support/ReviewPacks/ReportThemeResolver.php` exists from Spec 366 for management layout/theme derivation. +- `apps/platform/app/Services/ReviewPackService.php` generates Review Packs, signed ZIP/download URLs, and signed rendered-report URLs. +- `apps/platform/app/Http/Controllers/ReviewPackDownloadController.php` owns current signed ZIP download access/audit. +- `apps/platform/app/Models/StoredReport.php` and `StoredReportResource` provide a narrow report substrate for permission posture and Entra admin roles. +- `apps/platform/app/Services/OperationRunService.php`, `OperationRunType`, `OperationCatalog`, and Operations UI/actionability specs provide run truth. +- `docs/ui-ux-enterprise-audit/page-reports/ui-099-rendered-review-report.md`, `ui-042-review-pack-detail.md`, and `ui-048-stored-report-detail.md` are the relevant UI coverage references. + +Repo-truth constraints: + +- No native PDF runtime dependency is present in `apps/platform/composer.json`. +- `apps/platform/package.json` includes Playwright for dev/browser tooling, not an approved production PDF runtime. +- Gotenberg 8 Chromium is approved with controls as an internal Docker PDF rendering service. Root `docker-compose.yml`, Laravel app config, and `PdfRenderingGateway` / `PdfRendererClient` now exist for Sail/local; no Dokploy config file exists in the repo, so `docs/deployment-checklist.md` records the required staging/production controls. +- Existing `StoredReport` has no file path, format, generated-by, status, source review, source review pack, or operation-run columns today. +- Existing Review Pack ZIP contract must remain unchanged. + +## Renderer Governance Gate Result + +- **Gate result**: approved with controls. +- **Selected renderer**: Gotenberg 8 Chromium internal service. +- **Decision matrix**: `specs/378-management-report-pdf-v1/artifacts/spec378-pdf-renderer-decision-matrix.md`. +- **Security controls**: `specs/378-management-report-pdf-v1/artifacts/spec378-gotenberg-security-controls.md`. +- **Constitution review**: `specs/378-management-report-pdf-v1/artifacts/spec378-constitution-renderer-gate-review.md`. +- **Package governance**: `docs/package-governance.md` records the approved runtime service scope and controls. +- **Runtime status**: Sail/local renderer service, Laravel config, health check, timeout/size controls, and gateway/client safe result mapping are implemented. Staging/Dokploy runtime validation remains pending before management report PDF generation is enabled. +- **Approved service boundary**: Laravel must call Gotenberg through `PdfRenderingGateway` / `PdfRendererClient`; production PDF rendering must not install or execute Chromium, Chrome, Puppeteer, Playwright, Node, or browser binaries in Laravel app/queue containers. +- **Invoice / billing boundary**: this approval is not approval for legal invoice generation, German B2B e-invoicing, XRechnung, ZUGFeRD/Factur-X, GoBD archival, tax calculation, invoice numbering, or billing compliance. + +## PDF Rendering Layer Responsibilities + +- `PdfRendererClient`: low-level HTTP transport to the internal Gotenberg service. It owns multipart request assembly, timeout handling, HTTP status mapping, response byte validation, configured body/output size limits, and correlation id headers. +- `PdfRenderingGateway`: shared application boundary for server-generated HTML-to-PDF rendering. It accepts sanitized HTML/assets/render options and returns typed success/failure results suitable for OperationRun and audit mapping. It does not know management-report domain rules. +- `ManagementReportPdfRenderer`: Spec 378 domain adapter. It converts a prepared management report payload into sanitized HTML/assets, calls `PdfRenderingGateway`, and maps returned renderer outcomes into management-report generation flow. It does not own HTTP transport, storage persistence, OperationRun lifecycle, or billing/invoice semantics. + +## SaaS Invoice / Billing Boundary + +The shared renderer is infrastructure only. Domain truth remains separate: + +- PDF Rendering Infrastructure: shared. +- Report Domain: separate. +- Billing / Invoice Domain: separate. +- E-Invoice XML Compliance: separate. + +Target separation: + +```text +Shared PDF Rendering Infrastructure + -> Gotenberg / Chromium Renderer + -> PdfRenderingGateway + -> Storage + -> Audit + -> OperationRun + +Domain-specific generators + -> ManagementReportGenerator + -> EvidenceReportGenerator + -> InvoiceDocumentGenerator + -> CreditNoteDocumentGenerator +``` + +Identity boundaries: + +- `StoredReport` is not `BillingDocument`. +- `Report ID` is not `Invoice Number`. +- `OperationRun ID` is not `Invoice Number`. +- `StoredReport ID` is not `Invoice Number`. + +Future billing specs must separately define `BillingDocument`, `InvoiceNumberAllocator`, `InvoiceTaxCalculator`, `EInvoiceXmlBuilder`, `ZugferdFacturXBuilder`, `XRechnungBuilder`, `InvoicePdfRenderer via PdfRenderingGateway`, `BillingDocumentArchive`, and `BillingAuditTrail`. + +## Candidate Selection Gate + +- **Selected candidate**: Spec 378 - Management Report PDF v1. +- **Source**: Direct user-provided Spec 378 draft. +- **Why selected**: It is the natural bounded follow-up after Specs 356/357/366 made the rendered report, profile/disclosure policy, and management layout repo-real. +- **Deferred alternatives**: Technical/Auditor Evidence Report, Delivery Center, scheduled delivery, Customer Review Workspace polish, compliance-framework reports, AI summaries. +- **Completed-spec guardrail**: Specs 356, 357, 365, 366, 367, 372, and 377 are context only. They contain completed task markers, validation, close-out, or browser evidence and must not be rewritten. +- **Result**: PASS. + +## UI / Surface Guardrail Plan + +- **Guardrail scope**: changed existing report/review artifact surfaces plus one possible new signed/authorized PDF download route. +- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**: + - `ReviewPackRenderedReportController` + - rendered-report Blade template only if PDF payload generation requires shared partial/content extraction + - `EnvironmentReviewResource` and/or `ReviewPackResource` detail/header actions + - `StoredReportResource` only if generated management PDF artifacts appear there + - optional new management PDF download route +- **No-impact class, if applicable**: N/A. +- **Native vs custom classification summary**: owner surfaces remain native Filament actions; report canvas inherits Spec 366's legitimate custom report/print surface; stored report remains native Filament read-only registry. +- **Shared-family relevance**: report viewer, artifact download, status/readiness messaging, OperationRun start/link UX, audit. +- **State layers in scope**: detail header action state, source report payload, PDF artifact state, signed/download URL, OperationRun status/outcome, stored report/artifact lifecycle. +- **Audience modes in scope**: customer/read-only for PDF content; operator-MSP for generation/download/action state; support-platform only through existing OperationRun/log diagnostics. +- **Decision/diagnostic/raw hierarchy plan**: decision-first PDF; diagnostics only in OperationRun/logs; raw/support payload absent from PDF. +- **Raw/support gating plan**: no raw JSON/provider payload/support context in customer-executive PDF. Any failure detail remains safe summary in UI and deeper diagnostics in logs/OperationRun. +- **One-primary-action / duplicate-truth control**: source detail surfaces expose at most one dominant next step: generate if no current PDF and ready, download/open if generated, view operation if running/failed. The generate action must require explicit Filament confirmation before artifact creation starts. +- **Handling modes by drift class or surface**: hard-stop-candidate for raw leakage, unauthorized artifact access, unapproved PDF runtime package, second report engine, or historical/expired pack impersonation. +- **Repository-signal treatment**: update UI-099/UI-042/UI-048 only if action hierarchy or artifact presentation materially changes; otherwise record a no-update rationale in close-out. +- **Special surface test profiles**: report-viewer, high-impact artifact generation action, customer-facing artifact. +- **Required tests or manual smoke**: Unit payload/disclosure/readiness; Feature authorization/artifact/audit/run/download; Browser/content smoke for generate/download/PDF text. +- **Exception path and spread control**: custom PDF/report layout remains bounded to the existing rendered-report family. No generic report framework. +- **Active feature PR close-out entry**: Guardrail + Smoke Coverage. +- **UI/Productization coverage decision**: reachable report/action surfaces change; implementation must update or explicitly close out coverage docs. +- **Coverage artifacts to update**: update route inventory when a new signed/authorized PDF route is added; update `ui-099-rendered-review-report.md` when rendered-report/PDF source content or customer-facing report presentation materially changes; update `ui-042-review-pack-detail.md` when Review Pack detail owns the action/download state; update `ui-048-stored-report-detail.md` when StoredReport registry/detail exposes the artifact; update design coverage matrix when action hierarchy, artifact state, or customer-facing surface classification materially changes. +- **No-impact rationale**: N/A. +- **Navigation / Filament provider-panel handling**: no navigation or panel provider change planned. Laravel 12 Filament provider registration remains in `apps/platform/bootstrap/providers.php`. +- **Screenshot or page-report need**: yes, one bounded browser/content smoke with artifact evidence where local tooling permits it. + +## Shared Pattern & System Fit + +- **Cross-cutting feature marker**: yes. +- **Systems touched**: Review Pack rendered report, report profiles, disclosure policy, Review Pack service, stored report/artifact storage, OperationRun, audit, localization, Filament owner-surface actions. +- **Shared abstractions reused**: + - `ReportProfileRegistry` + - `ReportDisclosurePolicy` + - `ReportThemeResolver` + - `ReviewPackOutputReadiness` + - `ReviewPackOutputResolutionGuidance` + - `ReviewPackService` + - current signed route/download patterns + - `OperationRunService` + - current audit logger/action ID pattern +- **New abstraction introduced? why?**: likely one bounded generation service and one renderer adapter. This is justified because PDF generation must not live in Blade/controller code and must isolate rendering/storage failures. +- **Why the existing abstraction was sufficient or insufficient**: Existing abstractions are sufficient for HTML report content, profile/disclosure, source current-pack validation, and signed URL generation. They are insufficient for private PDF artifact storage, generation idempotency, OperationRun result metadata, and download audit. +- **Bounded deviation / spread control**: new code must live under existing report/review-pack/support namespaces and target only management PDF v1. No profile CRUD, delivery system, report engine, or artifact lifecycle framework. + +## OperationRun UX Impact + +- **Touches OperationRun start/completion/link UX?**: yes. +- **Central contract reused**: `OperationRunService`, `OperationRunLinks`, `OperationUxPresenter`, `OperationCatalog`, `OperationRunType` if a new type is needed. +- **Delegated UX behaviors**: queued toast/run link/artifact link/run-enqueued browser event/dedupe-or-blocked/start-failure messaging should use the shared OperationRun start UX path. +- **Surface-owned behavior kept local**: generation readiness explanation and source review/pack selection only. +- **Queued DB-notification policy**: no queued/running DB notification unless explicitly approved through the existing shared start UX contract. +- **Terminal notification path**: central lifecycle mechanism. +- **Exception path**: none expected. If implementation proposes synchronous no-run PDF generation, update spec/plan first and prove why OperationRun is unnecessary. + +## Provider Boundary & Portability Fit + +- **Shared provider/platform boundary touched?**: yes, indirectly through report/evidence content. +- **Provider-owned seams**: existing review/evidence summaries that may include Microsoft-specific evidence labels. +- **Platform-core seams**: report artifact identity, profile, disclosure, provenance, OperationRun type, StoredReport/source descriptor. +- **Neutral platform terms / contracts preserved**: management report, profile, evidence readiness, limitation, managed environment, workspace, generated artifact, source review, source pack, operation. +- **Retained provider-specific semantics and why**: only existing profile-safe evidence/finding summaries from stored review/pack truth. No new provider lookup or provider-specific renderer branch. +- **Bounded extraction or follow-up path**: Technical/Auditor Evidence Report follow-up if provider-specific detailed appendix becomes necessary. + +## Constitution Check + +*GATE: Must pass before implementation. Re-check after design and before code merge.* + +- Inventory-first: PASS. Report consumes existing released review/Review Pack/evidence truth, not live provider state. +- Read/write separation: PASS. PDF generation mutates TenantPilot artifact storage only and requires explicit operator action, audit, and tests. +- Graph contract path: PASS. No Graph calls are allowed during render, generation, or download. +- Deterministic capabilities: PASS with task. Reuse existing capability registry paths or add a tested capability only after repo verification. +- RBAC-UX: REQUIRED. Non-member/wrong scope 404; member missing capability 403; server-side authorization required. +- Workspace isolation: REQUIRED. Artifact, source review, source pack, operation, and download all resolve workspace/environment scope. +- Destructive-like/high-impact action: REQUIRED. Generate PDF is high-impact artifact creation; it needs readiness gate, authorization, audit, and safe feedback. It is not destructive. +- Global search: PASS. No global-search enablement planned; `StoredReportResource` remains non-globally-searchable unless spec/plan are updated. +- Run observability: REQUIRED. Preferred path creates/reuses an OperationRun for generation. +- OperationRun start UX: REQUIRED if queued generation is used; central UX path only. +- Ops-UX lifecycle: REQUIRED. Status/outcome transitions through `OperationRunService`; summary counts flat numeric-only if used. +- Data minimization: REQUIRED. No secrets/raw provider payloads/signed URLs in PDF/audit metadata. +- Test governance: REQUIRED. Unit/Feature/Browser lanes and fixture costs explicit. +- Proportionality: PASS with constraint. Generated PDF artifact truth and renderer/generation service solve current customer artifact gap. +- No premature abstraction: PASS with constraint. Generation/renderer/payload components are bounded to one report family and must not become a generic framework. +- Persisted truth: PASS. Generated PDF is durable artifact truth with provenance. +- Behavioral state: PASS if new status/reason keys change artifact visibility, OperationRun outcome, or download behavior. +- UI semantics: PASS. Directly map existing profile/readiness/disclosure into the PDF; no new taxonomy. +- Shared pattern first: PASS. Reuse report/profile/disclosure/OperationRun/audit paths first. +- Provider boundary: PASS. Stored summaries only; no provider runtime calls. +- V1 explicitness / few layers: PASS. One profile, one artifact, one source family. +- BLOAT-001: REQUIRED and covered in `spec.md`. +- Badge/Filament-native UI: PASS. Owner surfaces stay native Filament. Custom report canvas remains bounded to report/print artifact. +- UI-COV-001: REQUIRED. Coverage update/no-update rationale required. + +## Test Governance Check + +- **Test purpose / classification by changed surface**: + - Unit: payload/readiness/disclosure/profile filtering and renderer adapter failure mapping. + - Feature: generation action, authorization, artifact storage, OperationRun outcomes, audit metadata, signed download, cross-scope denial. + - Filament/Livewire: owner-surface action visibility/disabled state where generation action lands. + - Browser/content smoke: generate/download path and PDF/text content proof. +- **Affected validation lanes**: fast-feedback, confidence, browser; pgsql if schema/index changes. +- **Why this lane mix is the narrowest sufficient proof**: Unit tests prove deterministic content and safety decisions; Feature tests prove server-side trust; Browser/content smoke proves the customer-facing artifact is usable and leakage-free. +- **Narrowest proving command(s)**: + - `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec378` + - `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec378ManagementReportPdfSmokeTest.php --compact` + - `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec357` + - `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec366` + - `cd apps/platform && ./vendor/bin/sail pint --dirty` + - `git diff --check` +- **Fixture / helper / factory / seed / context cost risks**: Reuse Spec357/Spec366 report fixtures and review-output seed command. Any new PDF fixture helpers must remain local to Spec378 unless reused by at least three current specs. +- **Expensive defaults or shared helper growth introduced?**: no by default. +- **Heavy-family additions, promotions, or visibility changes**: one explicit browser/content smoke only. +- **Surface-class relief / special coverage rule**: customer-facing document artifact requires special content/leakage smoke. +- **Closing validation and reviewer handoff**: reviewers verify package governance gate, no raw leakage, artifact/operation/audit agreement, scope-safe download, and no report-center/auditor scope. +- **Budget / baseline / trend follow-up**: record follow-up if PDF rendering adds material runtime to browser/confidence lanes or production queue requirements. +- **Review-stop questions**: renderer approved? artifact storage private? no raw leakage? no second report engine? OperationRun/audit consistent? correct 404/403? +- **Escalation path**: document-in-feature for approved renderer choice; follow-up-spec for delivery center, technical/auditor PDF, scheduled delivery, artifact lifecycle. +- **Active feature PR close-out entry**: Guardrail + Smoke Coverage. +- **Why no dedicated follow-up spec is needed**: The renderer/artifact choice belongs inside Spec 378 if it stays bounded; any broader delivery/auditor lifecycle work is explicitly deferred. + +## Implementation Approach + +### Phase 0 - Pre-Implementation Verification + +1. Re-read current report/runtime files listed in the Repo Truth Map. +2. Renderer/package governance decision is complete: use Gotenberg 8 Chromium as an internal service with the controls in `artifacts/spec378-gotenberg-security-controls.md`. +3. Confirm the runtime config task has added the pinned Gotenberg service and app config before any report generation implementation starts. +4. Verify whether `StoredReport` can be extended narrowly to reference PDF artifacts. +5. Verify whether generation should land on Environment Review, Review Pack, or Customer Review Workspace first. Preferred v1 placement: Environment Review or Review Pack detail, because both already know source review/pack readiness. + +### Phase 0.5 - PDF Rendering Infrastructure Runtime Config + +This phase is the first allowed implementation phase after the governance gate. It must be completed before management report payload, storage, OperationRun, UI action, PDF templates, or report-generation work. + +Required future implementation work: + +- Identify exact runtime config targets before editing: root `docker-compose.yml` for Sail/local service wiring, the repo's Dokploy/deployment config or documentation location if present, and an explicit `not present` note if no Dokploy config file exists in the repo. +- Add a pinned Gotenberg 8 Chromium internal service to Sail/Dokploy runtime config. +- Keep the renderer internal-network only with no public port. +- Add renderer configuration for base URL, timeout, request body limit, asset size limit, PDF output size limit, queue/concurrency expectations, and disabled/unavailable behavior. +- Add health check verification using Gotenberg `/health`. +- Add failing gateway/client tests for health check, renderer unavailable, timeout, invalid response, size-limit rejection, and safe error mapping before completing `PdfRendererClient` / `PdfRenderingGateway`. +- Add `PdfRenderingGateway` / `PdfRendererClient` as the only Laravel integration boundary after the gateway/client tests are in place. +- Add request correlation using the OperationRun id or generation correlation id. +- Render server-generated HTML payloads only; do not use user-provided URL rendering. +- Apply the Gotenberg security controls before enabling report generation. +- Map renderer unavailable, timeout, invalid request, and output-size failures into safe internal result types for later OperationRun handling. +- Do not add Composer/npm PDF/browser packages unless this spec/plan and `docs/package-governance.md` are updated again. + +### Phase 1 - Payload and Readiness + +Create a deterministic management-report payload from existing report truth: + +- Source: current ready non-expired Review Pack and linked Environment Review. +- Profile: `customer_executive`. +- Disclosure: `ReportDisclosurePolicy::evaluate(...)`. +- Sections: cover, executive summary, governance posture, decisions, top risks/findings, accepted risks, evidence readiness, limitations, next actions, provenance, method summary. +- Readiness: block or disable generation when source is missing/not ready/expired/not current/profile-blocked/disclosure-blocked/unauthorized. +- No direct database queries in Blade/PDF template. + +Likely implementation surfaces: + +```text +apps/platform/app/Support/ReviewPacks/ManagementReportPayloadBuilder.php +apps/platform/app/Support/ReviewPacks/ManagementReportReadiness.php +apps/platform/app/Support/ReviewPacks/ManagementReportPdfRenderer.php +``` + +Names may differ if existing repo conventions suggest a better home. + +### Phase 2 - Artifact Storage + +Preferred path: + +- Use `StoredReport` if it can represent the generated PDF artifact with a narrow migration/addition. +- Add a management report type constant only if resource capability/report type maps are updated safely. +- Store private PDF file path/disk or artifact reference in the canonical substrate. +- Keep PDF payload/provenance in JSONB metadata without raw provider payloads. +- Do not create a new artifact table unless the spec/plan are updated and proportionality is re-approved. + +Migration impact if needed: + +- Safe incremental migration under `apps/platform/database/migrations`. +- Reversible where feasible. +- PostgreSQL lane if constraints/indexes/JSONB behavior are relevant. +- Staging validation required before production. + +### Phase 3 - OperationRun and Audit + +Preferred path: + +- Add or map canonical operation type `report.management.generate` only if no existing type fits. +- Register the operation type in `OperationCatalog`/related guards as required. +- Use `OperationRunService` for queued/running/completed/failed/blocked transitions. +- Use safe `context.results` summary with artifact id, source review/pack ids, profile, format, and file metadata. +- Do not write adapter reconciliation metadata unless a reconciliation adapter actually runs. +- Record audit for generation and download with safe metadata. + +### Phase 4 - UI Actions and Download + +Owner surface behavior: + +- Show or enable `Generate management PDF` only when source is ready and actor is authorized. +- If a current generated PDF exists for the same source/profile/fingerprint, prefer `Download management PDF` or `Open management PDF` rather than duplicate generation. +- When generation is running, show one clear operation link. +- When blocked/failed, show safe reason and operation link. +- Keep one primary action per focused area. + +Download behavior: + +- Use a signed route or server-authorized route with short-lived URL if signing is used. +- Re-resolve artifact/source scope server-side. +- Audit every successful download. +- Do not embed signed URLs in the PDF body. + +### Phase 5 - PDF Rendering + +Renderer requirements: + +- Use the approved Gotenberg 8 Chromium renderer path through `PdfRenderingGateway` only. +- Render from prepared payload, not live controller/global request state. +- No remote fonts/assets. +- Header/footer, page metadata, generated timestamp, and page numbers when supported. +- Clean table/section breaks; no huge raw tables. +- No technical appendix in `customer_executive` PDF. +- EN/DE labels through localization. + +### Phase 6 - Validation and Close-Out + +- Run targeted Spec378 tests. +- Run Spec357/Spec366 regressions. +- Run Review Pack and Customer Review Workspace focused regressions as needed. +- Run browser/content smoke. +- Run Pint and `git diff --check`. +- Record Livewire v4 compliance, provider registration, global-search posture, destructive/high-impact action safety, asset strategy, tests, and deployment impact. + +## Data Model / Persistence Impact + +Expected possibilities: + +1. **No schema change**: if current artifact storage can reference a PDF through existing fields. Unlikely based on current `StoredReport`. +2. **Narrow `StoredReport` extension**: likely path if adding `report_type=management_report_pdf`, artifact file metadata, tenant scope, source environment, source review/pack, operation run, generated by, generated at, status, and profile. This must stay tenant-owned with `workspace_id`, NOT NULL `tenant_id` where the table represents tenant-owned artifact truth, and source `managed_environment_id`. +3. **New artifact entity**: not approved by this plan. Stop and update spec/plan before implementation if this becomes necessary. +4. **Tenant-scope stop condition**: if the selected substrate cannot represent constitution-compliant tenant scope, stop before persistence changes and update spec/plan; do not rely on `managed_environment_id` alone for newly persisted tenant-owned artifact truth. + +Retention/lifecycle: + +- V1 may use existing stored report retention defaults or record "retained" semantics consistent with current StoredReportResource. +- Do not implement artifact lifecycle/hold/delete framework in this spec. + +## RBAC / Policy Implications + +- Generation requires workspace membership, tenant entitlement, managed-environment entitlement, and an existing review/export/manage capability unless a new capability is proven necessary. +- Download requires view/download permission for the generated artifact and same workspace/tenant/environment scope. +- Server handlers must call Gate/Policy/capability checks; UI disabled/hidden state is not security. +- Non-member/wrong workspace/wrong tenant/wrong environment gets 404. +- Member without capability gets 403 after scope is established. +- StoredReportResource capability map must include any new management report type or explicitly keep it out of the registry if a separate download route owns access. + +## UI / Filament / Livewire Implications + +- Filament v5 on Livewire 4.1.4 remains compliant; no Livewire v3 APIs. +- Panel providers remain registered in `apps/platform/bootstrap/providers.php`; no panel changes planned. +- `StoredReportResource` remains globally non-searchable unless spec/plan are updated. It has a View page today. +- Generate action is high-impact artifact creation and must use `Action::make(...)->action(...)`, `->requiresConfirmation()`, server-side authorization, readiness gate, explicit notification, and audit before artifact creation starts. +- No destructive action is added. +- No new Filament assets planned. + +## Audit / Observability / Evidence Implications + +- Generation audit must include actor, workspace, managed environment, source review id, source review pack id, stored report/artifact id, operation run id, profile, format, status, generated at, and safe metadata. +- Download audit must include actor, workspace, managed environment, artifact id, source review/pack ids, profile, format, downloaded at, and safe request metadata if existing audit captures it. +- OperationRun results should include artifact id/path hash/size/fingerprint only if safe; no signed URLs or secrets. +- Failed generation must be observable and safe. + +## Deployment / Ops Impact + +- **Env vars**: renderer base URL, timeout, body/output size limits, disabled/unavailable behavior, and matching Gotenberg timeout/body/concurrency controls are added to `apps/platform/.env.example`; no direct browser binary path is approved. +- **Migrations**: possible narrow `stored_reports` extension; staging migration validation required. +- **Queues**: likely uses queue worker for report generation. Dokploy worker command already includes `reports` queue in deployment guidance; implementation must confirm actual queue selection. +- **Scheduler/cron**: none. +- **Storage**: private PDF artifact storage required; production persistence/backup must include the disk/path. +- **Filament assets**: none planned; deployment already runs `php artisan filament:assets` when assets are registered. +- **Package/runtime**: Gotenberg 8 Chromium is approved with controls as an internal runtime service; no Composer/npm PDF/browser runtime package is approved. Any different renderer/runtime dependency requires package governance before code changes. + +## Filament v5 Output Contract For Implementation Close-Out + +Implementation response must explicitly state: + +1. Livewire v4.0+ compliance. +2. Provider registration remains in `apps/platform/bootstrap/providers.php`. +3. Global search status: `StoredReportResource` is globally non-searchable today; any changed resource must keep View/Edit safety explicit. +4. Destructive/high-impact actions: no destructive action; generation is high-impact artifact creation with authorization, readiness, audit, and explicit confirmation implemented. +5. Asset strategy: no new Filament assets unless implementation changes plan; deploy `filament:assets` only if assets are registered. +6. Testing plan/results for payload, generation, access, audit, OperationRun, download, and browser/content smoke. + +## Risks and Mitigations + +- **Renderer config gap**: Sail/local renderer service/config and gateway/client boundary exist, but staging/Dokploy controls are not yet validated. Mitigate by confirming internal-only staging runtime, health check, queue behavior, and persistence before enabling report generation. +- **Storage mismatch**: StoredReport may need schema changes. Mitigate with narrow extension or stop/split. +- **Raw leakage**: PDF may include hidden/internal content. Mitigate with prebuilt payload and negative content tests. +- **Scope leakage**: signed URL could bypass context. Mitigate by server-side re-resolution and 404/403 tests. +- **Duplicate artifact churn**: repeated generation could create clutter. Mitigate with source/profile/fingerprint idempotency where practical. +- **Queue failures**: renderer/storage can fail. Mitigate OperationRun failed/blocked outcomes and no ready artifact exposure. + +## Open Questions + +Renderer governance decision is complete. Gotenberg 8 Chromium is approved with controls as the internal production PDF rendering service for report-style documents. Runtime/gateway implementation has started: Sail/local service, Laravel config, health check, timeout/size controls, and `PdfRenderingGateway` / `PdfRendererClient` now exist. Management report payload/storage/OperationRun/UI/PDF generation work must wait until staging/Dokploy controls are confirmed and the next implementation phase begins. + +## Implementation Phases + +1. Repo verification and renderer/package gate. +2. Runtime config target discovery and Gotenberg service/config. +3. Failing gateway/client tests, then `PdfRenderingGateway` / `PdfRendererClient`. +4. Failing tests for payload, readiness, disclosure, authorization, audit, storage, OperationRun, download, renderer failure mapping, and content leakage. +5. Payload/readiness/disclosure implementation. +6. Artifact storage and idempotency. +7. OperationRun and audit integration. +8. UI action and download route integration. +9. PDF rendering through the approved Gotenberg gateway. +10. Localization. +11. Browser/content smoke and UI coverage close-out. +12. Final validation and close-out. diff --git a/specs/378-management-report-pdf-v1/spec.md b/specs/378-management-report-pdf-v1/spec.md new file mode 100644 index 00000000..b30a8891 --- /dev/null +++ b/specs/378-management-report-pdf-v1/spec.md @@ -0,0 +1,421 @@ +# Feature Specification: Spec 378 - Management Report PDF v1 + +**Feature Branch**: `378-management-report-pdf-v1` +**Created**: 2026-06-14 +**Status**: Renderer governance decision completed; renderer runtime/gateway boundary implemented; report generation not started (2026-06-14) +**Input**: User-provided "Spec 378 - Management Report PDF v1" draft plus repo truth from Specs 356, 357, 366, 365, 367, 372, 377, current Review Pack rendered-report runtime, StoredReport substrate, and roadmap/reporting productization docs. + +## Spec Candidate Check *(mandatory - SPEC-GATE-001)* + +- **Problem**: TenantPilot has a repo-real, profile-aware, management-style rendered Review Pack report, but there is no stored, downloadable PDF artifact that a customer executive or MSP service manager can archive, forward, or use in a management meeting without relying on browser print behavior. +- **Today's failure**: A stakeholder can open the HTML report, but the product cannot yet prove "this management report PDF was generated from this review/evidence basis, by this actor, at this time, with this profile and disclosure policy" as a durable artifact with generation and download audit. +- **User-visible improvement**: An entitled operator can generate a customer-executive management PDF from the current review/report truth, download it from a signed/authorized path, and show provenance, limitations, decisions, evidence readiness, accepted risks, and next actions without raw technical leakage. +- **Smallest enterprise-capable version**: Generate one management-ready PDF artifact for the existing `customer_executive` report profile from an existing ready/current Review Pack rendered-report source, store or reference it through the current StoredReport or canonical artifact substrate, expose one authorized download path, and audit generation plus download. +- **Explicit non-goals**: No Technical Evidence Report, no Auditor Evidence Report, no Report Delivery Center, no scheduled delivery, no email/Teams delivery, no public links, no customer portal, no Word/PowerPoint/Excel export, no AI summary, no new review engine, no new finding engine, no framework-specific compliance report, no raw JSON appendix for customers. +- **Permanent complexity imported**: A bounded management-report generation service or equivalent action, a deterministic payload/readiness builder over existing rendered report/review-pack truth, a PDF rendering adapter or approved existing renderer path, minimal StoredReport/canonical artifact extension if required, one OperationRun type or current canonical run mapping if required, audit action IDs if current audit actions are insufficient, localization keys, and focused Unit/Feature/Browser tests. +- **Why now**: Specs 356, 357, and 366 made the rendered report route, profile/disclosure policy, and management layout repo-real. The remaining sellability gap is durable PDF generation and audit, not another HTML layout pass. +- **Why not local**: A browser print instruction or another HTML button cannot create durable artifact truth, generation/download audit, OperationRun observability, or repeatable proof that the PDF respected the selected profile and disclosure boundary. +- **Approval class**: Core Enterprise. +- **Red flags triggered**: New PDF rendering seam, possible StoredReport/artifact extension, possible OperationRun type. Defense: the slice stays bound to one existing report family and one `customer_executive` profile, reuses current rendered-report/profile/disclosure truth, forbids a second report taxonomy, and includes a pre-implementation package/renderer gate. +- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12** +- **Decision**: approve as a bounded PDF artifact productization slice with a renderer/package governance gate before runtime implementation. + +## Candidate Selection Gate + +- **Selected candidate**: Spec 378 - Management Report PDF v1. +- **Source**: Direct user-provided attachment `/Users/ahmeddarrazi/.codex/attachments/9be0ca0f-b917-42f0-94d4-4a38f8477efc/pasted-text.txt`. +- **Why selected**: The active automatic queue says no safe next auto-prep target remains, but the user provided a concrete manual candidate that directly follows the implemented report productization chain. +- **Roadmap relationship**: Supports Governance-as-a-Service packaging, customer-safe review consumption, StoredReports/evidence artifact maturity, and management-ready reporting without opening auditor/technical delivery scope. +- **Close alternatives deferred**: + - Technical / Auditor Evidence Report v1: separate follow-up because it needs deeper evidence, appendix, and auditor-proof semantics. + - Report Delivery Center / Stored Reports UX: deferred until one management PDF artifact exists. + - Customer Review Workspace final polish: adjacent surface polish, not PDF artifact generation. + - Framework/compliance-specific reports: deferred until the general management PDF artifact path is proven. + - Scheduled/email delivery: deferred until generation, storage, access, and audit are stable. +- **Completed-spec guardrail result**: + - `specs/356-review-pack-pdf-html-renderer-v1/` has completed tasks/validation and is historical HTML/rendered-report context only. + - `specs/357-report-profiles-disclosure-policy-v1/` has completed tasks/validation and is historical profile/disclosure context only. + - `specs/366-management-report-layout-branded-report-themes-v1/` has implementation close-out and browser evidence for the management HTML report; it must not be rewritten. + - `specs/365-operations-ui-operator-actions-regression-gate/` and `specs/367-operationrun-actionability-system/` have completed actionability/OperationRun context and are not preparation targets. + - `specs/372-customer-auditor-surface-safety-pass/` and `specs/377-post-productization-browser-reaudit-closeout-gate/` carry completed task markers/validation artifacts and are context only. + - No `specs/378-management-report-pdf-v1/` package existed before this preparation run. +- **Smallest viable implementation slice**: one on-demand management PDF generation path from an existing current ready Review Pack/rendered report using `customer_executive`, with authorized generation/download, StoredReport or canonical artifact reference, OperationRun truth, audit, localization, and no raw technical appendix. +- **Gate result**: PASS. + +## Renderer Governance Decision *(2026-06-14)* + +- **Decision**: approved with controls. +- **Approved renderer**: Gotenberg 8 Chromium as an internal Docker-based PDF rendering service. +- **Approved integration boundary**: Laravel calls the renderer through a narrow `PdfRenderingGateway` / `PdfRendererClient`; no direct Chromium, Chrome, Puppeteer, Playwright, Node, or browser runtime may be installed or executed in the Laravel app/queue containers for production PDF rendering. +- **Layer ownership**: + - `PdfRendererClient`: low-level HTTP transport to the internal Gotenberg service, including multipart request assembly, timeout handling, response status mapping, size-limit rejection, and correlation id headers. + - `PdfRenderingGateway`: shared application boundary that accepts sanitized server-generated HTML/assets plus render options and returns a safe success/failure result without exposing raw renderer errors. + - `ManagementReportPdfRenderer`: Spec 378 domain adapter that converts the management report payload into sanitized HTML/assets and calls `PdfRenderingGateway`; it must not own HTTP transport, OperationRun lifecycle, storage persistence, or billing/invoice semantics. +- **Runtime implementation status**: renderer runtime/gateway boundary implemented for Sail/local and Laravel app config. Root `docker-compose.yml` now includes a pinned internal `gotenberg/gotenberg:8.34.0-chromium` service with no public port, health check, timeout/body-limit/concurrency/outbound controls, and app config now exposes `PdfRenderingGateway` / `PdfRendererClient` with safe result mapping and size-limit checks. No Dokploy config file exists in the repo; `docs/deployment-checklist.md` records the required Dokploy/runtime controls. Management report payload, storage, OperationRun, UI action, PDF templates, and report generation are not started. +- **Next required implementation task before report generation work**: confirm the Gotenberg runtime controls in staging/Dokploy, then implement management report payload/readiness/storage/OperationRun/UI/PDF generation through this gateway only. +- **Decision matrix**: `artifacts/spec378-pdf-renderer-decision-matrix.md`. +- **Constitution review**: `artifacts/spec378-constitution-renderer-gate-review.md`. +- **Package governance**: `docs/package-governance.md` now records Gotenberg 8 Chromium as an approved runtime service with controls. +- **Invoice / billing boundary**: Gotenberg is approved as shared internal PDF rendering infrastructure for report-style documents only. This does not approve legal invoice generation, German B2B e-invoicing, XRechnung, ZUGFeRD/Factur-X, GoBD archival, tax calculation, invoice numbering, or billing compliance. + +### SaaS Invoice / Billing Boundary + +The approved renderer is shared infrastructure, not shared billing truth: + +```text +Shared PDF Rendering Infrastructure + -> Gotenberg / Chromium Renderer + -> PdfRenderingGateway + -> Storage + -> Audit + -> OperationRun + +Domain-specific generators + -> ManagementReportGenerator + -> EvidenceReportGenerator + -> InvoiceDocumentGenerator + -> CreditNoteDocumentGenerator +``` + +- `StoredReport` is not `BillingDocument`. +- `Report ID` is not `Invoice Number`. +- `OperationRun ID` is not `Invoice Number`. +- `StoredReport ID` is not `Invoice Number`. +- Future billing specs must separately define `BillingDocument`, `InvoiceNumberAllocator`, `InvoiceTaxCalculator`, `EInvoiceXmlBuilder`, `ZugferdFacturXBuilder`, `XRechnungBuilder`, `InvoicePdfRenderer via PdfRenderingGateway`, `BillingDocumentArchive`, and `BillingAuditTrail` before any invoice implementation. + +## Repo Truth Reconciliation + +The user draft is accepted with these repo-based corrections: + +1. The existing management-ready report surface is the authenticated signed route `/admin/review-packs/{reviewPack}/report`, implemented by `ReviewPackRenderedReportController` and productized by Spec 366. Spec 378 must reuse that route/payload/theme/disclosure family rather than create a second report universe. +2. `ReportProfileRegistry` and `ReportDisclosurePolicy` already exist. The management PDF must use `customer_executive` or the repo-canonical equivalent, and invalid/unimplemented profile requests must fail closed or resolve through the existing policy. +3. There is no native PDF package in `apps/platform/composer.json`. Playwright exists as frontend/browser tooling, not an approved production PDF runtime. Gotenberg 8 Chromium is now approved with controls as an internal Docker-based renderer service; the Sail/local runtime service, app config, and gateway/client boundary now exist, while report generation remains gated until staging/Dokploy controls are confirmed. Any different renderer, new runtime package, or production browser dependency requires `docs/package-governance.md` review and spec/plan update before code changes. +4. `StoredReport` currently stores `workspace_id`, `managed_environment_id`, `report_type`, JSONB `payload`, `fingerprint`, and `previous_fingerprint`, and only supports existing permission/admin-role report types in `StoredReportResource`. Storing a PDF may require a narrow substrate extension; if a new persisted artifact entity is needed, the implementation must stop and split. +5. Review Pack generation already owns queued export and signed ZIP/download behavior. Spec 378 may reuse current Review Pack current-pack checks and signed URL patterns, but must not alter the ZIP contract or make historical/expired packs impersonate current management reports. +6. OperationRun truth exists through `OperationRunService`, `OperationRunType`, `OperationCatalog`, and Operations UI/actionability work. A new `report.management.generate` mapping is allowed only if current repo truth has no suitable canonical type; if added, it must be cataloged, tested, and surfaced consistently. +7. Download audit exists for Review Packs through `AuditActionId::ReviewPackDownloaded`. Management PDF generation/download must record distinct audit metadata if reusing that action would obscure artifact type, profile, format, or source review. + +## Problem Statement + +TenantPilot can now render a customer-safe management report in HTML, but a management meeting or customer handoff usually needs a stable PDF artifact. The current HTML/print path is useful for viewing, but it does not provide durable PDF artifact truth, artifact retention/discovery, generation observability, or auditable download history. + +Spec 378 closes only that productization gap: generate a management PDF from existing review/report truth, prove the report was profile-safe and disclosure-safe, store or reference it through the existing artifact model, and make access/download auditable. + +## Business / Product Value + +- MSPs can hand customers a management-ready governance PDF without exporting raw technical evidence. +- Customers and executives can understand current posture, key risks, decisions, accepted risks, evidence readiness, limitations, and provenance from one document. +- TenantPilot can prove when and how a report artifact was generated and downloaded. +- Future Technical/Auditor reports, delivery center, scheduled delivery, and service packaging get a smaller, safer foundation. + +## Primary Users / Operators + +- MSP service manager generating a customer governance report. +- IT lead, CISO, or executive reading the PDF. +- TenantPilot operator verifying that a report can be shared externally. +- Auditor-adjacent stakeholder who needs management context, not raw evidence detail. + +## Spec Scope Fields *(mandatory)* + +- **Scope**: workspace, tenant/managed-environment, canonical artifact/report view. +- **Primary Routes**: + - Existing rendered report route: `/admin/review-packs/{reviewPack}/report`. + - Existing Review Pack detail route: `/admin/workspaces/{workspace}/environments/{environment}/review-packs/{record}`. + - Existing Environment Review detail route: `/admin/workspaces/{workspace}/environments/{environment}/environment-reviews/{record}`. + - Optional existing Customer Review Workspace entrypoint: `/admin/reviews/workspace`. + - One new signed/authorized management PDF download route only if the existing download route cannot safely represent PDF format/profile/artifact identity. +- **Data Ownership**: + - Existing `EnvironmentReview`, `ReviewPack`, `EvidenceSnapshot`, `Finding`, `FindingException`, `StoredReport`, `OperationRun`, and `AuditLog` remain the authoritative data/artifact sources. + - Generated PDF is tenant-owned artifact truth bound to `workspace_id`, constitution-compliant tenant scope, and the source `managed_environment_id`. + - If the chosen persistence substrate does not already store `tenant_id` for tenant-owned artifact truth, implementation must add a NOT NULL `tenant_id` or stop and update this spec/plan with an explicit constitution-compliant exception before persistence changes. `managed_environment_id` alone is not sufficient for newly persisted tenant-owned artifact truth. + - No live Microsoft Graph/provider call may occur during report render or PDF generation. +- **RBAC**: + - Workspace membership, tenant entitlement, and managed-environment entitlement are required. + - Non-member / wrong workspace / wrong tenant / wrong environment access is deny-as-not-found. + - Member missing generation/download capability receives 403 after scope is established. + - Generation should reuse the existing review/export/manage capability path unless repo verification proves a new capability is required. + - Download should reuse current review-pack/stored-report view capability semantics unless repo verification proves a new download capability is required. + +For canonical-view specs: + +- **Default filter behavior when tenant-context is active**: The PDF is anchored to one concrete current ready Review Pack or its source Environment Review. It must not depend on hidden remembered environment/session state. +- **Explicit entitlement checks preventing cross-tenant leakage**: Generation, stored report lookup, artifact download, and signed route resolution must re-resolve workspace, tenant, and managed environment scope from the artifact source and actor, not from untrusted URL/query parameters alone. + +## 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 +- [x] New page/route added +- [ ] Navigation changed +- [ ] Filament panel/provider surface changed +- [x] New modal/drawer/wizard/action added +- [x] 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 + +## UI/Productization Coverage *(mandatory when UI Surface Impact is not "No UI surface impact")* + +- **Route/page/surface**: + - `ReviewPackRenderedReportController` and `resources/views/review-packs/rendered-report.blade.php` as source/render contract. + - `EnvironmentReviewResource` and/or `ReviewPackResource` header/detail action surfaces for generation/download. + - `StoredReportResource` only if the generated PDF appears in the stored-report register/detail. + - New signed/authorized PDF download route if required. +- **Current or new page archetype**: Existing rendered review report / evidence artifact detail / report download action. +- **Design depth**: Strategic customer-facing report surface. +- **Repo-truth level**: repo-verified for HTML/rendered report; spec-required for generated PDF artifact. +- **Existing pattern reused**: `ui-099-rendered-review-report.md`, `ui-042-review-pack-detail.md`, `ui-048-stored-report-detail.md`, `ReportProfileRegistry`, `ReportDisclosurePolicy`, `ReviewPackService::generateRenderedReportUrl()`, current signed download route pattern, current OperationRun/audit patterns. +- **New pattern required**: Bounded PDF artifact generation and download state only. No report center, theme editor, portal, or second report taxonomy. +- **Screenshot required**: yes for browser smoke of generation/download state and PDF/content smoke where tooling supports it. +- **Page audit required**: Update UI-099 and/or UI-042/UI-048 only if implementation materially changes hierarchy, action placement, or report artifact presentation; otherwise record a no-update rationale in close-out. +- **Customer-safe review required**: yes. The management PDF is customer-facing by default. +- **Dangerous-action review required**: yes by high-impact classification. "Generate management PDF" is not destructive, but it creates a durable customer-facing artifact and must be authorization-gated, readiness-gated, confirmation-gated via Filament confirmation, auditable, and clear about profile/disclosure. +- **Coverage files updated or explicitly not needed**: + - [ ] `docs/ui-ux-enterprise-audit/route-inventory.md` - update when a new signed/authorized PDF route is added. + - [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md` - update when the owner-surface action hierarchy, report artifact state, or customer-facing report surface materially changes. + - [ ] `docs/ui-ux-enterprise-audit/page-reports/ui-099-rendered-review-report.md` - update when rendered-report/PDF source content or customer-facing report presentation materially changes. + - [ ] `docs/ui-ux-enterprise-audit/page-reports/ui-042-review-pack-detail.md` - update when the Review Pack detail action/download state is used or changed. + - [ ] `docs/ui-ux-enterprise-audit/page-reports/ui-048-stored-report-detail.md` - update when the generated PDF appears in StoredReport registry/detail. + - [ ] `docs/ui-ux-enterprise-audit/strategic-surfaces.md` - update only if management PDF becomes a newly classified strategic surface beyond the existing rendered-report family. + - [ ] `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md` - update only if deferred Delivery Center, Technical/Auditor report, or artifact lifecycle follow-up scope changes. + - [ ] `docs/ui-ux-enterprise-audit/unresolved-pages.md` - update only if implementation exposes a new unresolved reachable surface. + - [ ] `N/A - no reachable UI surface impact` +- **No-impact rationale when applicable**: N/A. + +## Cross-Cutting / Shared Pattern Reuse + +- **Cross-cutting feature?**: yes. +- **Interaction class(es)**: report generation, artifact download, status/readiness messaging, header actions, report viewer, audit, OperationRun links. +- **Systems touched**: `ReportProfileRegistry`, `ReportDisclosurePolicy`, `ReviewPackRenderedReportController`, `ReviewPackService`, `StoredReport`, `StoredReportResource`, `OperationRunService`, `OperationRunType`, `OperationCatalog`, audit logging, localization, Review Pack and Environment Review owner surfaces. +- **Existing pattern(s) to extend**: current rendered report payload, current profile/disclosure policy, current signed URL pattern, current Review Pack current-pack guards, current OperationRun start/completion UX, current stored-report registry if it can represent this artifact. +- **Shared contract / presenter / builder / renderer to reuse**: existing report profile/disclosure/theme state first. Add only a bounded management-report payload/PDF adapter when existing controller-local state is insufficient to test generation safely. +- **Why the existing shared path is sufficient or insufficient**: Existing paths are sufficient for report content, profile, disclosure, layout, and signed report URLs. They are insufficient for durable PDF artifact storage, generation readiness, artifact download state, and generation/download audit. +- **Allowed deviation and why**: A bounded `ManagementReportPayloadBuilder`, `ManagementReportPdfRenderer`, and `ManagementReportGenerationService` or equivalent names are allowed if they keep PDF generation out of Blade/controller code and avoid duplicating report truth. The domain renderer may consume `PdfRenderingGateway`, but HTTP transport and Gotenberg-specific request construction belong to `PdfRendererClient`. +- **Consistency impact**: HTML report, PDF artifact, stored artifact metadata, OperationRun result, and audit metadata must agree on profile, source review, source Review Pack, evidence basis, generated time, actor, and limitations. +- **Review focus**: Confirm no raw provider payloads, internal MSP fields, signed URLs, access tokens, stack traces, SQL errors, or serialized jobs appear in the PDF body or audit metadata. + +## OperationRun UX Impact + +- **Touches OperationRun start/completion/link UX?**: yes. +- **Shared OperationRun UX contract/layer reused**: `OperationRunService`, `OperationRunType`/`OperationCatalog` if a new type is needed, `OperationRunLinks`, `OperationUxPresenter`, and current Operations UI/actionability patterns. +- **Delegated start/completion UX behaviors**: queued toast/link, artifact link, run-enqueued event, blocked/failed-to-start messaging, tenant/workspace-safe operation URL resolution, and terminal notification policy must use existing shared paths. +- **Local surface-owned behavior that remains**: only readiness explanation and the generation input/action placement. +- **Queued DB-notification policy**: explicit opt-in only if current shared OperationRun UX contract supports it; otherwise no running DB notification. +- **Terminal notification path**: central lifecycle mechanism. +- **Exception required?**: none expected. If PDF generation is implemented synchronously and under 2 seconds, implementation must still audit and justify skipping OperationRun; however the preferred v1 path is queued/observable because artifact rendering/storage can fail independently. + +## Provider Boundary / Platform Core Check + +- **Shared provider/platform boundary touched?**: yes, indirectly through report content and source metadata. +- **Boundary classification**: platform-core report/artifact truth with provider-owned evidence details when already present in review/pack summaries. +- **Seams affected**: report profile, evidence basis, scope/limitations, source metadata, StoredReport artifact source descriptors, OperationRun type/catalog labeling. +- **Neutral platform terms preserved or introduced**: report, profile, audience, evidence basis, limitation, managed environment, workspace, generated artifact, provenance, operation. +- **Provider-specific semantics retained and why**: Existing Microsoft/provider details may appear only as summarized evidence/source context already allowed by `customer_executive` disclosure policy. No new provider-specific PDF renderer logic is introduced. +- **Why this does not deepen provider coupling accidentally**: PDF generation consumes stored review/report truth only; it does not call Graph, inspect raw provider payloads, or make Microsoft-specific fields mandatory in platform-core artifact identity. +- **Follow-up path**: follow-up-spec for Technical/Auditor Evidence Report if detailed provider/evidence appendix is needed. + +## 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 | +|---|---|---|---|---|---|---| +| Generate management PDF action on Environment Review / Review Pack owner surface | yes | Native Filament action | header action, status/readiness, OperationRun start | detail header, modal/notification | no | High-impact artifact creation, not destructive | +| PDF download action/link | yes | Native URL/action plus signed route | artifact download, report viewer | detail, URL, stored artifact | no | Must be scope-safe and audit download | +| Rendered report source/PDF payload mapping | yes | Existing custom report canvas plus server-side PDF renderer | report viewer, disclosure | report payload, PDF artifact | `UI-EX-001` inherited for print/report canvas only | Do not create new visual language beyond PDF layout constraints | +| Stored report list/detail representation, if used | yes | Existing Filament resource | evidence/report registry | table/detail/readiness state | no | Preserve read-only registry semantics | + +## 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 PDF artifact | Primary Decision Surface for customer report consumption | Executive or MSP decides what risks, decisions, and evidence limitations matter | cover, executive summary, posture, decisions, top risks, accepted risks, evidence readiness, limitations, provenance | technical/auditor evidence is out of scope | Primary because the PDF must stand alone outside the app | Follows review/report handoff workflow | Avoids opening ZIP/JSON/admin pages for management context | +| Generate action | Secondary Workflow Action | Operator decides whether the current review/pack is ready to create a durable customer artifact | readiness, profile, source review/pack, limitations, blocked reason | OperationRun detail and stored artifact detail | Secondary because it initiates artifact creation | Follows review pack/report owner surfaces | One action, disabled when not ready | +| Stored report/detail entry, if used | Tertiary Evidence / Diagnostics | Operator inspects generated artifact metadata or re-downloads | artifact type, generated at, profile, source review/pack, lifecycle | payload/source descriptors collapsed | Tertiary because management reading happens in PDF | Follows StoredReport evidence registry | Avoids raw payload as first-read path | + +## 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 executive PDF | customer-read-only, operator-MSP | management summary, posture, decisions, top risks, accepted risks, evidence readiness, limitations, provenance | none by default | raw JSON/provider payloads absent | review recommended next actions | technical appendix, internal notes, raw context | PDF derives from existing profile/disclosure state | +| Generate/download UI | operator-MSP | readiness, profile, source artifact, generated/download status | OperationRun link and failure reason | raw stack/SQL/provider payload absent | generate or download management PDF | failed render diagnostics in logs/run detail only | OperationRun, audit, and artifact metadata use same IDs | + +## 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 | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| Environment Review / Review Pack detail action | Detail / Action | Report artifact generation | Generate management PDF or download existing PDF | Existing detail page | N/A | Header secondary or More if another primary exists | N/A | existing review/review-pack routes | existing review/review-pack routes | workspace + managed environment + profile | Management report PDF | readiness, profile, source review/pack, limitations | none | +| PDF download route | Artifact Download | Signed/authorized download | Download artifact | Direct signed/authorized URL | N/A | N/A | N/A | N/A | signed artifact URL | workspace + managed environment in resolved artifact | Management report PDF | provenance and safe filename/headers | none | +| StoredReport entry, if reused | Registry / Detail | Read-only report registry | Open/download current report | Clickable row/detail | required if listed | one safe shortcut max | N/A | existing stored reports route | stored report view | workspace + managed environment | Stored report | report type, profile, generated time, source review | none | + +## 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 management PDF action | Tenant/MSP operator | Decide whether to create a durable customer-facing report artifact | Detail action | Is this review/pack ready and safe to generate as a management PDF? | source review/pack, profile, readiness, limitation/blocked reason | run failure details | readiness, generation status, disclosure, artifact availability | TenantPilot only; no Microsoft tenant mutation | Generate management PDF, View operation, Download PDF | none; high-impact artifact creation is confirmation/readiness-gated | +| Management PDF | Customer/executive/MSP | Decide management follow-up | Report artifact | What is the governance state and what decisions are needed? | executive summary, posture, decisions, risks, accepted risks, evidence readiness, limitations, provenance | none in v1 | evidence readiness, risk/posture, acceptance/decision state | N/A read-only artifact | Download/view/share outside app | none | + +## Proportionality Review *(mandatory when structural complexity is introduced)* + +- **New source of truth?**: yes, a generated PDF artifact is durable artifact truth once created. It remains derived from source review/pack truth and must carry provenance. +- **New persisted entity/table/artifact?**: yes, generated PDF artifact storage/reference is required. Prefer a narrow extension of existing `StoredReport`/storage substrate; a new table/entity requires a spec/plan update before implementation. +- **New abstraction?**: likely yes, a bounded payload builder, renderer adapter, and generation service to keep queries/business logic out of Blade/controllers/jobs. +- **New enum/state/reason family?**: possibly one operation type/report type; no broad lifecycle/status family beyond current artifact/report states. +- **New cross-domain UI framework/taxonomy?**: no. +- **Current operator problem**: The product cannot produce and audit a durable management PDF artifact from a released review/report. +- **Existing structure is insufficient because**: The existing rendered report is HTML/print-only, `StoredReport` has no PDF artifact/download model today, and Review Pack ZIP download audit does not identify a management PDF profile/format artifact. +- **Narrowest correct implementation**: One management PDF profile, one source family, one artifact output, one generation action, one download path, and focused storage/audit/OperationRun integration. +- **Ownership cost**: PDF renderer maintenance, artifact storage/retention considerations, generation/download tests, browser/content smoke, audit/action ID maintenance, and deployment worker/storage notes. +- **Alternative intentionally rejected**: Tell users to use browser print. That avoids code but does not provide stored artifact truth, generation audit, download audit, or consistent server-side proof. +- **Release truth**: Current-release truth; this directly follows repo-real report profile/layout work. + +### Compatibility posture + +This feature assumes a pre-production environment. No legacy PDF/report compatibility is required. If storage schema changes are needed, implement the narrow current shape rather than compatibility shims. + +## Testing / Lane / Runtime Impact + +- **Test purpose / classification**: Unit, Feature, Filament/Livewire action tests, Browser/PDF content smoke. +- **Validation lane(s)**: fast-feedback, confidence, browser; PostgreSQL lane only if migrations/indexes/storage metadata require database-specific proof. +- **Why this classification and these lanes are sufficient**: Unit tests prove deterministic payload/readiness/disclosure; Feature tests prove authorization, OperationRun, storage, audit, and download; Browser/content smoke proves real operator generation flow and management PDF content/leakage boundaries. +- **New or expanded test families**: Spec378 unit/feature/browser files under Reports/ReviewPack/StoredReports as implementation decides. +- **Fixture / helper cost impact**: Reuse Spec 357/366 rendered-report fixtures and current review-output browser fixtures; any new helper must be local to Spec378 unless three current specs need it. +- **Heavy-family visibility / justification**: one explicit browser/content smoke is justified because the output is customer-facing and document-format dependent. +- **Special surface test profile**: report-viewer / customer-facing artifact surface plus high-impact artifact generation action. +- **Standard-native relief or required special coverage**: special coverage required for PDF text/content, leakage absence, signed/authorized download, and disabled/not-ready generation state. +- **Reviewer handoff**: Reviewers must confirm the renderer path is approved, no raw technical leakage appears, operation/audit/artifact metadata agree, and no Report Delivery Center/auditor report scope slipped in. +- **Budget / baseline / trend impact**: low to moderate; one browser/content smoke and artifact fixture may add runtime. Record budget follow-up if PDF rendering materially slows browser or confidence lanes. +- **Escalation needed**: document-in-feature for approved renderer/package decision; follow-up-spec for Technical/Auditor Report, Delivery Center, scheduled delivery, or artifact lifecycle expansion. +- **Active feature PR close-out entry**: Guardrail + Smoke Coverage. +- **Planned validation commands**: + - `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec378` + - `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec357` + - `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec366` + - `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec378ManagementReportPdfSmokeTest.php --compact` + - `cd apps/platform && ./vendor/bin/sail pint --dirty` + - `git diff --check` + +## User Scenarios & Testing + +### User Story 1 - Generate a management PDF from a ready review (Priority: P1) + +As an MSP operator, I can generate a customer-executive management PDF from a current ready Review Pack or Environment Review so that I can provide a stable customer-facing governance artifact. + +**Why this priority**: This is the core value of the feature. + +**Independent Test**: Given a ready current Review Pack and authorized actor, invoking the action creates/queues generation, completes with a stored PDF artifact, records OperationRun/audit metadata, and exposes a download link. + +### User Story 2 - Read a decision-first PDF without technical leakage (Priority: P1) + +As a customer executive, I can read the PDF and understand posture, decisions, top risks, accepted risks, evidence readiness, limitations, next actions, and provenance without raw Graph data, secrets, SQL errors, stack traces, or internal MSP details. + +**Independent Test**: Extract PDF text or inspect rendered artifact content and assert required chapter labels are present while forbidden strings/payload markers are absent. + +### User Story 3 - Enforce scope and capability on generation/download (Priority: P1) + +As a platform owner, I need cross-workspace and cross-environment report access to fail closed so that generated customer artifacts do not leak tenant data. + +**Independent Test**: Users without workspace/environment entitlement cannot generate or download; scoped members without capability get 403 after scope; no artifact is created for denied generation. + +### User Story 4 - Fail safely when rendering or storage fails (Priority: P2) + +As an operator, I need failed PDF generation to leave clear OperationRun/audit evidence and no half-visible false report. + +**Independent Test**: Simulate renderer/storage failure; OperationRun ends failed or blocked with safe reason, no ready artifact/download is exposed, and user-facing copy avoids stack traces. + +## Functional Requirements + +- **FR-378-001**: The system MUST generate a management PDF only from an existing current ready Review Pack or repo-equivalent ready Environment Review source. +- **FR-378-002**: The generated PDF MUST use `customer_executive` or repo-canonical management profile by default. +- **FR-378-003**: The system MUST apply `ReportDisclosurePolicy` or its repo-canonical successor before rendering PDF content. +- **FR-378-004**: The PDF MUST include cover, executive summary, governance posture, key decisions, top risks/findings, accepted risks, evidence readiness, scope and limitations, recommended next actions, report provenance, and a short method summary. +- **FR-378-005**: The PDF MUST NOT include raw provider payloads, raw JSON appendix, access tokens, refresh tokens, client secrets, signed URLs, SQL errors, stack traces, serialized jobs, internal MSP-only notes, or raw operation context. +- **FR-378-006**: The PDF artifact MUST be stored or referenced through `StoredReport` or the repo-canonical report/artifact substrate with workspace, tenant scope, managed environment, source review/pack, profile, format, generated-at, generated-by, and operation-run provenance. +- **FR-378-007**: Generation MUST create or reuse a canonical `OperationRun` unless implementation proves a synchronous no-run path is safer and updates this spec/plan before code changes. +- **FR-378-008**: Generation and download MUST write audit evidence with actor, workspace, tenant scope, managed environment, source review/pack, artifact/report id, operation run id, profile, format, and safe metadata. +- **FR-378-009**: Download MUST be signed and/or server-authorized, scope-safe, and short-lived when URL signing is used. +- **FR-378-010**: Generation action MUST be hidden or disabled with a safe reason when source review/pack is missing, not ready, expired, not current, profile-blocked, disclosure-blocked, or unauthorized. +- **FR-378-011**: Failure states MUST not expose a ready artifact or stale download link. +- **FR-378-012**: EN and DE labels MUST cover PDF chapter titles, generation/download actions, readiness reasons, and core disclosure labels through existing localization seams. +- **FR-378-013**: The `Generate management PDF` action MUST execute through a Filament action with server-side authorization and explicit confirmation (`->requiresConfirmation()` or the repo-approved equivalent) before artifact creation starts. +- **FR-378-014**: PDF rendering MUST use the approved internal Gotenberg 8 Chromium service through a Laravel `PdfRenderingGateway` / `PdfRendererClient`, unless this spec/plan are updated through package/runtime governance before implementation. + +## Non-Functional Requirements + +- **NFR-378-001**: No live Graph/provider/API calls during render, generation, or download. +- **NFR-378-002**: PDF rendering MUST avoid remote fonts/assets and must be deterministic enough for tests and offline worker execution. +- **NFR-378-003**: PDF artifact storage MUST use a private disk or repo-approved private object storage path. +- **NFR-378-004**: PDF generation MUST be idempotent for the same source review/pack/profile/fingerprint when practical, or explicitly explain why a new artifact per generation is required. +- **NFR-378-005**: Queue/runtime failures MUST be observable through OperationRun and logs without secret leakage. +- **NFR-378-006**: Any new package/runtime dependency MUST pass package governance, license/security/version checks, and deployment impact review before implementation. +- **NFR-378-007**: Filament v5 and Livewire v4.0+ compliance MUST be preserved; no Livewire v3 or Filament v3/v4 APIs. +- **NFR-378-008**: Laravel panel provider registration MUST remain in `apps/platform/bootstrap/providers.php`; no panel provider changes are planned. +- **NFR-378-009**: Global search posture MUST remain explicit. `StoredReportResource` currently disables global search and has a View page; no globally searchable management report resource is introduced. +- **NFR-378-010**: No new Filament assets are planned. If implementation registers assets unexpectedly, deployment must document `cd apps/platform && php artisan filament:assets`. +- **NFR-378-011**: Gotenberg MUST be internal-network only, pinned to an explicit Gotenberg 8 Chromium image tag or digest, health-checked, timeout-limited, request/output-size-limited, and unavailable-safe before management PDF generation can run. +- **NFR-378-012**: Spec 378 v1 MUST NOT use user-provided URL rendering, remote fonts/assets, signed URLs in HTML, secrets in HTML, raw provider payloads in HTML, or public renderer endpoints. + +## Acceptance Criteria + +- **AC-378-001**: Authorized operator can generate a management PDF from a current ready review/pack. +- **AC-378-002**: Generated PDF uses the customer executive profile and disclosure policy. +- **AC-378-003**: PDF contains decision-first management sections and provenance. +- **AC-378-004**: PDF artifact is stored or referenced through the repo-canonical report/artifact substrate. +- **AC-378-005**: Generation is observable through OperationRun or an explicitly justified no-run path. +- **AC-378-006**: Generation and download are audit-visible. +- **AC-378-007**: Cross-workspace, cross-tenant, and cross-environment generation/download are denied. +- **AC-378-008**: Missing readiness or blocked disclosure prevents generation or produces a clearly limited/blocked state according to policy. +- **AC-378-009**: PDF text/content smoke proves required sections present and forbidden raw/internal content absent. +- **AC-378-010**: EN/DE labels are present or routed through existing localization fallback. +- **AC-378-011**: Spec357, Spec366, Review Pack, Customer Review Workspace, and OperationRun regressions remain green or unrelated failures are documented honestly. + +## Success Criteria + +- Operators can create a management PDF without manual browser print instructions. +- The PDF can stand alone as a customer-facing management artifact. +- Audit and OperationRun records are sufficient to answer who generated/downloaded what, when, for which review/pack/profile. +- No technical/auditor evidence dump appears in v1. + +## Risks + +- **PDF runtime risk**: Gotenberg 8 Chromium is approved with controls and the Sail/local service, app config, and gateway/client boundary now exist, but staging/Dokploy controls still need validation before report generation is enabled. Mitigation: confirm internal-only service wiring, health check, timeout/size controls, and queue failure behavior on staging before enabling generation. +- **Artifact model risk**: Existing `StoredReport` may be too narrow for file artifacts. Mitigation: extend narrowly or stop/split before creating a new artifact entity. +- **Disclosure leakage risk**: PDF renderer may include hidden HTML or raw fields. Mitigation: build payload before render, test absence of forbidden content, and keep raw sections out of `customer_executive`. +- **Queue/storage risk**: PDF rendering may fail independently of source review readiness. Mitigation: OperationRun failed/blocked outcomes, private storage, no ready artifact on failure. +- **Scope creep risk**: Auditor/technical/report-center features are attractive adjacent work. Mitigation: explicit non-goals and follow-up candidates. + +## Assumptions + +- The source of truth is an existing ready current Review Pack generated from an Environment Review. +- The existing HTML rendered report is the preferred content/layout source for management sections. +- `customer_executive` remains the default management profile. +- The product is still pre-production, so narrow schema adjustments may replace current shape without compatibility shims if approved by the implementation spec/plan. +- Local/Sail and Dokploy deployment can provide a queue worker and private artifact storage path. + +## Open Questions + +- Renderer governance decision is complete. Gotenberg 8 Chromium is approved with controls as the internal renderer service. +- Runtime/gateway boundary implementation has started and the Sail/local service plus Laravel app config now exist. No Dokploy config file is present in the repo; deployment checklist controls are documented for staging. +- Management report payload, storage, OperationRun, UI action, PDF templates, and report generation remain out of scope until staging/runtime controls are confirmed and the next implementation phase begins. + +## Out Of Scope + +- Technical Evidence Report PDF. +- Auditor Evidence Report PDF. +- Full report delivery center. +- Scheduled/email/Teams delivery. +- Public share links or customer portal. +- New Review Engine or Finding Engine. +- AI-generated summaries. +- Word/DOCX, PowerPoint, Excel, or raw JSON customer appendix. +- Framework-specific compliance reports. +- Broad StoredReports UX redesign beyond the minimum artifact representation needed for this PDF. + +## Follow-Up Spec Candidates + +- Spec 379 - Technical / Auditor Evidence Report v1. +- Report Delivery Center / Stored Reports UX. +- Scheduled Report Delivery and Approval v1. +- Customer Review Workspace final PDF handoff polish. +- Governance Service Packaging report cadence and co-branding follow-up. diff --git a/specs/378-management-report-pdf-v1/tasks.md b/specs/378-management-report-pdf-v1/tasks.md new file mode 100644 index 00000000..3951d859 --- /dev/null +++ b/specs/378-management-report-pdf-v1/tasks.md @@ -0,0 +1,221 @@ +# Tasks: Spec 378 - Management Report PDF v1 + +**Input**: `specs/378-management-report-pdf-v1/spec.md`, `specs/378-management-report-pdf-v1/plan.md` +**Prerequisites**: Spec and plan are complete. Renderer governance is approved with controls for Gotenberg 8 Chromium. Renderer runtime/gateway work must complete and be staging-validated before report generation work. +**Tests**: Required. Use Pest 4 Unit, Feature/Filament action, and Browser/content smoke. PostgreSQL lane is required if migrations/indexes are introduced. +**Implementation note**: Renderer governance and the first runtime/gateway boundary are implemented. Downstream report generation tasks remain pending for later `/spec-kit-implementation-loop` passes. + +## Phase 1: Repo Verification And Hard Gates + +**Purpose**: Confirm current repo truth before runtime edits. + +- Gate stop note (2026-06-14): branch `378-management-report-pdf-v1`, HEAD `f1eadadf docs: add spec 377 post-productization browser reaudit closeout gate (#448)`, dirty baseline was the untracked active spec directory `specs/378-management-report-pdf-v1/`. Runtime implementation stopped before code edits because T008 initially found no approved production-safe PDF renderer in `apps/platform/composer.json`, Playwright is dev/browser tooling only in `apps/platform/package.json`, and `docs/package-governance.md` did not yet approve a production PDF renderer. +- Renderer governance update (2026-06-14): T008 has been reviewed and updated through package/runtime governance. Gotenberg 8 Chromium is approved with controls as an internal Docker service, documented in `artifacts/spec378-pdf-renderer-decision-matrix.md`, `artifacts/spec378-gotenberg-security-controls.md`, `artifacts/spec378-constitution-renderer-gate-review.md`, `spec.md`, `plan.md`, and `docs/package-governance.md`. No runtime service, app code, report generation, or tests were implemented by this governance update. +- Runtime/gateway update (2026-06-14): Sail/local runtime target is root `docker-compose.yml`; no Dokploy config file is present in the repo, so `docs/deployment-checklist.md` records staging/production controls. `apps/platform/.env.example`, `apps/platform/config/tenantpilot.php`, and `apps/platform/app/Services/Pdf/*` now define the Gotenberg app config and gateway/client boundary. Report payload, storage, OperationRun, UI action, download route, PDF template, and browser smoke remain pending. +- [x] T001 Re-read `specs/378-management-report-pdf-v1/spec.md`, `plan.md`, `tasks.md`, and `checklists/requirements.md`. +- [x] T002 Record branch, HEAD, dirty state, and touched-file baseline in the active implementation close-out notes before runtime edits. +- [x] T003 Verify the existing rendered report source paths: `ReviewPackRenderedReportController`, `rendered-report.blade.php`, `ReportProfileRegistry`, `ReportDisclosurePolicy`, `ReportThemeResolver`, and `ReviewPackService`. +- [x] T004 Verify current Review Pack source readiness/current-pack rules in `ReviewPackService`, `ReviewPackDownloadController`, `EnvironmentReviewResource`, `ReviewPackResource`, and related tests. +- [x] T005 Verify current `StoredReport` schema/model/resource capability map and decide whether a narrow extension can represent a management PDF artifact. +- [x] T006 Verify current `OperationRunType`, `OperationCatalog`, `OperationRunService`, and Operations UI/actionability rules before adding or mapping `report.management.generate`. +- [x] T007 Verify current audit action IDs and audit logger usage for review exports/downloads; identify whether distinct management PDF generation/download action IDs are required. +- [x] T008 Hard gate: verify an approved production-safe PDF renderer path. If no approved path exists, stop before runtime edits, run package-governance review, and update `spec.md`/`plan.md`. +- [x] T009 Confirm no live Graph/provider call is required for payload, render, generation, or download. +- [ ] T010 Decide the first owner surface for v1 generation: Environment Review detail, Review Pack detail, or Customer Review Workspace. Prefer Environment Review or Review Pack detail unless repo verification proves otherwise. + +## Phase 1A: Renderer Governance Decision Completed + +**Purpose**: Document the renderer/package/runtime decision without implementing report generation. + +- [x] G001 Review the renderer/package gate after the manual `gate stop valid` decision. +- [x] G002 Evaluate renderer candidates: Gotenberg 8 Chromium, Spatie Browsershot / Spatie Laravel PDF, dompdf / laravel-dompdf, wkhtmltopdf / Snappy, and PrinceXML. +- [x] G003 Create `specs/378-management-report-pdf-v1/artifacts/spec378-pdf-renderer-decision-matrix.md`. +- [x] G004 Document required Gotenberg security controls in `specs/378-management-report-pdf-v1/artifacts/spec378-gotenberg-security-controls.md`. +- [x] G005 Document invoice/billing/e-invoice boundary in `spec.md`, `plan.md`, and `specs/378-management-report-pdf-v1/artifacts/spec378-constitution-renderer-gate-review.md`. +- [x] G006 Update `docs/package-governance.md` with Gotenberg 8 Chromium approved-with-controls runtime service entry. +- [x] G007 Update `spec.md` and `plan.md` with the selected renderer and pending runtime-config boundary. +- [x] G008 Identify exact runtime config targets before editing: root `docker-compose.yml` for Sail/local service wiring, Dokploy/deployment config or documentation location if present, and explicit `not present` notes for missing deployment config files. +- [x] G009 Add the Gotenberg runtime service to the identified Sail/Dokploy config target(s) with a pinned image, no public port, health check, timeouts, request/output limits, and outbound/file-access controls. +- [x] G010 Add renderer app config for base URL, timeout, body/output limits, unavailable behavior, and correlation id handling. +- [x] G011 Add failing renderer gateway/client tests for health check, renderer unavailable, timeout, invalid response, size-limit rejection, and safe error mapping before completing the gateway/client. +- [x] G012 Implement the Laravel `PdfRenderingGateway` / `PdfRendererClient` as the only production renderer integration boundary. +- [ ] G013 Confirm Gotenberg runtime controls in staging before enabling report PDF generation. + - Remaining validation note: `docker compose config --quiet` passes locally, but a direct pull/probe of `gotenberg/gotenberg:8.34.0-chromium` was cancelled after staying silent and returning a Docker credential interruption. Staging/Dokploy must verify the configured `/health` healthcheck command runs inside the pinned image. + +## Phase 2: Test Design First + +**Purpose**: Add failing or pending proof for the core behavior before implementation. + +- [ ] T011 Add Unit coverage for management report payload sections: cover, executive summary, governance posture, key decisions, top findings, accepted risks, evidence readiness, limitations, next actions, provenance, and method summary. +- [ ] T012 Add Unit coverage proving `customer_executive` profile and disclosure policy hide technical appendix, raw provider payloads, internal MSP fields, signed URLs, and raw operation context. +- [ ] T013 Add Unit coverage for readiness decisions: missing review, non-current pack, expired pack, not-ready pack, invalid/unimplemented profile, disclosure blocker, missing evidence limitation, and unauthorized actor. +- [x] T014 Add Unit or Feature coverage for Gotenberg gateway renderer failure mapping to safe failed/blocked result without stack trace leakage. +- [ ] T015 Add Feature coverage for authorized generation from a ready current Review Pack or Environment Review source. +- [ ] T016 Add Feature coverage for artifact storage or canonical artifact reference with workspace, tenant scope, managed environment, source review, source pack, profile, format, generated-by, generated-at, and operation-run provenance. +- [ ] T017 Add Feature coverage for generation audit metadata and failed generation audit/OperationRun evidence. +- [ ] T018 Add Feature coverage for successful download audit metadata. +- [ ] T019 Add authorization coverage for no workspace membership, wrong tenant, and wrong managed environment returning 404. +- [ ] T020 Add authorization coverage for scoped member without generation/download capability returning 403 after scope is established. +- [ ] T021 Add Feature coverage proving denied generation creates no artifact and exposes no download. +- [ ] T022 Add Feature coverage proving download route re-resolves artifact/source scope and rejects stale, expired, cross-workspace, cross-tenant, or cross-environment access. +- [ ] T023 Add Filament/Livewire action coverage for visibility, disabled reason, notification, and operation/download link behavior on the chosen owner surface. +- [ ] T024 Add Browser/content smoke coverage for generate or download flow when local fixtures support it. +- [ ] T025 Add Browser/content smoke assertions that PDF text includes required management chapters and excludes forbidden strings: `SQLSTATE`, `access token`, `client secret`, `raw Graph payload`, `internal_msp_review`, serialized job markers, and signed URLs. + +## Phase 3: Payload, Readiness, And Disclosure + +**Purpose**: Build deterministic report content from existing source truth only. + +- [ ] T026 Implement a bounded management report payload builder or equivalent under the existing report/review-pack support namespace. +- [ ] T027 Build payload only from existing `ReviewPack`, `EnvironmentReview`, `EvidenceSnapshot`, findings summary, accepted risk/exception, decision summary, and rendered-report support truth. +- [ ] T028 Ensure the payload builder does not query from Blade/PDF templates and does not call Graph/provider services. +- [ ] T029 Resolve the effective profile through `ReportProfileRegistry` and default to `customer_executive`. +- [ ] T030 Apply `ReportDisclosurePolicy` before rendering and fail closed for unknown/unimplemented profile input. +- [ ] T031 Implement readiness/blocked reason mapping for source missing, not current, not ready, expired, evidence limitation, disclosure blocker, Gotenberg renderer unavailable, storage unavailable, and authorization failure. +- [ ] T032 Ensure missing evidence either blocks generation or appears as a management-visible limitation according to existing review/report policy; document the chosen behavior in code/tests. +- [ ] T033 Keep next actions derived from existing report/review/finding/evidence data; do not invent AI or unsupported recommendations. + +## Phase 4: Artifact Storage And Idempotency + +**Purpose**: Store or reference the generated PDF without inventing a report center. + +- [ ] T034 If current fields are insufficient, add a narrow reversible migration for the existing report/artifact substrate rather than a new table. +- [ ] T035 If extending `StoredReport`, add only fields required for PDF artifact identity, tenant scope, source environment, source review/pack, profile, format, status, file disk/path, generated by, generated at, and operation run provenance; newly persisted tenant-owned artifact truth must include NOT NULL `tenant_id` unless a constitution-compliant exception is added before implementation. +- [ ] T036 Add or update `StoredReport` constants/casts/relationships for management PDF only if `StoredReport` is the chosen substrate. +- [ ] T037 Update `StoredReportResource` capability/report type handling only if management PDFs are listed/viewed there; keep global search disabled. +- [ ] T038 Store PDF artifacts on a private disk/path and avoid public filenames or secret-bearing paths. +- [ ] T039 Implement source/profile/fingerprint idempotency or document why repeated generation intentionally creates separate artifacts. +- [ ] T040 Ensure no ready/downloadable artifact is exposed when generation fails before storage commit. +- [ ] T041 If any schema/index behavior is PostgreSQL-specific, add PostgreSQL lane proof. + +## Phase 5: OperationRun And Audit + +**Purpose**: Make generation observable and accountable. + +- [ ] T042 Add or map canonical operation type `report.management.generate` only if no existing type fits; update `OperationRunType`, `OperationCatalog`, labels, guards, and tests consistently. +- [ ] T043 Start/queue generation through `OperationRunService` unless a spec/plan update explicitly approves a synchronous no-run path. +- [ ] T044 Route queued toast/link/artifact-link/start-failure behavior through existing OperationRun UX patterns. +- [ ] T045 Mark successful generation completed/succeeded with safe results metadata. +- [ ] T046 Mark renderer/storage/source blocked cases completed/failed or completed/blocked with safe reason codes according to existing OperationRun outcome semantics. +- [ ] T047 Do not write `context.reconciliation` unless an actual reconciliation adapter runs. +- [ ] T048 Record generation audit with actor, workspace, tenant scope, managed environment, source review, source pack, stored artifact/report id, operation run id, profile, format, generated at, and safe metadata. +- [ ] T049 Record download audit with actor, workspace, tenant scope, managed environment, artifact/report id, source review/pack, profile, format, downloaded at, and safe request metadata if available. +- [ ] T050 Confirm audit metadata excludes secrets, signed URLs, raw provider payloads, raw operation context, stack traces, and SQL errors. + +## Phase 6: UI Action And Download Route + +**Purpose**: Expose one clear, safe generation/download flow. + +- [ ] T051 Add the `Generate management PDF` action to the selected owner surface using `Action::make(...)->action(...)`. +- [ ] T052 Gate the action with server-side authorization, readiness, and scope checks; UI state is not security. +- [ ] T053 Implement explicit Filament confirmation for high-impact artifact creation using `->requiresConfirmation()` or the repo-approved equivalent before generation starts. +- [ ] T054 Show disabled/blocked reason when source review/pack is not ready, expired, not current, profile-blocked, disclosure-blocked, unauthorized, or renderer unavailable. +- [ ] T055 If a matching ready PDF artifact already exists, prefer a `Download management PDF` or `Open management PDF` affordance over duplicate generation. +- [ ] T056 If generation is queued/running, show a single canonical `View operation` or equivalent operation link through existing link helpers. +- [ ] T057 Implement a signed and/or server-authorized PDF download route if existing routes cannot safely represent management PDF format/profile/artifact identity. +- [ ] T058 In the download route/controller, re-resolve workspace, tenant scope, managed environment, source review/pack, artifact status, and actor capability before returning file bytes. +- [ ] T059 Set safe PDF download filename and headers without exposing internal IDs as the primary label. +- [ ] T060 Keep Review Pack ZIP download behavior unchanged. + +## Phase 7: PDF Rendering + +**Purpose**: Render the management PDF from prepared payload only. + +- [ ] T061 Implement the PDF renderer through the approved Gotenberg 8 Chromium path via `PdfRenderingGateway` / `PdfRendererClient`. +- [ ] T062 Render cover, executive summary, governance posture, key decisions, top risks/findings, accepted risks, evidence readiness, scope/limitations, next actions, provenance, and method summary. +- [ ] T063 Include generated timestamp, source review/pack metadata, profile, and classification/confidentiality marker. +- [ ] T064 Include header/footer and page numbering when supported by the approved renderer. +- [ ] T065 Avoid remote fonts, external assets, public images, or network-dependent resources. +- [ ] T066 Avoid huge raw tables; limit top findings to a management-safe count and route full details to deferred technical/auditor reports. +- [ ] T067 Ensure PDF output does not include hidden technical appendix content for `customer_executive`. +- [ ] T068 Ensure Gotenberg renderer errors produce safe exceptions/results that can be mapped to OperationRun failed/blocked. + +## Phase 8: Localization + +**Purpose**: Make the report labels and actions DE/EN-ready through existing seams. + +- [ ] T069 Add EN localization keys for action labels, notifications, blocked reasons, PDF chapter titles, profile labels, limitation labels, and provenance labels. +- [ ] T070 Add DE localization keys for the same user-facing labels. +- [ ] T071 Ensure date/time/number formatting uses existing locale-aware conventions where available. +- [ ] T072 Add tests for central labels or rendered content in EN and DE where feasible. + +## Phase 9: UI Coverage And Documentation-In-Feature + +**Purpose**: Satisfy UI-COV without broad docs churn. + +- [ ] T073 Apply the UI coverage rule: update route inventory for any new signed/authorized PDF route, `ui-099-rendered-review-report.md` for rendered-report/PDF source or customer-facing report presentation changes, `ui-042-review-pack-detail.md` for Review Pack detail action/download changes, `ui-048-stored-report-detail.md` for StoredReport registry/detail exposure, and the design coverage matrix for material action hierarchy, artifact state, or surface classification changes. +- [ ] T074 If materially changed, update the relevant UI coverage artifact(s) with screenshot/reference/status. +- [ ] T075 If not materially changed, record the checked no-update rationale in the implementation close-out. +- [ ] T076 Store browser screenshots/content evidence under `specs/378-management-report-pdf-v1/artifacts/` if captured. +- [x] T077 Record renderer/package decision in the governance artifacts. +- [ ] T092 Record the storage substrate decision in implementation close-out after `StoredReport` versus alternate canonical artifact representation is resolved. +- [ ] T093 Record the OperationRun type decision in implementation close-out after confirming whether `report.management.generate` or an existing canonical type is used. +- [ ] T094 Record the UI coverage decision in implementation close-out after owner surface, download route, and StoredReport exposure are known. + +## Phase 10: Validation + +**Purpose**: Prove the feature and prevent adjacent report regressions. + +- [x] T078 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec378`. +- [ ] T079 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec357`. +- [ ] T080 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec366`. +- [ ] T081 Run focused Review Pack regressions selected from current touched surfaces, including rendered-report/profile/download/resource tests as applicable. +- [ ] T082 Run focused Customer Review Workspace regression if the implementation touches customer workspace action/state copy. +- [ ] T083 Run focused OperationRun regression if a new operation type or OperationCatalog mapping is added. +- [ ] T084 Run `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec378ManagementReportPdfSmokeTest.php --compact` if browser/content smoke was added. +- [ ] T085 Run PostgreSQL lane if migrations, JSONB indexes, or artifact storage constraints require it. +- [x] T086 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`. +- [x] T087 Run `git diff --check`. +- [x] T088 Static scan changed runtime files for Livewire v3 API names (`emit`, `emitTo`, `dispatchBrowserEvent`) and confirm none were introduced. +- [x] T089 Review changed runtime files for Graph/provider calls during render/generation/download and confirm none were introduced. +- [x] T090 Review final implementation diff for new packages, env vars, migrations, queues, scheduler, storage, panel providers, global search, Filament assets, native PDF runtime, customer portal, scheduled delivery, AI, and auditor/technical report scope. +- [ ] T091 Complete final close-out with Livewire v4 compliance, provider registration location, global search status, destructive/high-impact action status, asset strategy, tests, and deployment impact. + +## Non-Goals + +- [ ] NT001 Do not build a Technical Evidence Report PDF. +- [ ] NT002 Do not build an Auditor Evidence Report PDF. +- [ ] NT003 Do not build a Report Delivery Center. +- [ ] NT004 Do not add scheduled/email/Teams delivery. +- [ ] NT005 Do not add public links or a customer portal. +- [ ] NT006 Do not add Word/DOCX, PowerPoint, Excel, or raw JSON customer appendix export. +- [ ] NT007 Do not add AI-generated summaries or AI review workflow. +- [ ] NT008 Do not create a second report taxonomy or generic report engine. +- [ ] NT009 Do not change Review Pack ZIP download contract. +- [ ] NT010 Do not add a new artifact lifecycle/retention framework. +- [ ] NT011 Do not call Microsoft Graph/provider APIs during render, generation, or download. +- [ ] NT012 Do not rewrite completed historical specs or remove close-out/validation evidence from related specs. + +## Dependencies And Ordering + +- Phase 1 gates must complete before runtime edits. +- Phase 1A governance is complete, but G008-G013 must complete before management report payload/storage/OperationRun/UI/PDF generation implementation. +- T010 must complete before T023 and T051-T060, but it is not a prerequisite for G008-G013 runtime/gateway work. +- Tests in Phase 2 should be added before or alongside implementation. +- Payload/readiness/disclosure must complete before PDF rendering. +- Artifact storage and OperationRun/audit must complete before exposing download. +- Browser/content smoke runs after the owner surface and download route are usable. +- Validation and close-out run last. + +## Parallel Opportunities + +- T003-T007 can run in parallel during verification. +- T011-T014 can be developed in parallel with T015-T023 after fixtures are agreed. +- T069-T072 can run after visible labels are known. +- T078-T083 can run in parallel once implementation is stable. + +## Implementation Strategy + +1. Resolve the renderer/package gate first. Completed decision: Gotenberg 8 Chromium internal service approved with controls. +2. Add the Gotenberg runtime service/config and Laravel gateway before report generation work. +3. Keep content/payload derived from existing rendered-report truth. +4. Prefer narrow StoredReport/canonical artifact extension over new persistence. +5. Use OperationRun/audit as generation truth. +6. Add the smallest owner-surface action and signed/authorized download path. +7. Prove customer-safe content and leakage boundaries with tests and browser/content smoke. + +## Expected Task Count + +- Implementation tasks: 94 plus 13 renderer-governance/runtime-boundary tasks +- Non-goal guardrails: 12 +- MVP path: T001-T068 plus validation subset T078, T081, T083 if operation type changed, T084, T086-T091