Skip to content

LWLP-935: replace crc-pdf-generator with in-app PDF generation - #1209

Open
ochosi wants to merge 14 commits into
content-services:mainfrom
ochosi:inline-pdf-renderer
Open

ochosi wants to merge 14 commits into
content-services:mainfrom
ochosi:inline-pdf-renderer

Conversation

@ochosi

@ochosi ochosi commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

What

Replace the external crc-pdf-generator microservice with a lightweight in-app Express + Puppeteer server for Beacon PDF exports. This cuts export time from ~20 s to under 3 s by eliminating the Scalprum module-federation round-trip, S3 storage, and Kafka status polling.

Why

The current flow goes through crc-pdf-generator, which:

  1. Loads the federated ./BeaconPdfEntry module in a new Puppeteer page
  2. Fetches data through an internal proxy
  3. Stores the result in S3
  4. Polls for completion via Kafka

This adds ~20 s of latency per export and requires four separate processes for local development. The new approach SSR-renders the existing BeaconPdfTemplate directly in Node, prints it with Puppeteer, and streams the PDF back — all in a single POST /pdf/beacon call.

How

New src/pdf/ module:

  • pdfServer.ts — Express server with a single POST /pdf/beacon endpoint. Validates the request, fetches all pages of vulnerability data (or uses built-in mock data), then calls the renderer.
  • pdfRenderer.tsx — SSR pipeline: renders BeaconPdfTemplate to HTML via renderToStaticMarkup, prints to PDF via a singleton Puppeteer browser instance.
  • pdfHeader.tsx — Puppeteer header/footer templates with Lightwell branding and page numbering (using Puppeteer's built-in pageNumber class).
  • register-css-stub.ts — Node require hook that stubs CSS/SCSS imports so PatternFly components can be SSR'd.

UI changes:

  • ExportMenu.tsx — PDF export now posts to /pdf/beacon and triggers a blob download instead of calling chrome.requestPdf.
  • Removed itemCount prop (no longer needed for pre-splitting into paginated tasks).

Removed legacy code:

  • BeaconPdfEntry.tsx — module federation entry point for crc-pdf-generator.
  • beaconPdf.ts — removed fetchData(), buildBeaconPdfPayload(), and associated constants/types that only served the old flow.
  • fec.config.js — removed ./BeaconPdfEntry expose and PDF_GENERATOR_PORT proxy route.

Dev setup:

  • yarn start:pdf runs the PDF server via tsx (zero-config TypeScript runner).
  • fec.config.js proxies /pdf/ to the PDF server when PDF_SERVER_PORT is set.

Dependencies added

Package Purpose Section
express HTTP server for the PDF endpoint devDependencies
puppeteer Headless Chrome for HTML→PDF devDependencies
tsx TypeScript runner for start:pdf devDependencies
@types/express Type definitions devDependencies

Dependencies removed

Package Reason
pdf-lib Page numbering moved to Puppeteer's built-in footer template
ts-node Replaced by tsx
tsconfig-paths No longer needed (tsx resolves baseUrl natively)

Testing

  • pdfServer.spec.ts — handler unit tests (success, 400, 500 paths)
  • pdfRenderer.spec.tsx — SSR HTML output assertions
  • beaconPdf.test.ts — retained tests for formatBeaconPdfGeneratedAt and shouldUseLandscapePdf
  • ExportMenu.test.tsx — updated to test the new axios.post + blob download flow

Here's the section to append to the PR description:


How to test this PR locally

Manual test with mock data (no backend needed):

  1. Start the PDF server with mock data:

    USE_MOCK=true yarn start:pdf

    If Puppeteer can't find Chrome, point it at your system install:

    CHROME_PATH=$(which google-chrome-stable) USE_MOCK=true yarn start:pdf
  2. In a second terminal, start the frontend dev proxy:

    PDF_SERVER_PORT=3001 yarn start
  3. Open https://stage.foo.redhat.com:1337/lightwell/beacon, select customer CID-01 or CID-214, and click Export → Export as PDF.

  4. Verify in the downloaded PDF:

    • Lightwell header with "Prepared: ..." date
    • Summary section with total/critical counts and stage pipeline
    • Vulnerability table with the expected columns
    • Page numbers at the bottom center ("Page 1", "Page 2", ...)
    • CSV and JSON exports from the same menu still work

Manual test against stage (real data):

  1. Start the PDF server (routes through the dev proxy for auth):

    yarn start:pdf
  2. In a second terminal:

    PDF_SERVER_PORT=3001 yarn start:stage
  3. Open https://stage.foo.redhat.com:1337/lightwell/beacon, select a customer, and export as PDF.

@ochosi
ochosi force-pushed the inline-pdf-renderer branch from 8393acf to 2654d3a Compare September 2, 2026 23:45
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 63.72%. Comparing base (7bc3798) to head (5f1d1c4).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1209      +/-   ##
==========================================
- Coverage   63.89%   63.72%   -0.17%     
==========================================
  Files         149      149              
  Lines        5511     5511              
  Branches     1077     1077              
==========================================
- Hits         3521     3512       -9     
- Misses       1688     1697       +9     
  Partials      302      302              
Flag Coverage Δ
e2e 63.72% <ø> (-0.17%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.
see 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ochosi
ochosi force-pushed the inline-pdf-renderer branch 2 times, most recently from 80fd807 to c4c3833 Compare September 3, 2026 13:40
@ochosi

ochosi commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

What could become a standalone npm package?

The generic parts of this pipeline that other console.redhat.com tenants would want:

Package: @redhat-cloud-services/pdf-renderer (or similar)

What goes in:

  • Puppeteer browser lifecycle management (singleton, launch args, graceful shutdown)
  • printPdf(html, options) -- HTML string to PDF buffer via Puppeteer
  • Header/footer template helpers (Puppeteer's displayHeaderFooter with HCC branding)
  • CSS stub mechanism for Node SSR of PatternFly components
  • Express middleware/handler scaffold for a /pdf/<name> endpoint
  • Page numbering (via Puppeteer footer, no pdf-lib needed)

What stays in each tenant app:

  • The React template component (BeaconPdfTemplate) -- each tenant defines their own report layout
  • Data fetching logic (fetchAllVulnerabilities) -- each tenant calls their own API
  • Column/layout configuration -- domain-specific

Boundary (simplified):

graph LR
  subgraph npmPackage ["@redhat-cloud-services/pdf-renderer"]
    BrowserMgmt["Browser lifecycle"]
    PrintPdf["printPdf(html, opts)"]
    HeaderFooter["Header/footer templates"]
    CssStub["CSS stub for SSR"]
    ExpressHandler["Express handler scaffold"]
  end

  subgraph tenantApp ["Tenant app (e.g. content-sources)"]
    Template["React PDF template"]
    DataFetch["Data fetching"]
    Config["Column / layout config"]
  end

  Template --> PrintPdf
  DataFetch --> ExpressHandler
  Config --> Template
Loading

Usage in a tenant app would look like:

import { createPdfHandler, printPdf, closeBrowser } from '@redhat-cloud-services/pdf-renderer';

const handler = createPdfHandler({
  route: '/pdf/beacon',
  fetchData: async (req) => { /* tenant-specific */ },
  renderHtml: (data) => renderToStaticMarkup(<MyTemplate data={data} />),
  pdfOptions: { landscape: false },
});

app.post('/pdf/beacon', handler);

This package would be ~200 lines of library code and eliminate ~150 lines of boilerplate from each consuming app. The main value is the Puppeteer lifecycle management, CSS stub wiring, and the tested print pipeline -- things every tenant would otherwise copy-paste.

@ochosi
ochosi force-pushed the inline-pdf-renderer branch 7 times, most recently from 5082f39 to 01ff4c2 Compare September 4, 2026 08:55
@ochosi
ochosi marked this pull request as ready for review September 4, 2026 10:11
@ochosi
ochosi requested a review from a team as a code owner September 4, 2026 10:11

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/pdf/pdfHeader.tsx" line_range="13-22" />
<code_context>
+import { type PropsWithChildren } from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+
+function getHeaderDate(): string {
+  const date = new Date();
+  const day = date.getDate();
+  const year = date.getFullYear();
+  return `${day} ${date.toLocaleString('en-us', {
+    month: 'short',
+  })} ${year} ${date.toLocaleString('en-us', {
+    hour: '2-digit',
+    hour12: false,
+    minute: 'numeric',
+  })} UTC`;
+}
+
</code_context>
<issue_to_address>
**issue (bug_risk):** getHeaderDate formats the server's local date and time but appends `UTC`, so the PDF header shows an incorrect timestamp whenever the PDF server runs outside UTC.

**Triggers:** When the PDF server timezone is not UTC.

**Suggested fix:** Pass `timeZone: 'UTC'` to both `toLocaleString` calls and use UTC getters for the day and year.

```suggestion
  const date = new Date();
  const day = date.getUTCDate();
  const year = date.getUTCFullYear();
  return `${day} ${date.toLocaleString('en-us', {
    month: 'short',
    timeZone: 'UTC',
  })} ${year} ${date.toLocaleString('en-us', {
    hour: '2-digit',
    hour12: false,
    minute: 'numeric',
    timeZone: 'UTC',
  })} UTC`;
```
</issue_to_address>

### Comment 2
<location path="src/pdf/pdfRenderer.tsx" line_range="34-46" />
<code_context>
+const VIEWPORT_WIDTH = (A4_HEIGHT_MM - 20) * 4; // 1108
+const VIEWPORT_HEIGHT = (A4_WIDTH_MM - 40) * 4; // 680
+
+let browserInstance: Browser | null = null;
+
+async function getBrowser(): Promise<Browser> {
+  if (browserInstance && browserInstance.connected) {
+    return browserInstance;
+  }
+  const executablePath = process.env.CHROME_PATH || undefined;
</code_context>
<issue_to_address>
**issue (bug_risk):** Concurrent printPdf calls can both observe `browserInstance` as null and launch separate Puppeteer browsers; only the last browser is retained, so earlier browser processes are leaked and accumulate under concurrent exports.

**Triggers:** When multiple PDF requests start before the first Puppeteer launch completes.

**Suggested fix:** Serialize browser initialization with a shared launch promise, or otherwise ensure only one launch can occur at a time.
</issue_to_address>

### Comment 3
<location path="src/pdf/lightwellLogomark.ts" line_range="11-16" />
<code_context>
+import { readFileSync } from 'fs';
+import { resolve, dirname } from 'path';
+
+const svgPath = resolve(
+  dirname(require.resolve('frontend-assets/package.json')),
+  'src',
+  'partners-icons',
+  'lightwell-logomark.svg',
+);
+
+export const LIGHTWELL_LOGOMARK_SVG = readFileSync(svgPath, 'utf-8');
</code_context>
<issue_to_address>
**issue (bug_risk):** The PDF server resolves `frontend-assets` unconditionally during module initialization even though the package is declared optional; an install that omits or cannot install the optional dependency makes `yarn start:pdf` fail before the server starts.

**Triggers:** When the optional `frontend-assets` dependency is absent or its GitHub install fails.

**Suggested fix:** Declare `frontend-assets` as a required dependency for the PDF server, or provide a fallback logo path and avoid unconditional `require.resolve`.
</issue_to_address>

Sourcery assessment

Approval pending. 3 findings to address first.

Blocking findings: src/pdf/pdfHeader.tsx:22, src/pdf/pdfRenderer.tsx:46, src/pdf/lightwellLogomark.ts:16


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/pdf/pdfHeader.tsx
Comment thread src/pdf/pdfRenderer.tsx Outdated
Comment thread src/pdf/lightwellLogomark.ts Outdated
itemCount: count,
}) as unknown as PDFRequestPayload,
});
const data = await fetchAllFilteredVulnerabilities(customerId, filters);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EXPORT_PAGE_SIZE = 200 only pages API reads; PDF generation still accumulates every row, sends one JSON body, and renders one SSR/Chromium document. Could we document the intended dataset limit and test one representative large report before carrying this design forward?

