Skip to content

R1: the résumé editor: sections in your order, every printed field, a PDF preview - #436

Merged
Taleef7 merged 6 commits into
mainfrom
restart/415-resume-editor
Oct 21, 2026
Merged

Taleef7 merged 6 commits into
mainfrom
restart/415-resume-editor

Conversation

@Taleef7

@Taleef7 Taleef7 commented Oct 15, 2026

Copy link
Copy Markdown
Owner

Refs #415. Refs #352, #408, #425, #333.

Why

I want the base résumé to look the way I lay it out, as Jobright does: rename the headings, put the sections in my order, hide one, and have tailored résumés keep that layout. None of it existed. Headings and order were fixed in code, the base résumé couldn't be previewed or downloaded, and Settings couldn't edit fields the PDF prints:

  • a role's summary, which printed as a stray line under each role;
  • project bullets;
  • a school's start date, GPA and bullets;
  • the location and links on the contact line.

What changed

  • Sections in Settings → Résumé: every section in print order, with Move up / Move down (each move announced, focus kept), an inline heading (Enter or leaving the field saves, Escape cancels), Hide/Show, the page size (Letter or A4) and Reset to default. Every change saves at once; a failed one is put back, with a toast. No dragging, so nothing needs a mouse.

  • PATCH /api/profile/base-resume/layout saves both copies of the base résumé in Save the base résumé in one transaction (profile and base version row) #408's one transaction, under the profile row lock. Only the fields sent change, and a malformed layout is a 400 (so is a body that isn't JSON). A résumé save, or a replacement read from a file, keeps the stored layout: it's read inside the save's transaction (one step of the file queue in file mode), so a layout change made at the same time is never undone. The layout's headings don't count toward the résumé's size cap.

  • The editor covers everything the PDF prints:

    • location, website and profile links;
    • a role's summary;
    • bullets one per field, for roles, schools and projects;
    • a school's start date and GPA, project keywords, and a certificate's link.

    Every list moves up and down. Enter in a bullet, or a comma in a skill or keyword, starts the next one; stored lines are never split. A test checks that every field the renderer reads is one the editor writes.

  • Preview PDF and Download PDF use GET /api/profile/base-resume/pdf, which renders the saved résumé and stores nothing: no version row, no file. It has its own per-user limit (30 a minute), not the AI routes'. The preview draws the pages with pdf.js's legacy build (older Chrome and Safari included) and shows the page count. It's not a frame, because the CSP's object-src 'none' can stop the browser's PDF viewer, and phones don't show a PDF inside a page. A failed preview can be tried again. render-pdf is unchanged and has no caller; render-pdf can create a second base résumé version that disagrees with Settings #425 fixes it.

  • Tailored drafts carry the base layout they were tailored from and print with the base résumé's current layout. Downloading one, or moving its job into Applied or a later stage (follow-up due, interview, offer, rejected), freezes the layout on it, so what was sent doesn't change. Outreach, referrals and archiving don't freeze it. A draft tailored after applying stays unfrozen until it's downloaded.

Evidence

  • Red first: the new tests failed before the code, including:
    • the concurrent-save tests (Postgres and file mode) on the old reads of the layout;
    • real pdf.js on the modern build ("toHex is not a function");
    • a near-cap résumé's 413 after a rename;
    • the stage guard.
  • npm run check clean. npm test: 699 API, 391 web. npm run test:pg: 59/59 on a fresh database, including:
    • a layout change whose second write fails changes neither copy;
    • résumé saves and layout changes at once never undo the layout;
    • the preview writes no row.
  • PDF text via pdfjs: headings in the saved order with the saved titles, and the hidden section's text absent. A tailored draft re-renders with a changed base layout; after Download, a later change leaves its PDF text unchanged.
  • Sample preview: Technical Skills, Education, Professional Experience, Summary, Certifications, with Projects hidden, on A4, 1 page. The canvas preview (legacy build) was checked in Chromium on a page sent with the app's enforced CSP.
  • Still to do: Playwright, keyboard only at 375 px (move Education to the top, rename Skills, hide Certificates, reload, Preview).

Deploy

API and web together; no migration. The web adds pdfjs-dist 6.4.299. After deploy, on live:

  • run the Playwright pass above, and confirm the preview's pages show on a phone;
  • download the base PDF and open it (name, order, headings, links);
  • tailor a [TEST] job, change the layout, download, and confirm the PDF follows the layout, then stays fixed;
  • confirm the CSP reports show nothing from the preview.

Taleef added 5 commits October 15, 2026 14:42
… PDF preview

