plans/12-preview-and-export-failures.md
This is a diagnostic plan for five reports, not a proposed shared fix. Follow each issue's branch before editing runtime code. Stop when required source data is missing; report the precise missing fixture. The coordinating maintainer owns index updates and issue disposition.
7a98f6662, 2026-09-05. Priority P1; effort M per reproduced cause; risk medium. Confidence low for historical root causes, high for current source paths below.| Issue | Exact report and relevant history | Evidence still needed |
|---|---|---|
| #3323 | Download succeeds but entered template content is absent. No version, format, template, steps, or output. Maintainer requested clarification twice. A bot's #3076 duplicate suggestion is not evidence. | Input field and text, exported format, exact action sequence, sanitized JSON before/after save, actual download, application revision. |
| #3290 | Cloud preview and downloaded pages are black; template field says None. Maintainer explicitly requested recurrence after #3104. | Original JSON and black PDF; browser/version; whether an independent viewer also shows black pages; design/background/custom styles. |
| #3033 | Ditgar existing resume blank; changing font weight redraws it. Comments separately show getOrInsertComputed is not a function and PT Sans blanking on 5.1.4. | Preserve original PT Sans weight selection before editing; exact old browser build for the compatibility subtype; console stack and source JSON. |
| #3007 | Center view absent across templates; changing fonts did not help. Firefox/Zen comments differ from Chrome. Another self-hosted commenter reports AI-provider requests failing without optional encryption configuration. | Browser build and source PDF; minimal resume; actual failing request/stack. Do not copy the comment's example secret into files or logs. AI errors are an unverified separate subtype. |
| #2609 | Self-hosted 5.0.3: Ditto/“Kikorita” do not load when selected. Later commenter separately describes Ditto education spacing, missing accent borders/full-width header, bold descriptions after migration. | Current deployment/browser errors for template loading; matching older/newer JSON and PDFs for visual parity. Do not conflate load failure with intended/accidental template redesign. |
No row is closed by an all-template smoke pass. Fresh issue body/comments were read on the planning date; no newly supplied exact reproduction removed these limitations.
Run first:
rtk proxy git diff --stat 7a98f6662..HEAD -- apps/web/src/features/resume/preview apps/web/src/features/resume/export apps/web/src/features/resume/builder/draft.ts packages/pdf/src/document.tsx packages/pdf/src/browser.tsx packages/pdf/src/server.tsx packages/pdf/src/hooks/use-register-fonts.ts
Inspect drift before using these excerpts. Worktree branches use codex/; main stays untouched. Node 24/pnpm 11.21.0, named React props types, package export maps, and separate browser/server adapters apply.
apps/web/src/features/resume/preview/preview.browser.tsx, ResumePreviewClient: const blob = await createResumePdfBlob(resumeData); then promotes a staged layer only after pages render. PDF-generation failure keeps the last valid layer and shows resume-preview-render-error. A stale visible layer is not proof the latest PDF generated successfully.
preview/pdf-canvas.tsx imports both API and worker from pdfjs-dist/legacy/build/.... PdfCanvasDocument logs “Failed to load PDF document”; PdfCanvasPage logs “Failed to render PDF page N”. Those are different boundaries. Current canvas setup includes:
canvas.width = Math.floor(width * renderScale);
canvas.height = Math.floor(height * renderScale);
canvasContext.direction = 'ltr';
// page.render includes background: 'white'
export/use-resume-export.ts, onDownloadPDF, derives getResumeExportData(resume.data, target) for owner downloads, then calls createResumePdfBlob; public downloads use resolvePublicResumePdfBlob. JSON export serializes resume.data. Compare the same export target before declaring lost sections.
export/pdf-document.tsx resolves localized section titles. packages/pdf/src/browser.tsx parses input and calls pdf(document).toBlob(). packages/pdf/src/server.tsx parses input and calls renderToBuffer(document). Both instantiate ResumeDocument; this is an architectural fact, not proof of equivalent cached inputs/fonts.
packages/pdf/src/document.tsx resolves typography, scripts, stylesheet mode, template component, and authored layout pages. Hidden/unplaced sections can be absent by data semantics; inspect those fields before blaming rendering.
builder/draft.ts, flushResumeSave, serializes pending writes. Do not add a new save path just because an export lacks text. Prove draft, export input, and persisted revision differ first.
packages/pdf/src/hooks/use-register-fonts.ts, resolvePdfFontWeights and registerFonts, resolves available weights and aliases. The presence of fallback code does not prove a requested remote font loaded successfully.
Use structuredClone(defaultResumeData) from @reactive-resume/schema/resume/default, set basics.name = 'Output Boundary Probe', hide the picture, set summary to <p>SUMMARY_SENTINEL_3323</p>, and make one full-width authored page containing summary. Use Helvetica body and heading to remove network fonts from the first control. Repeat template Ditgar, Ditto, Chikorita; then restore the reporter's exact font and data. Set semantic source to @version 1; only in the semantic control; retain original styles in the original fixture.
Build packages/pdf/src/output-boundary.integration.test.tsx using semantic/rich-text-table.integration.test.tsx's act/renderToBuffer/PDF.js cleanup pattern. Record document page count, page MediaBox, extracted sentinel text, and operator count. Add rendered pixel checks: text extraction alone can pass while pages look black or glyphs disappear.
For browser production tests, use tests/e2e/fixtures/test.ts and the import/save/export patterns already in tests/e2e/specs/preview-raster-direction.spec.ts. Capture the actual generated Blob before PDF.js consumes it, plus the separately downloaded PDF; do not compare a screenshot with a newly invented JSON fixture. Persist artifacts through testInfo.outputPath/attachments, never a machine-specific path.
Keep a result row per stage: {issue, appRevision, browserBuild, template, fontFamily, weights, exportTarget, sourceRevision, stage, pageCount, sentinelPresent, rasterInk, errorName}. Redact cookies, URLs containing credentials, and private content; do not broadly log network response bodies.
Gate: a retained regression must assert the sentinel at every relevant boundary and first fail at exactly one boundary. Without the entered field/export format, record missing fixture and stop this issue's implementation.
metadata.design.colors.background/text and authored page styles. A legitimate black background is not a bug; determine why text is unreadable if so.Gate: assert a known text region contains foreground pixels and background has the expected color; do not use merely inkPixels > 0, which an all-black rectangle satisfies.
Gate: both original font case and compatibility case receive separate results. Passing one must not mark the other fixed.
Gate: engine-specific failure must use identical input bytes; optional-config cause needs a test showing toggling only that configuration changes preview success. Never publish secret values from issue comments.
metadata.template, active preview data-resume-preview-template, nonempty exported PDF, and no worker/font/module error.<strong> weights independently. A changed template design requires maintainer parity decision, not automatic restoration of all old CSS.Gate: selection regression asserts active template and sentinel content. Visual regressions need exact original layout or explicit design acceptance; do not substitute the smoke fixture for it.
Initially add only the output-boundary integration test and tests/e2e/specs/output-boundary.spec.ts. A runtime edit requires a reproduced red assertion and a narrowed file list reviewed by the maintainer. Candidate owners are the exact files above; no schema migrations, bulk font replacement, generic cache clearing, or cross-package source imports.
rtk proxy pnpm --filter web exec vitest run src/features/resume/preview/pdfjs-legacy-entrypoints.test.ts src/features/resume/preview/preview.browser.test.tsx src/features/resume/export/use-resume-export.test.tsx
rtk proxy pnpm --filter @reactive-resume/pdf exec vitest run src/output-boundary.integration.test.tsx src/semantic/all-templates-smoke.test.tsx
rtk proxy pnpm --filter web typecheck
rtk proxy pnpm --filter @reactive-resume/pdf typecheck
rtk proxy pnpm exec turbo boundaries
rtk proxy pnpm build
rtk proxy dotenvx run -f .env.local -- pnpm exec playwright test tests/e2e/specs/output-boundary.spec.ts --reporter=list
New test filenames are proposed additions; create them before running those commands. All commands must exit 0. For production E2E, use a unique port, disposable account, and dedicated database; .env.local must never point to user data. pnpm check writes files: inspect changes after running it. Do not push or open a PR without executor authorization; never merge.
Stop on absent original fixtures, source drift, a second failed verification attempt without a new hypothesis, or required changes outside the proven owner. Preserve the last valid preview/error behavior. Record uncertainty rather than removing reports because old versions differ.