Comment thread src/pdf/pdfRenderer.tsx
const page = await browser.newPage();

const renderId = randomUUID();
pendingRenders.set(renderId, html);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate from the browser-launch race: each concurrent request creates a Chromium page and retains its full HTML until printing completes. Since isExporting only protects one mounted menu, could we bound concurrent renders with a small queue or semaphore before carrying this design forward?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

ochosi and others added 12 commits September 10, 2026 00:32
Bypass the external crc-pdf-generator microservice by rendering
Beacon PDFs directly via a lightweight Express + Puppeteer server.
This cuts export time from ~20s to under 3s.

- Add src/pdf/ module: SSR renderer, Puppeteer printer, Express
  server with mock-data mode
- Rewrite ExportMenu to POST /pdf/beacon and trigger a blob download
  instead of chrome.requestPdf polling
- Add dev proxy route and start:pdf script
- Update tests and local dev documentation

Co-authored-by: Cursor <cursoragent@cursor.com>
getHeaderDate() used local-time getters (getDate, getFullYear) and
toLocaleString without timeZone, but labelled the output 'UTC'. On a
server running outside UTC the header would show the wrong date/time.

Switch to getUTCDate/getUTCFullYear and pass timeZone: 'UTC' to both
toLocaleString calls. Export the function and add a test that runs
under a non-UTC timezone to verify correctness.