- Settings → Résumé → Sections lists the PDF's sections in print order. Each one
  can move up or down (buttons, with the move announced and focus kept on it),
  take its own heading (Enter or leaving the field saves, Escape cancels) and be
  hidden without losing its content. Page size (Letter or A4) and "Reset to
  default" sit below. Every change saves at once; a failed one is put back, with
  a toast.
- PATCH /api/profile/base-resume/layout saves the layout to both copies of the
  base résumé, on the résumé as it is now; only the fields sent change, and a
  malformed layout is a 400. A résumé save, and a replacement read from a file,
  keep the stored layout whatever the body carries.
- The editor now covers everything the PDF prints: location, website and profile
  links; a role's summary; bullets one per field for roles, schools and projects;
  a school's start, GPA and bullets; project keywords; a certificate's link. Roles,
  schools, projects, certificates, links, skill groups, skills and bullets all
  move up and down. A test checks the fields the renderer reads are ones the
  editor writes.
- Preview PDF and Download PDF for the base résumé use GET
  /api/profile/base-resume/pdf, which renders the saved résumé and stores nothing
  (no version row, no file). The preview shows the page count.
- A tailored draft carries the base layout it was tailored from, prints with the
  base résumé's current layout, and keeps the one it was downloaded with. Marking
  its job applied freezes the layout on its drafts too.
…en't split, stale previews

- Moving a role, school, project or skill group keeps focus on its own Move
  buttons; it used to land on a bullet's, which carry the same markers
- Opening a section and changing nothing is not a change, whatever the stored
  lines look like ("AWS (EC2, S3)" stays one skill, even after a save)
- Lines split only where the user asks: Enter in a bullet, or a comma in a skill
  or keyword, starts the next one after it with the focus there, and a pasted
  list becomes one line each. Nothing is split when a field is left
- A new line gets the focus; removing one leaves it on the next line, or on Add
- Project keywords are a list like skills (the comma text box kept a trailing
  ", " that couldn't be deleted)
- A layout save that fails puts the editor back to what was saved once every
  change made is answered, even when a page-size change followed it
- Preview and Download wait for layout changes still being saved, and a preview
  made before the latest save is dropped and rendered again
- A hidden section's field keeps its name ("Skills heading") and is described as
  hidden; while a section saves, the sections say they're busy, not missing
- The reported CSP allows the preview's blob: frame
…es drafts

- PATCH /layout saves both copies through changeBaseResume's one transaction,
  under the profile row lock; the separate base-version write is gone
- A résumé save reads the stored layout inside saveBaseResume's transaction,
  under the same lock, so a layout change made at the same time is never undone
  (it was, before: the new pgtest fails on the old read)
- pgtests: a layout change whose second write fails changes neither copy, and
  résumé saves and layout changes at once keep the layout
- A job moved into applied, follow-up due, interview, offer or rejected, from a
  stage before them, freezes the layout on its drafts. Outreach, referrals and
  archiving don't
- The editor test that fills in every field gets 30 s: it timed out at 5 s with
  the other test files running alongside
The preview was a blob: frame, which needs the browser's PDF viewer: the CSP's
object-src 'none' can stop it, and phones don't show a PDF inside a page. The
pages are now drawn into canvases with pdf.js (on the main thread, its worker
module loaded alongside), one under the other, each named "Page 1 of 2". If
they can't be drawn, it says so and points to Download PDF. pdfjs-dist 6.4.299,
the version the API's tests already use, is now a web dependency. The CSP
change for blob: frames is dropped.

Checked in Chromium on a page sent with the app's enforced CSP: the sample
résumé draws in full, with no CSP errors.
…retryable preview

- The preview loads pdf.js's legacy build: the modern one calls
  Uint8Array.prototype.toHex, Map.prototype.getOrInsertComputed and
  Math.sumPrecise, missing in Chrome before ~140 and older Safari. A test reads a
  résumé the API drew with pdf.js for real (it failed with "toHex is not a
  function" on the modern build); a page that can't be drawn is logged
- A failed preview can be tried again: each preview draws afresh
- The layout's headings don't count toward the résumé's 60,000-character cap
  (they're bounded on their own): a résumé near the cap rendered 413 after a
  rename
- File mode: a résumé save reads the stored layout and writes both copies in one
  step of the file queue, so a layout change at the same time is never undone
- A draft tailored after the job was applied stays unfrozen when the job moves
  on to an interview; the test now fails without the guard
- The PDF preview has its own per-user limit (30 a minute in production), not
  the AI routes'
- A body that isn't JSON, or is JSON null, is a 400, not a 500
- Sections copy: tailored résumés follow it until downloaded or the job moves to
  Applied
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T18:57:09.490247Z 2b77e81 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b77e814bf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/web/src/lib/pdf-pages.ts Outdated
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.

1 participant