Co-authored-by: Cursor <cursoragent@cursor.com>
Concurrent printPdf calls could both observe browserInstance as null,
launch separate Puppeteer browsers, and leak the earlier instance.

Add a launchPromise guard so concurrent callers coalesce on the same
puppeteer.launch() promise. Clear the promise in closeBrowser() too.

Co-authored-by: Cursor <cursoragent@cursor.com>
The unconditional require.resolve('frontend-assets/package.json') at
module init crashed the PDF server if the optional dependency was
absent (e.g. GitHub auth failure during install).

Replace with an inlined SVG string, removing the fs/path/require.resolve
runtime dependency entirely.

Source: frontend-assets/src/partners-icons/lightwell-logomark.svg
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a promise-based semaphore (MAX_CONCURRENT_RENDERS=3) that caps the
number of simultaneous Puppeteer page renders. Excess requests queue
until a slot is freed. This prevents unbounded Chromium page creation
from exhausting pod memory when multiple users export PDFs at once.

The limit controls concurrent render *processes*, not PDF pages -- each
render produces a complete multi-page PDF in a single Chromium tab.

Co-authored-by: Cursor <cursoragent@cursor.com>
Reject POST /pdf/beacon payloads with more than 5,000 vulnerabilities
to prevent Chromium from running out of memory on oversized renders.
Returns 400 with a descriptive error message.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add 30-second timeout to page.goto() and page.pdf() Puppeteer calls so
a hung Chromium process throws instead of blocking forever. Wrap the
Express handler in a 60-second overall timeout that returns 504 if the
full pipeline exceeds the budget.

Co-authored-by: Cursor <cursoragent@cursor.com>
Apply express-rate-limit (5 requests/minute per IP) to POST /pdf/beacon
to prevent abuse. Chromium renders are expensive and even with the
concurrency semaphore, unlimited queuing could exhaust memory.

Co-authored-by: Cursor <cursoragent@cursor.com>
5,000 vulnerabilities with all fields is ~2-3 MB of JSON. The previous
50 MB limit was far larger than any realistic payload and invited memory
abuse. 10 MB provides comfortable headroom.

Co-authored-by: Cursor <cursoragent@cursor.com>
Export getBrowser from pdfRenderer so the health check can verify
Chromium is alive by opening and closing a throwaway page. Returns
200 {status:'ok'} or 503 {status:'unhealthy',error:...}.

This will serve as the readiness/liveness probe target in the
ClowdApp deployment.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add prom-client with three metrics:
- pdf_generation_duration_seconds: histogram of end-to-end time
- pdf_generation_errors_total: counter by error type
- pdf_active_renders: gauge tracking in-flight render slots

Expose GET /metrics endpoint on the PDF server. Wire the duration
timer and error counter into handleBeaconPdf, and the active renders
gauge into the concurrency semaphore.

Co-authored-by: Cursor <cursoragent@cursor.com>
Two-stage build on UBI9 Node.js 22:
- Builder: yarn install with chrome-headless-shell download
- Runtime: Node.js + Chrome system deps, runs tsx directly

Uses the same entrypoint as 'yarn start:pdf' (no webpack build).
Listens on port 8000 to match Clowder convention.

Co-authored-by: Cursor <cursoragent@cursor.com>
ochosi and others added 2 commits September 10, 2026 00:40
Standalone ClowdApp deployment with:
- 2Gi/4Gi memory (Chromium headless needs ~1-2Gi per render)
- Readiness probe on GET /pdf/healthz (30s interval)
- Liveness probe on GET /pdf/healthz (60s interval)
- PDF_MAX_CONCURRENT env var for tuning concurrency
- 1 replica (PDF exports are user-initiated and infrequent)

Co-authored-by: Cursor <cursoragent@cursor.com>
Push and pull-request pipelines that build Dockerfile.pdf into
a separate container image (content-sources-pdf-server). Uses the
same docker-build-oci-ta pipeline as the frontend, with its own
service account and output image path.

PR images expire after 5 days.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ochosi
ochosi force-pushed the inline-pdf-renderer branch from f6b18c8 to 5f1d1c4 Compare September 9, 2026 22:52
Comment thread src/pdf/pdfServer.ts
Comment on lines +152 to +159
app.post('/pdf/beacon', pdfRateLimiter, (req, res) => {
const timer = setTimeout(() => {
if (!res.headersSent) {
res.status(504).json({ error: 'PDF generation timed out' });
}
}, HANDLER_TIMEOUT_MS);

handleBeaconPdf(req, res).finally(() => clearTimeout(timer));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout path can take the process down.

Once the 60s timer fires and sends the 504, handleBeaconPdf keeps running. When it finishes it hits res.setHeader('Content-Type', ...) at line 78, which throws ERR_HTTP_HEADERS_SENT. That's caught by the handler's own catch, which then calls res.status(500).json(...) at line 85 — and that throws too. The second throw escapes handleBeaconPdf, and since the call here only has .finally() and no .catch(), it surfaces as an unhandled rejection. Node 22 exits the process on those by default, so a single slow render takes out the pod.

Two fixes needed:

  • guard the success and error writes in handleBeaconPdf with if (res.headersSent) return;
  • add a .catch() here (or make the wrapper async and await inside a try/catch) so nothing escapes

Separately: the 504 doesn't cancel the underlying render, so the Chromium work continues and still occupies a semaphore slot for a response nobody will read.

Comment thread src/pdf/pdfServer.ts
Comment on lines +144 to +150
const pdfRateLimiter = rateLimit({
windowMs: 60_000,
limit: 5,
standardHeaders: 'draft-7',
legacyHeaders: false,
message: { error: 'Too many PDF requests. Please wait a minute before trying again.' },
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In production this degrades into a single global bucket.

There's no app.set('trust proxy', ...), so express-rate-limit keys on the socket address. Behind the console gateway every request arrives with the gateway's IP, which means limit: 5 per minute applies to all users combined rather than per user — the first person to export three reports locks out everyone else for the rest of the minute.

Setting trust proxy blindly isn't the fix either, since X-Forwarded-For is then spoofable. Options: key the limiter on the identity header instead of the IP, or drop IP-based limiting here and rely on the render semaphore plus the handler timeout for back-pressure.

Comment thread src/pdf/pdfServer.ts
Comment on lines +124 to +127
app.get('/metrics', async (_req, res) => {
res.set('Content-Type', registry.contentType);
res.send(await registry.metrics());
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/metrics is served unauthenticated on the same port as web: true, and collectDefaultMetrics publishes process-level detail (heap, file descriptors, versions, arguments).

Clowder convention is to expose metrics on the private metrics port (9000 by default) with a metrics block in the ClowdApp, so the endpoint is only reachable by the Prometheus scraper. deploy/pdf-server.yaml has no metrics section at all right now, so as written this is public surface and the scraper has nothing declared to find.

Comment thread src/pdf/pdfServer.ts
data: BeaconPdfData;
};

export async function handleBeaconPdf(req: express.Request, res: express.Response): Promise<void> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no authentication or authorization anywhere on this endpoint.

The validation below checks shape (customerId present, matches [\w-]+, columns non-empty, vulnerability count under the cap) but never checks who is asking or whether they're entitled to that customerId. The request body is entirely client-supplied, so anything that can reach the service can drive arbitrary Chromium renders with arbitrary content and get a branded Lightwell PDF back for any customer ID it names.

The old crc-pdf-generator flow sat behind the platform gateway with the identity header available. Whatever replaces it needs the equivalent: read x-rh-identity, and check the caller's entitlement to the requested customer before rendering.

Comment thread src/pdf/pdfRenderer.tsx
Comment on lines +94 to +110
// Concurrency semaphore: limits simultaneous Puppeteer renders to avoid OOM.
let activeRenders = 0;
let waitQueue: Array<() => void> = [];

function acquireSlot(): Promise<void> {
if (activeRenders < MAX_CONCURRENT_RENDERS) {
activeRenders++;
activeRendersGauge.inc();
return Promise.resolve();
}
return new Promise((resolve) =>
waitQueue.push(() => {
activeRendersGauge.inc();
resolve();
}),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semaphore accounting itself is correct (the slot hand-off in releaseSlot deliberately skips the decrement — took me a second read, a comment there would help). Two gaps around it though:

  1. waitQueue is unbounded. With the rate limiter behaving as a single global bucket (see my note on pdfServer.ts:144), nothing caps how many requests pile up behind three slots.
  2. There's no abort path. A request that waits past HANDLER_TIMEOUT_MS gets its 504, but its queued callback still fires later and burns a full render slot producing a PDF that's already been abandoned. Under sustained load the service spends its capacity on dead work and never catches up.

Suggest capping the queue (reject with 503 past a threshold, which is honest back-pressure) and passing an AbortSignal — or at minimum a cancelled flag checked in the acquire callback — so timed-out requests release rather than consume their slot.

Comment thread deploy/pdf-server.yaml
Comment on lines +32 to +38
livenessProbe:
httpGet:
path: /pdf/healthz
port: 8000
scheme: HTTP
initialDelaySeconds: 15
periodSeconds: 60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the same browser-launching endpoint for liveness is risky. handleHealthz does getBrowser() + newPage(), and that newPage() isn't bounded by the render semaphore, so it competes with in-flight renders.

Under load the probe gets slow, liveness fails, and the kubelet restarts the pod while it's rendering — killing real work at exactly the moment the service is busiest, and potentially looping.

Usual split: liveness stays cheap (a static 200 proving the event loop is alive), readiness does the browser check so a wedged pod is pulled from the service without being killed.

Comment thread package.json
"prettier": "^3.9.6",
"pretty-format": "^30.5.1",
"prop-types": "15.8.1",
"puppeteer": "^25.9.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dependency placement is inconsistent now that this is a deployed service rather than a dev-only script. puppeteer, express, express-rate-limit and tsx are all in devDependencies and all required at runtime by the container, while prom-client — used by exactly the same code — sits in dependencies.

It happens to work because the Dockerfile installs everything, but it blocks a --production install and makes the real runtime dependency set impossible to read. Either move all five into dependencies, or split the PDF server into its own workspace/package with its own manifest (which would also make the npm-package extraction you sketched in the PR comments a much smaller step later).

Comment thread package.json
Comment on lines +142 to +143
"optionalDependencies": {
"frontend-assets": "github:RedHatInsights/frontend-assets"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still needed? It was added for the require.resolve('frontend-assets/package.json') lookup in lightwellLogomark.ts, which is now an inlined SVG string. If nothing else in the tree imports frontend-assets, dropping this also removes a GitHub-sourced dependency from the install path (and from the container build, which currently resolves it).

itemCount: count,
}) as unknown as PDFRequestPayload,
});
const data = await fetchAllFilteredVulnerabilities(customerId, filters);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on my earlier comment about the dataset limit: MAX_VULNERABILITIES = 5000 on the server covers the server side, thanks. The client side is still unguarded though.

This call pages through the entire result set at 200 per request before anything is validated, so a customer with 8000 findings sits through 40 sequential API calls — potentially a minute or more of spinner — and then gets a 400 saying there were too many. The count is already available in meta.count from the first page, so the check can happen after one request instead of forty.

Still outstanding from that thread: is 5000 documented anywhere users will see it, and has one representative large report actually been rendered end to end? A 5000-row single-tab Chromium render is the case the timeouts and memory limits hinge on, and right now nothing in the PR shows what it costs.

Comment on lines +86 to +90
const response = await axios.post(
'/pdf/beacon',
{ customerId, visibleColumns, data },
{ responseType: 'blob' },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This relative URL only resolves in local dev. fec.config.js proxies /pdf/ only when PDF_SERVER_PORT is set, and in stage/prod there's nothing routing /pdf/beacon to the new ClowdApp — deploy/frontend.yaml adds no /pdf/ path and no apiPath, and the path isn't under /api/ so the gateway won't pick it up by convention either.

Deployed, this will 404. Needs the gateway/Frontend CRD route wiring (and the corresponding app-interface saas entry for the ClowdApp) before merge, and the URL here changed to whatever prefix that lands on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants