Bump workspace dependencies to their latest versions and dedupe the lockfile.
The upgrade left stale duplicates in pnpm-lock.yaml that broke the build and tests:
- @deepseek-ai/schemastery resolved to both 3.18.2 and 3.18.4. Both copies declare
the global Schemastery namespace, so dsh-plugin's declaration emit failed with
TS2883 on `Config`. `pnpm dedupe` collapses it to 3.18.4.
- vite's optional tsx peer resolved to 4.23.13 for importers without a direct tsx
dependency and 4.23.15 elsewhere, producing two vitest 5.0.2 instances. Loading
both in one run broke `expect(...).rejects`. Re-resolving tsx unifies the graph.
Audit every animation in the app and shared UI primitives against a
frequency-first motion bar: keyboard and high-frequency actions no longer
animate, remaining motion uses interruptible CSS transitions with shared
easing tokens, and redundant animation code is removed.
UI primitives (@reactive-resume/ui)
- Dialog, alert dialog, popover and tooltip move from tw-animate keyframes
to Base UI data-starting/ending-style transitions; menus, popovers and
tooltips skip motion when opened from the keyboard (data-instant).
- Dialog gains an `instant` prop; the command palette uses it.
- Accordion animates its real panel height; caret rotates instead of
swapping icons.
- Menu backdrop blur moves onto the popup so it no longer snaps in after
the fade; context menus and comboboxes fade only.
- Sidebar collapse uses the strong ease-out curve and snaps on Cmd+B.
- Toast, sheet, checkbox, tabs, toggle, inputs and message scroller get
tokenised easing, correct transition properties and press feedback.
- Tabs no longer squeeze a trigger narrower than its label.
- Spinners keep spinning under prefers-reduced-motion.
Web app
- Remove the default route view transition and page-entrance slides.
- Add EASE_OUT_STRONG for Motion; replace built-in "easeOut" everywhere.
- Switch LazyMotion to domMax so layout and Reorder animations run.
- Builder: consolidate 12 section list files into one ItemsSection,
opacity-only list rows with popLayout, instant Cmd+0, faster dock zoom,
crossfade that no longer dips, transform-based progress bars.
- Dashboard: keep previous results while sorting/filtering, no empty-state
flash, uncontrolled sidebar (no network round trip on collapse), calmer
resume card tilt, no stacked hover wrappers.
- Settings: drop entrance/stagger wrappers and ActionButton.
- Agent: CSS marquee paused on hover; thread switches keep the layout.
- Homepage: fix invalid transition declarations, CSS spotlight drift,
scroll-hiding header without a JS spring, tokenised curves.
- Theme switches change every color at once.
- Remove SSR-only useIsClient guards from the SPA.
Docs: rewrite the DESIGN.md animation section around the new tokens.
quay.io/minio/mc:latest is no longer publicly pullable (401 UNAUTHORIZED),
which broke the Docker publish workflow. Use the official amazon/aws-cli
image to create the bucket idempotently via head-bucket || s3 mb.
* feat(deploy): support Vercel Hobby alongside Docker
* fix(deploy): include PDFKit runtime font assets
* docs(deploy): document Vercel and Docker setup
* docs(deploy): record storage persistence checks
* refactor(deploy): drop scheduled staging cleanup
Staging uploads are deleted after finalization and expired ones are swept
on each new upload, so the Vercel cron job, its route, and CRON_SECRET are
no longer needed. The Deploy with Vercel wizard now asks for two secrets.
* docs(deploy): restructure Vercel guides
Split the Vercel page into a how-to with its environment reference, move the
large RPC staging protocol to an API reference page, and move CI deployment
checks to the contributing section. Point Deploy with Vercel buttons at main.
* chore: remove agent planning records and fix web app description
Delete superpowers plans/specs, ADRs, issue plans, execution briefs, domain
context maps, and Europass research. Describe apps/web as a TanStack Router
SPA served by apps/server.
* refactor(deploy): simplify Vercel support code
- Share one Redis client and key namespace through @reactive-resume/db/redis
for API and auth instead of a second auth-only client.
- Drop the auth seeding retry; the provider already treats concurrent inserts
as no-ops and deployment preparation seeds before runtime.
- Detect staging support from POST /api/storage/stage (404 on Docker) instead
of a separate GET probe.
- Read staged bodies directly; the signed upload already caps their size.
- Close per-subscription Redis connections with disconnect() alone.
- Check Blob health with one list call instead of write/read/delete.
- Remove redundant tsdown onlyBundle list, dead namespace fallbacks, and the
conditional spread in the health status.
* fix(deploy): heal stopped runs with dead owners and keep auth up without Redis
- Run owners refresh a Redis heartbeat until they release their claim. Stop
requests reap the run immediately when the owner has stopped heartbeating,
instead of leaving the thread blocked until the 15-minute TTL reaper.
- Auth and oRPC rate limiters fall back to per-instance memory limits when
Redis errors, instead of rejecting every login or failing requests.
* ci: allow esbuild build for Vercel CLI and register deployment deps with knip
pnpm 12 fails dlx installs with ignored build scripts, so allow esbuild
explicitly. The server bundle keeps @vercel/blob, ioredis, and jose external,
and api/index.mjs is the Vercel Function entry.
* fix(web): send buffered RPC bodies instead of teed streams
Reading a request clone turned the original body into a stream, which
browsers send without inspectable request data and which needs duplex
mode. Send the already buffered Blob for direct requests.
* fix(web): send direct RPC bodies as bytes
Blob request bodies are sent as data pipes, so browser tooling cannot
inspect them. Buffer the original request as an ArrayBuffer and send those
bytes; this restores the e2e save assertions that match on request data.
Opening another dialog during the previous dialog's 300 ms close animation could clear the new dialog and its close handler. This caused the post-merge dashboard lifecycle test to lose the Duplicate Resume dialog after renaming a resume.
- Scope delayed cleanup to the original dialog and require it to remain closed.
- Add regression coverage for both open and closing replacement dialogs; both cases failed before the fix and pass afterward.
- Include the fix in the v5.3.1 release notes.
Validation: `pnpm check`, `pnpm typecheck`, `pnpm test`, and the focused dialog-store suite (12 passing tests).
Prepare v5.3.1 with dashboard search and thumbnail improvements, PDF layout fixes, cover-letter integrations, and self-hosting updates.
- Bump the root version and add release notes with contributor credits, cover-letter REST migration instructions, and the new GHCR image path.
- Align the dashboard authentication plugin with Better Auth's fetch dependency to restore auth-client type inference.
- Regenerate the OpenAPI specification so published validation limits match runtime schemas.
Validation: `pnpm lingui:extract` (no missing translations), `pnpm check`, `pnpm typecheck`, and `pnpm test`.
Dynamic client registration unconditionally rewrote token_endpoint_auth_method
to "none" for every unauthenticated request, downgrading clients that asked for
client_secret_basic or client_secret_post to public clients. Those clients were
issued no client_secret but still authenticated at the token endpoint with the
method they registered, so the exchange failed with 401 invalid_client.
Connecting Composio to the MCP server hit this on every attempt.
Default to "none" only when the client omits the field, which keeps PKCE-only
MCP clients working while confidential clients receive a usable secret.
1.0.9 rejects the Path2D objects pdfjs-dist 6.3.289 passes to fill/clip,
failing every raster test with "Value is none of these types `String`, `Path`".
pnpm reads audit overrides and patch mappings from pnpm-workspace.yaml, which
already carries both. The top-level package.json copies were npm-shaped fields
that pnpm never consults, and they had already drifted: the workspace file maps
'@react-pdf/textkit' unversioned while the package.json copy pinned 7.0.1.
pnpm install --frozen-lockfile still passes with pnpm-lock.yaml unchanged, and
all four patches remain applied at their recorded hashes, which is what shows
the removed block was inert.
Adding packages/pdf to CI turned it red on ubuntu-latest for two reasons,
neither of which is a real regression.
Rasterized pixels depend on the host font rasterizer. The chikorita, ditto
and all-template baselines differ on Linux in rasterSha256 alone: every page
count, item count and text coordinate is byte-identical to the macOS-authored
baseline. Compare the portable geometry on every host and the pixels only on
the platform the PNGs were generated on, so the characterization keeps
protecting layout without asserting another machine's antialiasing.
The picture-fit override case rasterizes twice and timed out at Vitest's 5s
default on a CI runner, with the date suite landing at 3.5-4.7s. Give the
package a 30s timeout rather than leaving every rendering test a runner
slowdown away from failing.
Verified on linux/amd64 in Docker: both files pass, 18/18.
The Lapras section marginTop added in c0c658c0 shifted every date marker in
that template down the page, but the date-layout characterization baseline
was not regenerated, so packages/pdf has been failing on main since. The
delta is geometry-only: same page count (2), same text item count (87), same
markers, x and width unchanged; 17 markers move on y and the raster hashes
follow.
Nothing caught it because the workflow ran test:ci for only server and
tooling, leaving 17 packages uncovered. Replace that filter list with the
full workspace run so a package cannot silently lose coverage again, and
move it after the migration and storage steps that the api suite needs.
Serial execution is deliberate. Running the packages in parallel oversubscribes
the runner and starves the PDF rasterization and API rate-limit suites past
their timeouts; 19/19 pass consistently at --concurrency=1.
Patch and minor bumps across the AI provider SDKs (@ai-sdk/*, ai),
@aws-sdk/client-s3, react-email/@react-email/ui, knip and jszip, with
pnpm-lock.yaml regenerated to match.
Also records the audit overrides and patched dependencies in the root
package.json alongside the existing pnpm-workspace.yaml entries.
The "stops waiting for a slow save while preserving late acknowledgements
and queued edits" test races Playwright's fake clock against real debounce
and network timing, and has failed intermittently on main and in PRs since
it landed. Six prior stabilization attempts, including bumping its timeout
to 60s, did not hold; the latest run on main still exceeded that budget.
The same behavior is covered deterministically with fake timers in
apps/web/src/features/resume/builder/draft.test.ts ("ends a stalled
navigation wait without aborting or discarding the pending save", plus
the queued-edit and pending-snapshot cases), so removing the e2e test
loses no coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018z9CKmSSEuS2UFMoqhHWtQ
Default 30s budget was too tight for this multi-step test (resume
creation, warm-up save, fake-clock save juggling, re-navigation),
causing a CI timeout that surfaced as a generic closed-context error
rather than a real assertion failure.
- Fix Grid/Compact/List tab overlap on the resumes dashboard: the fixed
three-column grid forced cells narrower than their labels, so tab content
spilled into neighboring cells.
- Replace the "Resume styling" resume picker with a template picker in the
cover-letter create form and editor. The API accepts a `template` on create
and update, and refreshing style from a resume no longer overwrites it. The
resume control remains in the editor as "Sender details" since it is the
only source for the letter header.
- Remove the cover-letter library button from the builder sidebar and add an
"Import from library" option to the create-cover-letter dialog. Resume to
library copying stays in the library with its own resume picker.
- Remove the authored-pages/PDF-overflow note from the layout sidebar.
Remove the Semantic CSS acceptance suite (six specs, fifteen visual
baselines, and its fixtures) along with the --grep-invert that excluded it
from CI. With the serial PDF preflight gone, Playwright can run four
workers in CI instead of one.
Also drop the slowest and most redundant specs: PDF raster direction,
thumbnail resolution, import reproduction, imported tables, picture
rendering, and literal whitespace, plus the basic authored-page guidance,
settings profile, and resume lifecycle checks already covered elsewhere.
Trim the OAuth consent matrix to allow and deny on an existing session.
* docs: explain local Git backup workflow
* docs: correct export and version history details
* docs: show how to select backup revisions
* docs: save recovered backup revision to file
* docs: clarify recovered backup filename
* fix(pdf): isolate character metadata for cached font glyphs
* test(pdf): verify glyph aliases do not grow cache
* test(pdf): assert glyph aliases are unique
* docs: reconcile issue audit with current resolutions
* docs: record pagination PR and new Ditgar reproduction
* docs: reconcile merged fixes and preserve pending scope decisions
* docs: reconcile audit scopes and merged PR states
* docs: record Ditgar fix and verified margin closure
* docs: reconcile incremental audit totals
* docs: record RTL preview fix and latest issue resolutions
* docs: record final audit PR merges
* docs: reconcile final review evidence
* docs: record paragraph indentation and RTL canvas PRs
* docs: record marker fix and Unicode-space reproduction
* docs: record final implementation PR merges
* feat(editor): support whole-paragraph indentation
* fix(docx): retain indentation in quotes and RTL documents
* fix(exports): bound paragraph insets in narrow PDF columns
* fix(pdf): type bounded paragraph rendering consistently
* test(editor): use explicit list conversion commands
* fix(docx): preserve quote inset on list items
* feat(import): parse a PDF resume without an AI provider
Importing a PDF required a connected AI provider, so anyone without a
paid API key could only import the three JSON formats. Almost nobody
arrives with one of those files; they arrive with a PDF. The first thing
a new user tries to do was blocked behind bringing their own key.
Adds a deterministic parser that reads the text out of the PDF in the
browser and prefills the builder. It pulls the contact block, segments
the body on conventional headings, and maps entries to real items,
reusing the ATS period parser for dates so a date range is not mistaken
for a phone number.
Nothing is thrown away: header parts that do not map to a field go into
the description, and unrecognized headings become custom sections. The
imported sections are placed on the page so the result renders straight
away. Output is validated against the resume schema before it is
returned.
Text extraction groups items by baseline rather than trusting hasEOL,
and turns wide column gaps into a double space, which is what lets a
row split into company, position and location.
The AI path still runs when a provider is connected. Word import is
unchanged and still requires one.
Closes#3334
* fix(import): keep every section and entry the PDF actually contains
Review found three ways the parser lost or mangled content, all of them
reproducible.
A document whose first heading was not one of the known aliases never
started a section, because unknown-heading detection was gated on a
section already being open. Everything after it was swallowed as contact
header text. The header block is now bounded by where the contact
details stop, so a heading is recognized wherever it appears.
An entry spreading company, position and dates over three lines was
imported as two malformed items. A line that introduces an entry now
merges into the open entry instead of starting a second one.
An uppercase company such as ACME CORPORATION was read as a section
heading and fragmented the entry. A heading candidate followed by a date
line is now treated as an entry header, which is what it is.
Also escape single quotes, and construct the PDF worker inside the try
so the nested worker is terminated even if construction throws.
Title-case headings are deliberately still not treated as headings:
company and school names are title case too, and splitting on them would
fragment real entries. Such a section stays in the preceding one with its
text intact rather than risking loss.
* fix(import): look past a multi-line preamble before calling a line a heading
The previous guard only inspected the next line, so an uppercase company
followed by a separate role line and then the dates was still read as a
section heading. The experience or education entry was moved into a
custom section and lost.
Heading detection now scans a two-line window for the date that marks an
entry, and stops early at a bullet so a genuine heading whose section
opens with bullet points is still recognized.
The window can suppress a real heading whose first entry puts a bare date
two lines below it. That is the deliberate direction to fail in: a missed
heading leaves the text in the preceding section, while a misread entry
fragments structured content.
* fix(import): collect an entry preamble until its dates appear
An entry that spread company, role, location and dates over four lines
was imported as two broken items: the company with no dates, and the
location carrying the period.
The cause was in entry grouping rather than heading detection. Lines
before a date were only folded into the entry header when the date sat
on the very next line; anything earlier fell through to the description.
Preamble lines are now collected into the entry header until the dates
turn up, bounded by the same lookahead and stopping at a bullet, so an
undated section cannot swallow itself.
The heading lookahead widens to four lines to match, which is the
realistic maximum for company, role, location and dates.
* fix(import): harden local PDF resume parsing
* chore(import): document audited HTML construction
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* feat(skills): add inline layout option for skill items
* fix: restore default skills layout (regressed by inline feature)
- Restore metrics rowGap style for default layout
- Only render LevelDisplay inside the row for inline layout, not default
* refactor(pdf): Extract inline skills style logic from JSX to reusable function
* test(pdf): add test coverage for inline skills item layout
- Add test suite SkillsSectionInlineFormat to verify isInlineSkillsItem and getSkillsItemStyle behavior
* test(pdf): add comprehensive test coverage for inline skills item style logic
- Test combinations of proficiency, level, and keywords fields (0, 1, 3 fields)
* test(schema): add test coverage for column equals 1 when layout is inline
* test(web): add component-level tests for inline and columns layouts
* test(import): add v4 parser-level test for missing skills layout
* docs: regenerate skills layout references
* test(docx): include skills layout in section fixtures
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(pdf): keep list markers with their first text fragment
* fix(pdf): preserve page breaks while rewinding list companions
* fix(pdf): consume oversized list marker presence hints
* test(pdf): allow cold startup for pagination process guard
* fix(pdf): key list presence spacer
* fix(ai): make provider test timeout configurable via AI_TEST_TIMEOUT_MS
- Problem: the 30s hardcoded timeout is too short for self-hosted
deployments with cold-start models (e.g. Ollama). Makes it impossible
to pass the provider test (issue #3374).
- Fix: read AI_TEST_TIMEOUT_MS from the environment, defaulting to 30_000.
Zero behaviour change when the env var is absent.
- Verification: existing test asserts "30 seconds" in the timeout
message; default is unchanged so the test continues to pass.
(CI needs Node 22+ — not available on this host.)
* fix(ai): add AI_TEST_TIMEOUT_MS to turbo globalEnv so it reaches the API process
- Problem: Turborepo filters env vars not listed in globalEnv, so
AI_TEST_TIMEOUT_MS would always be undefined at runtime under
turbo dev/start, making the override dead code.
- Fix: add AI_TEST_TIMEOUT_MS to the globalEnv array.
- Verification: turbo.json validates as valid JSON.
* fix(ai): validate AI_TEST_TIMEOUT_MS as a finite non-negative integer
* docs(ai): add JSDoc to timeout parser and test helper
* test(ai): restore AI_TEST_TIMEOUT_MS after timeout tests
- Problem: loadWithTimeout() mutates process.env.AI_TEST_TIMEOUT_MS but nothing restores it, so the last value tested ("999999999999") leaked to every test that runs after this describe block in the same file.
- Fix: save the pre-test value and restore it in an afterEach hook.
- Verification: pnpm exec vitest run src/features/ai/service.test.ts in packages/api — 18/18 passed.
* test(api): isolate AI timeout environment cases
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(pdf): add left padding to section heading text to prevent first-character clipping
Closes#3380
* fix(pdf): apply heading padding default after style composition
Apply paddingLeft: 1 only when no composed style fragment already defines it, so an explicit paddingLeft from a template or style rule is preserved. Keep the fallback for an empty style list.
* fix(pdf): keep heading safety padding on text only
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(components/form): resolve FormControl label target regressions (#3369)
- Expose FormControlContext and wrap FormControl children in Base UI's
LabelableProvider so the generated control id reaches the actual
labelable element.
- Update InputGroup/InputGroupInput to consume the context and place
the id on the real input instead of the fieldset.
- Update Slider to discard the wrapper id and use the context via
LabelableProvider so the thumb input receives the id and
aria-labelledby.
- Update ChipInput to consume the context, set id and aria-labelledby
on the inner input, and only fall back to aria-label when not inside
a FormItem.
- Restructure the sidebar layout so a single FormControl labels the
numeric input and the visible FormLabel is referenced by id for the
sibling Slider, removing the duplicate-id defect.
- Add a dev-time warning when the generated id lands on a non-labelable
or missing element.
- Extend form.test.tsx with regression coverage.
* test(form): add regression coverage for chip-input and dual-control layout
* fix(ui): surface FormControl error state as aria-invalid on the Slider control
- Problem: FormControl injects aria-invalid={hasError} onto its rendered
element, but Slider stripped it without re-applying it anywhere, so the
error state never reached the DOM (flagged by Codacy/Greptile/CodeRabbit).
- Fix: bridge aria-invalid onto Base UI's native range input via the Thumb's
public inputRef prop; Base UI v1.7 has no prop path for it (its validation
props only apply through Base UI Field context). id stays stripped since
LabelableProvider already delivers it to the input.
- Verification: new regression test in form.test.tsx fails on the pre-fix
head (aria-invalid null) and passes post-fix; packages/ui 363/363 tests
green; tsc --noEmit on packages/ui clean.
* fix(ui): let a caller-supplied data-slot override the Slider default
- Problem: the FormControl label-target fix moved data-slot="slider" after
{...props} on SliderPrimitive.Root, so a caller's data-slot was silently
overwritten with the default — a prop-ordering regression against both the
prior file and the repo-wide convention (FormItem, FormLabel, InputGroup all
place data-slot before the spread).
- Fix: restore data-slot="slider" before {...props} so caller values win.
- Verification: packages/ui — vitest src/components/slider.test.tsx
src/components/form.test.tsx = 30/30 passing; new regression test
("lets a caller-supplied data-slot override the default") fails on the
pre-fix head (data-slot="slider" wins) and passes with the fix; tsc
--noEmit clean.
* fix(ui): preserve standalone Slider and InputGroup identity props
- Problem: the FormControl prop strip dropped a standalone caller's id on
Slider and id/aria-describedby/aria-invalid on InputGroup, so standalone
compositions rendered no element carrying those attributes (regression
vs main, flagged by maintainer review on this PR).
- Fix: strip the FormControl-generated props only when a FormControl
ancestor is present (useFormControl context); preserve explicit caller
props for standalone usage in both components.
- Verification: new standalone + FormControl-wrapped tests fail on the
prior head and pass after the fix; packages/ui 367/367, apps/web
595/595, tsgo --noEmit clean.
* fix(ui): remove internal label provider dependency
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(api): translate copilot AI provider failures to BAD_GATEWAY
- Problem: AI provider errors (bad key, unknown model, quota, 5xx) from the
AI SDK bubble out as opaque 500 INTERNAL_SERVER_ERROR from copilot
endpoints (autofill, match-score, draft-message, tailor-resume).
- Fix: catch AISDKError in generatePlainText and a local generateJson
wrapper that delegates to the shared generate-json module, translating
both to BAD_GATEWAY (502) with the original error preserved as cause.
Mirrors the existing pattern in features/ai/router.ts.
- Verification: vitest (CI — requires Node 22+). Test file unchanged in
assertion logic from the original PR; the local generateJson now
wraps the shared module instead of duplicating it.
Rebased onto main after v5.2.9 AI-layer refactor (generateJson extracted
into features/ai/generate-json.ts).
* fix(api): align generateJson prompt shape with callers and shared module
- Problem: local generateJson wrapper accepted (model, prompt: string,
schema) but all callers pass (model, { prompt: string }, schema).
Caught by CodeRabbit review.
- Fix: match the shared generate-json module signature — accept
{ system?, prompt } as the second argument and pass it through.
Updated test calls to match.
* fix(test): remove stray leading dots from mock object property names
- Problem: rebase onto v5.2.9 introduced `.use`, `.output`, `.errors`
as property names in the chain mock object, which is invalid JS
syntax and would cause a parse error when tests run.
- Fix: remove the leading dots to restore valid property names.
- Verification: cat -A confirms tabs-only indentation, no leading dots.
* fix(docs): correct 'a actionable' to 'an actionable' in comment
- Problem: Grammar typo in inline comment.
- Fix: 'a actionable' → 'an actionable'.
- Verification: grep confirms no remaining instances.
* fix(api): narrow copilot AI BAD_GATEWAY predicate to APICallError and exhausted RetryError
* feat(applications): export applications as CSV
* fix(applications): strip export CSV formula guard on import and resort catalogs
Re-importing an exported CSV kept the apostrophe that csvCell prepends to
formula-triggering cells, so a note starting with "- " came back as "'- ".
mapCsvToApplications now drops a leading apostrophe when the remainder would
have been guarded, sharing the predicate with csvCell so both sides stay in
sync.
Also runs pnpm lingui:extract: the new msgids were hand-appended to en-US.po
and missing from the other 54 catalogs.
* fix(applications): preserve CSV import values
* fix(stylesheet): keep color picker state aligned with source
* fix(stylesheet): serialize picker edits as hex with alpha
* fix: preserve contextual colors in stylesheet editor
* docs: track open issue audit and resolution plan
* docs: update issue audit with verified fixes
* docs: record cover-letter library verification
* docs: record OAuth and cover-letter CI verification
* docs: track compact views and remaining accessibility fixes
* docs: track CSV export and picture rendering fixes
* docs: record PDF localization, cache fix and consent review
* docs: track consent and rendering fixes in repository-only audit
* docs: refresh issue audit progress and review evidence
* docs: record latest issue reproductions and published fixes
Rewrites the landing page, in-app microcopy, and public docs, then fixes what the rewrite exposed: stale template counts, a broken quickstart anchor, out-of-sync FAQ structured data, dead error-hint branches in the MCP tools, and wrong-sense translations across all 53 locales. Adds GLOSSARY.md so translators get the right sense of the ambiguous UI terms.
* feat(ats): add ATS checker and replace resume analysis
Adds a public, browser-only ATS checker at /ats-checker and an ATS Check
section in the builder's right sidebar. PDFs are parsed locally: text
extraction, reading order, contact and date recovery, section detection,
and file-level readability are scored deterministically, with evidence
cited per finding and skipped checks reported rather than counted as
passes.
Removes the AI-scored resume analysis it supersedes: the resume_analysis
table (dropped via migration), the get_resume_analysis MCP tool, and
POST /ai/analyze-resume. The replacement, POST /ai/ats-review, reviews
extracted resume text and returns qualitative feedback with no score.
Also bumps the version to 5.2.9 and adds the changelog entry.
* chore(deps): bump workspace dependencies
* fix(ats-checker): keep negation inside each 'what this does not do' bullet
The three bullets were bare fragments whose negation came from the
section heading, which translators never see. A dozen locales rendered
them as affirmative assertions or imperatives, so the page claimed the
checker enforces a one-page rule and predicts rejection -- the opposite
of the source, and directly contradicted by the sentence beside it.
Each bullet now carries its own negation, so the polarity cannot be
lost in translation. Re-extracted and refilled across all 53 target
locales.
* fix(pdf): resolve bold text weight from the family's bold face
Bold text (<strong>, rich-text bold, template bold styles) previously
rendered at the last stored body weight, which is ambiguous: families
are commonly stored as ["400","600"] (the typography picker's default
pairing), so bold rendered at SemiBold — nearly indistinguishable from
Regular for faces like Open Sans (#3310).
Add resolveBoldFontWeight() to the fonts package: keep a deliberate
stored bold-class choice (>= 700), else prefer the family's true Bold
face ("700"), else the heaviest >= 600 face; return null so callers
keep their existing fallback when the family has no bold-class face.
Wire it through use-register-fonts, the shared base-template-styles
builder, base-styles and the Scizor template. Default body IBM Plex
Serif ["400","500"] now renders bold at 700 (base-reset-fidelity
expectation updated accordingly).
Fixes#3310
* fix(pdf): register bold fallback faces for CJK glyph substitution
When resolveBoldFontWeight maps stored weights like ["400","600"] to the
family's 700 face, register that weight on each PDF fallback font too so
glyph-level substitution keeps bold glyphs instead of snapping to 600.
Also reorder @reactive-resume/fonts imports per Biome convention.
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(pdf): render emoji via a Noto Emoji script fallback
Emoji in resume content (flags, globe, pictographs) rendered as mojibake
in the preview and PDF export because the per-codepoint fallback chain
registered no emoji-capable font: every font in the stack lacked the
glyphs, so layout fell through to single-byte standard-font encoding —
each UTF-16 code unit truncated to its low byte (#3321).
Follows the #2986/#3190 script-fallback pattern: detect emoji content
(regional indicators unioned with Extended_Pictographic), map it to the
monochrome Noto Emoji web font (TrueType glyf outlines, PDF-embeddable),
and register it in the fallback stack for both serif and sans stacks.
Out-of-range weight requests alias to the nearest served weight (300-700)
so registration never falls back to the preview subset.
* fix(pdf): detect keycap emoji via the combining enclosing keycap
Greptile review on #3351: keycap sequences like 1\uFE0F\u20E3 carry no
regional indicator and no Extended_Pictographic codepoint, so they
bypassed the emoji detector and rendered garbled — the exact class of
bug #3321 fixes. Union U+20E3 into the detector; every valid keycap
sequence contains it.
* Update packages/utils/src/locale.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* [autofix.ci] apply automated fixes
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
* fix(fonts): register Vazirmatn in webfont catalog for JSON imports
Imported resumes can set typography.fontFamily to Vazirmatn, but the
popularity-sorted Google Fonts slice omits it so PDF registration fell
back to IBM Plex Serif and Persian/Arabic glyphs stacked or tofu (#3098).
Add Vazirmatn as a locale-coverage manual entry (same pattern as Carlito)
and cover catalog resolution with unit tests.
* test(pdf): keep Vazirmatn as primary family for fa-IR registration
Prove JSON-imported Vazirmatn is handed to Font.register instead of
being rewritten to IBM Plex Serif (#3098).
* fix(fonts): address CodeRabbit review on Vazirmatn catalog
Assert getWebFontSource resolves files["400"] for Vazirmatn instead of
only matching the preview fallback, and split the font-generation log
line to satisfy the 120-column Biome limit.
Change richListItemContent from flex: initial to flex: 1 with minWidth: 0.
This makes the content area fill remaining space after the marker and
columnGap, so text wraps within the user-set margin instead of overflowing.
Fixes#3336
Upgrades to Better Auth 1.7, expands Custom Styles coverage of item headers, and adds a human-approval step to the AI agent's resume edits.
Breaking for self-hosters using a custom OAuth provider: the callback path changes from /api/auth/oauth2/callback/custom to /api/auth/callback/custom, and installs using OAUTH_DISCOVERY_URL need one additional UPDATE after upgrading. Both are documented in docs/self-hosting/sso.mdx.
- Better Auth 1.7, with the account issuer migration and the jwks alg/crv columns the 1.7 jwt plugin requires
- Agent edits gated behind an approval step, with crash-safe runs and context pruning
- item-header now covers every section header row on every template; adds the item-header-row part
- Fixes provider unlinking, auth error messages, and version conflicts on freshly created resumes
- New /auth/error page, translated across all 53 target locales
- DeepSeek Harness plugin moved into packages/dsh-plugin
- Dependency bumps across the workspace
Postgres defaultNow() stores microseconds while JS Dates are millisecond-truncated, so the SQL equality guard matched zero rows on freshly created resumes and every guarded agent patch failed with a permanent version conflict. The SELECT ... FOR UPDATE lock plus the in-transaction ms-precision check already provide the guarantee; drop the SQL predicate. Verified A/B against a live database.
Better Auth guards `/unlink-account` with `freshSessionMiddleware`, which
rejects any session whose `createdAt` is older than `freshAge` (one day by
default). Sessions here last a week and there is no re-authentication flow to
refresh that timestamp, so disconnecting a provider failed with
`SESSION_NOT_FRESH` for every user who signed in more than a day ago.
Disable the freshness gate, and teach `getReadableErrorMessage` to read plain
error objects: Better Auth client errors are `{ code, message, status }`
objects rather than `Error` instances, so every auth toast was collapsing to
its generic fallback instead of showing the real reason.
* docs(adr): propose agent AI SDK v7 adoption plan
* fix(ai): bind analyzeResume through aiService in service test
The test destructured analyzeResume as a named export that does not exist; main was red.
* test(agent): keep pure ai helpers real via spread-actual mock factory
* feat(agent): add run guards, patch version guard, run wall-clock timeout
* feat(agent): validate UI messages at the send boundary
* feat(agent): crash-safe draft-row persistence and server-side cancellation
* feat(agent): reap stale run claims at boot, on send, and on thread open
* feat(agent): fresh-document patch output and tiered context pruning
* feat(ai): shared agent tool contracts and message metadata schema
* feat(agent): add per-thread review-patches setting with update endpoint
* feat(agent): gate resume patches behind hmac-signed tool approval
* feat(agent): merge question answers and approval decisions before run claim
* feat(agent): approval ui with composed auto-send and fixture-driven tests
* feat(agent): usage metadata, tool activity cards, smoother streaming
* feat(agent): tool-call repair, input examples, structured step logging
* chore(i18n): translate new agent workspace strings across all locales
* fix(agent): gate stale-run draft cancellation on winning the claim clear
Snapshot streaming drafts before the conditional clear and skip the flip entirely when another reaper or a replacement run already cleared the claim. Also address review nits in eleven locale catalogs.
* fix(agent): flip reaped drafts only when their snapshotted state is unchanged
* fix(agent): address review findings across run lifecycle, context budget, and approval flow
- bind patches to the revision the model read via signed baseUpdatedAt
- claim the run before consuming a continuation; recorded-but-unexecuted approvals retry as pending continuations
- keep run ownership on stop() until cancellation persists; preserve the claim for the reaper when final persistence fails
- estimate tokens without serializing binary attachments (tokenx) and enforce the budget by dropping oldest whole turns
- mark crash-recovered patch results as snapshot boundaries; strip /data prefixes at execution time
- retry failed continuations without regenerate; mount a single AgentChat; disable response controls on read-only threads; freeze review toggle during runs (client+server)
- accumulate usage across continuations and match the SDK's nested usage shape; label-form token strings; reorderable source label; accessible note field; state-neutral web-search label
* chore(i18n): translate revised agent strings across all locales
* fix(agent): harden baseUpdatedAt validation and address review follow-ups
- bundle tokenx in the server runtime dependencies (e2e boot failure)
- strict ISO schema for baseUpdatedAt plus loud executor rejection of unparseable values
- it-IT source label consistency (Fonte)
- prove penultimate-turn retention in the context pruning test
* chore(deps): exempt tokenx from knip for the externalized server bundle
`SectionItemHeader` only rendered its own box when a template opted into
`mainItemHeaderBorder` (only Ditgar did). Everywhere else it walked the
header children and attached the resolved `item-header` style to the first
descendant that happened to be a literal `View` or `InlineItemHeader`.
Sections whose header starts with anything else — certifications, awards,
projects, publications, references — matched nothing, so the style was
silently dropped; stacked headers such as experience matched only their
first row, so a second row went unstyled.
The header now always renders its own `Div`, so `item-header` covers the
whole header row of every section on every template. `Div` rather than
`View` keeps the base row gap the rows used to inherit from the item box,
and Ditgar keeps its tight header via `rowGap: 0` on its own
`sectionItemHeader` slot, so rendered output is unchanged apart from the
newly styled rows. `mainItemHeaderBorder` is now dead and removed.
Fixes#3349
* docs: remove .superpowers
* feat(dsh-plugin): bring the DeepSeek Harness plugin into the monorepo
Moves dsh-plugin-reactive-resume out of its own repository and into
packages/dsh-plugin. It stays a published, public npm package — the only
one here — but now builds, typechecks, tests, and lints under the same
turbo tasks as everything else.
The move pays for itself in the drift guard. Standalone, the plugin kept a
generated snapshot of the tool names scraped from the live server card at
https://rxresu.me, plus a weekly CI job to notice when that snapshot went
stale. Sitting next to packages/mcp, it reads MCP_TOOL_NAME directly, so a
tool rename breaks the prompt guide on the same pull request instead of
days later. The snapshot, the fetch script, and the scheduled job are gone.
packages/mcp gains a ./tool-names export so that import goes through the
public export map rather than another workspace's src.
Also flips autoInstallPeers off. The DeepSeek Harness rc packages declare
peers that are host-supplied and, in one case
(@deepseek-ai/dsh-type-meta), not published at all, so auto-install 404s
the whole workspace. Turning it off drops only optional peers elsewhere;
@neodrag/core was the single hard peer that had been arriving implicitly,
and it is now declared where it is used. Full typecheck and test suites
pass, and pnpm peers check reports nothing new beyond the pre-existing
drizzle-orm range mismatch.
Tests move from test/ to colocated src/*.test.ts and the build output from
lib/ to dist/ to match repository conventions.
* fix(dsh-plugin): ship a bundle manifest and target the current Harness
`dsh plugin add` warned that the package "declares no dsh.bundle — installed
as a plain dependency, not a profile layer", and it was right. Every other
Harness plugin, in-box and third-party, ships a cordis.patch.yml and points
dsh.bundle.patch at it; that declaration is what joins a package to a
profile's bundle stack. Without it the package installed and then sat inert,
and the README's hand-written insert row was a workaround for the gap rather
than the intended way in.
The peer ranges were also a generation behind. They asked for
@deepseek-ai/dsh-mcp-client and dsh-system-prompt at ^0.0.1-rc.1, which
cannot match the 0.1.0-rc.6 a current harness ships, so the plugin could
never have resolved against the thing it targets. Both APIs are unchanged
across the bump — StreamableHttpConfig still takes the same six fields and
PromptSection still takes name/order/text — so this is a range correction,
not a migration.
That bump pays for itself elsewhere. The old generation peer-depended on
@deepseek-ai/dsh-type-meta, which was never published, and working around
that 404 is why merging this package turned autoInstallPeers off for the
whole repository and pulled @neodrag/core in by hand. The new generation
dropped that peer and publishes every other one, so both changes are
reverted and pnpm-workspace.yaml is back to what it was.
Because a bundle patch mounts the plugin the moment it is installed, a
required apiKey would fail config validation and take the profile down
before the user ever had a chance to mint a key. It now defaults to empty
and apply() warns and mounts nothing, matching how dsh-honcho-memory
handles the same problem.
Verified by packing the tarball and installing it into a clean project with
default pnpm settings: it resolves, imports, and reports its exports.
context.issue spreads to { owner, repo, number }, but Octokit v9 requires
issue_number. The request hit /repos/.../issues//labels and returned 404,
so no opened issue was ever labeled.
Section item headers render the title and its trailing date inside a shared
split row styled with `flex-wrap: wrap`. When the title is long the date wraps
onto its own line and left-aligns instead of staying pinned right, which reads
as inconsistent down a list of certifications.
That row had no selector. It is a bare View, so it never reached the semantic
tree: `item-header` matches the box around the row, and the title and date are
text nodes that layout properties do not apply to. There was no stylesheet that
could reach it.
Expose it as `template-part[name="item-header-row"]`, shared by every template
because the row comes from the shared section components. Awards,
certifications, projects and publications are covered; experience, education
and volunteer stack two rows and hand their headers to the
`inline-item-header-*` parts on some templates, so they are left alone.
Readers can now write:
@version 1;
template-part[name="item-header-row"] { flex-wrap: nowrap; }
The sonner wrapper was the only consumer of next-themes; the Base UI toast
that replaced it does not use the hook. knip flagged it as unused, and CI's
`knip --fix` step removed it and then failed `pnpm check` against a lockfile
that still listed it.
Fills the 25 strings added this cycle by the toast migration, the account page
rename, the Custom Styles status labels and the job posting auto-fill, across
all 53 target catalogs. The zu-ZA pseudo-locale is intentionally left empty.
Adds the questionnaire and empty-state primitives and renders the
ask_user_question tool call inline in the chat, so the agent can offer choices
instead of guessing when a request is ambiguous.
Fetching an arbitrary job URL server side meant owning SSRF defence, redirect
and size limits, and per-site scraping quirks. The autofill tool now takes only
pasted text, so the URL input, the fetch path and its MCP annotation are gone.
The sheet gates the call behind a tested AI provider and a minimum paste length
so a stray snippet does not spend an AI call.
The page now holds account-level actions rather than only destructive ones, so
it is reachable at /dashboard/settings/account and presented with a neutral
icon in the sidebar and command palette.
Swaps sonner's toast.success/error/loading/dismiss for the new toast.add({ type,
description }) and toast.close across dialogs, auth pages, the builder, the
dashboard and the applications views. Behaviour is unchanged.
Turbo defaults to ten concurrent tasks and each vitest sizes its pool to the
core count, so a ten-core machine ran roughly a hundred workers and a 1.6s test
blew its 15s budget. Different suites failed on every run. At concurrency four
the whole repo passed five runs straight with no wall-clock cost.
Each case rebuilt the whole spec, which walks every router and resume JSON
schema. The first case already carried a raised 15s timeout and still timed out
on a loaded machine. The spec is deterministic and only read here, so build it
once: the file drops from over 15s to 1.86s.
Without isolation the files in a worker share one module registry, so a
vi.mock of @reactive-resume/env/server in one file leaked into another and
whichever file imported the module first won. Measured on a clean cache,
isolate: false failed four of four whole-repo runs; with isolation, none.
Units that transitively import the validated server env threw at import time
whenever no .env was present, taking out packages/auth and packages/api. Seeding
the three required variables in the shared setup fixes every current and future
caller in one place. Real values still win.
The case passed cursor position 5 into "--resume-", which lands mid-token and
reads as a selector context, so it received the selector list. Every other case
in the file uses source.length.
The expected offset disagreed with its own line and column: line 2 column 17 is
offset 28, which is where `red` starts. Offset 31 pointed at `; }`. The sibling
UTF-16 case in the same file already used the correct arithmetic.
Anchors the offset to the source it must point at so it cannot drift again.
Disabling the textarea for the duration of a response made the browser blur it,
so the caret left the composer on every send and had to be clicked back. send()
already ignores calls mid-stream, so Enter stays a no-op and type-ahead works.
Every step-start part serialises to the same JSON, so the content-derived key
collided for any multi-step assistant message and React warned about duplicate
keys on each incoming chunk. Two identical text parts collided the same way.
Parts are append-only and never reordered by the AI SDK, so the index is stable.
Public resume pages only produced their OpenGraph and Twitter tags client side,
so a shared link had no card at all. The server now injects them into the shell
and swaps in the resume's own title and description.
The lookup is scoped to public, password-free resumes and deliberately avoids
resumeService.getBySlug: that counts a view and would expose a protected
resume's summary to an unauthenticated crawler. User-authored values are escaped
before they reach the HTML, and any lookup failure falls back to the plain shell.
getResumeSocialMeta is shared with the client route head so the two cannot drift.
X reads twitter:* meta tags from the name attribute, not property, so the card
validator reported twitter:title and twitter:description as missing. Also adds
the og:type tag the root head was never emitting.
The static middleware was mounted ahead of the web app fallback, and Hono's
serveStatic resolves "/" to the directory and returns dist/index.html verbatim.
handleWebApp never ran for the root route, so the OpenGraph, Twitter, canonical
and JSON-LD markup it injects was missing in production - fetching
https://rxresu.me/ as Twitterbot returned zero og: tags.
Route "/" explicitly before the static middleware so the injection runs.
* feat(resume): add a deterministic ATS parseability check
Adds an offline ATS linter that reports whether a parser can read a
resume, surfaced as an always-on panel in the builder.
The existing Resume Analysis panel needs a configured AI provider, so
users who never set one up get no feedback at all. These 22 rules run
as a pure function over ResumeData with no provider, no network and no
rendered PDF, so they work for everyone on every edit.
Rules cover contact details, date parseability, sections that hold
content but never render, column and sidebar placement, and typography
thresholds. The catalog mirrors the Semantic CSS diagnostic catalog:
stable codes carrying a severity, meaning and action, with no i18n
dependency so the web layer translates by code. Each finding carries a
JSON Pointer, which is what makes jump-to-field work.
Deliberately no second score. Resume Analysis owns overallScore, so
this reports "N of M checks passed" and counts by severity instead.
* fix(resume): accept localized ongoing periods and reject bare ones
Two period-parsing bugs found in review.
The ongoing-token set was English-only, so a German resume reading
"2020 - heute" was reported as unparseable and the panel told the user
to rewrite a perfectly valid range. Rather than guess translations for
55 locales, a range ending that carries no digits and is not a month
name in the resume's locale is now read as ongoing. That keeps a
genuinely incomplete ending such as "Jan 2020 - Feb" reported, since
"Feb" resolves as a month.
A bare "Present" also parsed as a valid period, so an experience entry
with no start date passed the check. A standalone ongoing token is now
rejected; ongoing tokens remain valid as the end of a range.
* feat(web): scroll ATS findings to the item they belong to
Findings for different items in one section all landed on the section
header, so a date problem on the third role gave no more help than
naming the section.
getAtsFindingTarget now resolves the offending item from the finding's
JSON Pointer against the resume, and SectionItem carries a matching DOM
id. The panel scrolls to that item and falls back to the section header
when the item is not mounted, which is what happens while its section
is collapsed.
* fix(resume): recognize ongoing periods by token, not by shape
The previous heuristic read any short, digit-free range ending as an
ongoing marker, so "2020 - unknown", "2020 - later" and "2020 - tbd"
parsed cleanly and suppressed the finding they should have raised.
Replaced with an explicit table of ongoing words keyed by language,
covering the locales the app ships. Matching is exact, so unrecognized
endings are reported again. A locale missing from the table falls back
to the earlier behaviour of reporting its ongoing periods, which is a
visible gap someone can close by adding a word rather than a silent
hole in detection.
Tests assert every listed token parses and that the table stays
lowercase, since lookups normalize that way.
* fix(ats): parse punctuated ongoing tokens
* fix(ats): parse Unicode punctuated ongoing tokens
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* docs(agents): align Redis compose commands with development guide
- Problem: AGENTS.md omitted Redis from dev infrastructure compose commands
while docs/contributing/development.mdx starts redis for local dev.
- Fix: document full postgres/redis/seaweedfs compose command and note that
REDIS_URL and ENCRYPTION_SECRET are required for AI agent features.
- Verification: preflight upstream fetch; manual diff against development.mdx
and compose.dev.yml redis service; duplicate PR gate passed.
* fix(pdf): ignore phantom gengar skill text nodes
- Problem: gengar template resumes with skills keywords fail semantic CSS activation because the legacy renderer emits a harmless empty text node that parity treats as a mismatch.
- Fix: treat the specific empty text artifact as presentation-neutral in the legacy parity comparator and add a regression test for the phantom fontSize 9 node.
- Verification: pnpm test src/semantic/legacy-parity.test.ts in packages/pdf passed (31 tests).
* docs: clarify host and container Redis URLs
- Problem: the development guide only showed the Docker Redis hostname, which fails for host-run development.\n- Fix: document localhost for host execution and redis for Docker execution.\n- Verification: pnpm test src/semantic/legacy-parity.test.ts (31 passed).
* fix(pdf): omit empty skill proficiency text
* test(pdf): cover blank skill proficiency
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(import): auto-detect JSON format and show readable errors (#1)
Readable import errors, a fail-soft v4 parser, and auto-detect of the JSON format so uploads just work. The format dropdown becomes an optional override. PDF and DOCX (AI) paths are untouched.
* fix(import): address review feedback on the v4 guard and error message
- reactive-resume-v4-json.tsx: reject arrays in isRecord so array-valued
basics, sections, or metadata no longer pass the v4 shape guard.
- reactive-resume-v4-json.tsx: reuse the guard's error instance in the
catch arm instead of allocating a duplicate NOT_V4_MESSAGE.
- error.ts: use a singular "Problem" label for root-level Zod issues so
the message stays grammatical.
- add a regression test for array-valued v4 branches.
* fix(import): preserve selected JSON format
* test(import): cover selected JSON parser
---------
Co-authored-by: MrTig-afk <MrTig-afk@users.noreply.github.com>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
react-zoom-pan-pinch calls preventDefault() on its window-level mousedown
listener so that dragging the canvas does not select text. That also cancels the
browser's native focus shift, so focus stays wherever it was before the pan --
typically the sidebar button that opened the last dialog, since closing a dialog
restores focus to its trigger. A focused button activates on Space keyup, so
holding space to pan and then releasing it reopened the most recent dialog.
Blur the focused element from onPanningStart, which reinstates exactly the focus
change the browser would have made on its own. onPanningStart only fires when the
mousedown target is inside the transform wrapper, so sidebar and dialog clicks are
unaffected, and keyboard-only users never trigger a pointer pan.
Closes#3300
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
The icon picker grid starts with an empty string entry that renders the
"no icon" (prohibit) symbol, but the onClick guard "if (icon)" treated
the empty string as falsy and ignored the click. Change the guard to
check for a defined string value so the empty/no-icon option can be
selected.
Closes#3252Closes#3261
Co-authored-by: Devin <devin@example.com>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* docs(agents): align Redis compose commands with development guide
- Problem: AGENTS.md omitted Redis from dev infrastructure compose commands
while docs/contributing/development.mdx starts redis for local dev.
- Fix: document full postgres/redis/seaweedfs compose command and note that
REDIS_URL and ENCRYPTION_SECRET are required for AI agent features.
- Verification: preflight upstream fetch; manual diff against development.mdx
and compose.dev.yml redis service; duplicate PR gate passed.
* docs(agents): clarify Redis development URLs
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(auth): use loopback URL for MCP OAuth JWKS verification
Fetch the JWKS endpoint over the internal loopback address instead of the public APP_URL, so token verification works under Docker port-mapping, reverse proxies, and other deployments where the public URL does not loop back to the Node process.
Also log the specific MCP OAuth verification error instead of swallowing it with a bare catch.
Fixes#3077
* fix(auth): normalize internal JWKS URL and throttle MCP OAuth warnings
- Problem: default loopback JWKS URL used PORT in dev where the server
listens on SERVER_PORT (3001), and trailing-slash overrides produced
//api/auth/jwks; unthrottled warn logs could flood on bad bearer tokens.
- Fix: resolveInternalBaseUrl trims/normalizes BETTER_AUTH_INTERNAL_URL,
mirrors apps/server listen-port selection, and MCP OAuth warnings are
throttled to once per minute.
- Verification: pnpm exec biome check on changed files; pnpm typecheck.
* fix(auth): declare BETTER_AUTH_INTERNAL_URL in turbo globalEnv
- Problem: Turborepo strict env mode strips undeclared BETTER_AUTH_INTERNAL_URL under pnpm dev, so the JWKS override silently falls back to loopback.
- Fix: add BETTER_AUTH_INTERNAL_URL to turbo.json globalEnv (required for any new env var per CLAUDE.md).
- Verification: python3 JSON parse of turbo.json; confirmed var was absent from globalEnv before this change.
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(pdf): register Noto punctuation fallback for missing glyphs
- Problem: U+2022 bullet characters render as garbled glyphs when the body
font (e.g. IBM Plex Serif) lacks the glyph and no PDF fallback is registered.
- Fix: append Noto Serif/Sans to the PDF fallback stack as a general-purpose
punctuation source covering General Punctuation (U+2000–U+206F).
- Verification: pnpm --filter @reactive-resume/fonts test;
pnpm --filter @reactive-resume/pdf test src/hooks/use-register-fonts.test.ts
* test(fonts): clarify zh-CN fallback test description
- Problem: getPdfFallbackFontFamilies("Times-Roman", { locale: "zh-CN" }) now
returns ["Noto Serif SC", "Noto Serif"] (the general-purpose punctuation
fallback is appended), so the test description "returns only the Simplified
Chinese font for zh-CN (unchanged behavior)" is no longer accurate.
- Fix: rename the test to describe that it uses the Simplified Chinese font
plus the punctuation fallback. The assertion is unchanged.
- Verification: pnpm --filter @reactive-resume/fonts test -> 45/45 passing.
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
- Problem: development.mdx and architecture.mdx frontmatter still referenced
removed client/worker/artboard apps even though the monorepo only ships
apps/web and apps/server.
- Fix: update both descriptions to say web and server apps.
- Verification: docs-only; grep confirms only apps/web and apps/server exist.
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* docs(contributing): align development guide with dotenvx workflow
- Problem: development.mdx told contributors to use a root `.env` file and
export DATABASE_URL manually, while AGENTS.md and compose.dev.yml use
`.env.local` loaded through dotenvx for dev and migration commands.
- Fix: update the setup, migration, dev-server, and database sections to
match the dotenvx commands documented in AGENTS.md.
- Verification: preflight_ship.py (upstream bug marker present); duplicate
PR check clean; docs-only change.
* docs(contributing): add cp command to env setup step
- Problem: setup step said to copy .env.example but the bash block only listed variable assignments.
- Fix: add explicit cp .env.example .env.local command and label the following block as edits.
- Verification: manual review of development.mdx; addresses CodeRabbit review on #3286.
* docs(contributing): align AGENTS.md env copy target with dotenvx
- Problem: AGENTS.md told contributors to copy .env.example to .env while all dev commands use .env.local.
- Fix: update the copy instruction to .env.local for consistency with the dotenvx workflow.
- Verification: manual review; folded into #3286 dotenvx alignment PR.
* docs(contributing): dotenvx-wrap remaining dev script references
- Problem: scripts table and troubleshooting still showed bare pnpm dev/db commands after the dotenvx workflow update.
- Fix: prefix dev, db, and port-override examples with dotenvx run -f .env.local --.
- Verification: manual review of development.mdx; folded into #3286.
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Node 24 terminates the whole process on an unhandled promise rejection, so a
single request's stray rejection could take the server down for every user
(as the USER_STOPPED agent-abort bug did). Add a process-level unhandledRejection
handler that logs and keeps serving. Uncaught exceptions are intentionally left
on Node's default crash-and-restart, since process state is unsafe afterward.
Claude-Session: https://claude.ai/code/session_01ULhhLQ24DvnYwzP4afDuye
Stopping (or archiving) an agent run called controller.abort("USER_STOPPED")
with a plain string reason. The AI SDK only recognizes a cancellation when the
reason is an AbortError (err.name === "AbortError" / isAbortError); a bare
string is treated as a real stream error, and its rejection escaped the
background resumable-stream pump and crashed the whole server process with
ERR_UNHANDLED_REJECTION on every user Stop. Abort with a DOMException named
AbortError (label preserved as the message) so the SDK cancels the run
gracefully. Same fix for the USER_ARCHIVED path.
Claude-Session: https://claude.ai/code/session_01ULhhLQ24DvnYwzP4afDuye
- Add an sr-only "Copy secret" label to the icon-only copy button in the 2FA
enable dialog; it was previously announced as an unlabeled button.
- Add a focus-visible ring to template gallery cards. The only ring was gated
on the selected state, so keyboard focus was invisible while tabbing.
- Disable the API-key create submit button while the request is in flight to
prevent duplicate keys from a double-click.
Surfaced by a shadscan UI audit. The remaining ~95 findings were false
positives from the auditor not understanding the pnpm monorepo and the
TanStack Start root-route shell, and were waived.
Claude-Session: https://claude.ai/code/session_01JYTniVDeA56o1kGhdoCUoD
The dependency-update commit bumped @react-pdf/renderer 4.5.1->4.6.0 (textkit
6.3.0->6.4.0) and silently dropped the pnpm patch that overrides font vertical
metrics to prefer OS/2 sTypo* over inflated hhea values, matching browser line
boxes. 6.4.0 did not upstream it, so the semantic-CSS template visual snapshots
(baselined with the patch, maxDiffPixelRatio 0) no longer matched and the E2E
job failed. Re-create the patch for textkit@6.4.0 and re-register it in
patchedDependencies; remove the orphaned 6.3.0 patch.
Claude-Session: https://claude.ai/code/session_01ULhhLQ24DvnYwzP4afDuye
The server PDF preflight spawned a fresh worker per semantic-CSS edit, each
racing a 15s startup deadline to cold-load the ~721kB+5MB PDF runtime. That
load is super-linear in CPU (~3s at 1 vCPU, >15s on a throttled/shared vCPU),
so on a constrained box every edit hit the startup-timeout path and returned
STYLESHEET_PREFLIGHT_WORKER_FAILED. Since the service only advances the applied
stylesheet when preflight passes, custom styles never applied and the editor
stuck on Checking.
Warm one worker at boot and reuse it (message-based input, respawn on
crash/timeout), so the cold load is paid once instead of per edit. Raise the
render deadline 5s->30s (a rich resume renders ~5-18s on a slow box) and the
readiness ceiling to 120s so the one-time warm completes even when throttled.
Surface worker load failures instead of an unhandled-rejection crash, and log
runner-side failure paths so the previously opaque failure is diagnosable.
Verified in node:24-slim under --cpus=0.25/0.35/0.5: all reused requests pass.
Claude-Session: https://claude.ai/code/session_01ULhhLQ24DvnYwzP4afDuye
Concurrent compile requests from editor intelligence were rejecting
in-flight edit compiles as stale, and the store swallowed that rejection
without leaving compiling. Resolve all compile results and surface
compile failures as an error status so styles can apply again.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Add comprehensive Application Tracker REST and MCP coverage, document the MCP workflow, add Markdown/ActionLint checks, bump the release version, and fill all extracted translations.
* feat(applications): job application tracker with AI copilot
Add an Applications module at /dashboard/applications: pipeline board
(dnd-kit), table view with bulk actions, Insights (fit tiles, funnel,
sources, shareable funnel-flow SVG), campaigns, tags, CSV import, and
Add/Edit/Detail slide-overs. Each application links a live Reactive
Resume.
AI "Application Copilot" (applications.ai.*): job-posting autofill,
resume↔job match score (fit ring), resume tailoring, and cover-letter /
follow-up drafting — via the user's configured provider.
Board cards + table rows get context menus (edit / move / archive /
delete). Charts are CSS/SVG (no new chart dep); adds a UI Checkbox.
Also includes local TanStack devtools setup and toolchain bumps.
Claude-Session: https://claude.ai/code/session_01TEeRHnEayw2MFCShFRyL5f
* feat(applications): close follow-up gaps + squash migrations
Finish the deferred/open items on the applications tracker:
- Cover-letter upload re-enabled. Fix the storage blocker by deriving the
key extension from content type (buildFileKey/EXTENSION_BY_CONTENT_TYPE)
instead of hardcoding .jpeg, so PDFs serve correctly and non-JPEG image
avatars keep working under FLAG_DISABLE_IMAGE_PROCESSING. Add
coverLetterUrl/coverLetterName columns + Documents-section upload/remove.
- Contacts editor in the detail sheet (add/edit/remove, keyed per app).
- Board caps rendered cards per column (COLUMN_PAGE_SIZE=50 + "Show more").
- Extract new Lingui messages across locales.
- Guard coverLetterUrl to http(s)/relative at the API boundary.
Squash the five branch-only application-table migrations (create -> +tags
-> +cover-letter -> drop -> re-add) into a single clean CREATE TABLE via
drizzle-kit generate.
Claude-Session: https://claude.ai/code/session_01TEeRHnEayw2MFCShFRyL5f
* chore: update dependencies
* fix(web): address React Doctor findings — compiler, purity, query, component structure
prefer-module-scope-pure-function: hoist buildSubtitle, getDecimalPlaces,
handleLocaleChange, onLocaleChange, stop, listContent/groupedListContent to
module scope so they aren't rebuilt on every render.
react-compiler-todo (??=): rewrite draft.metadata.styleRules ??= [] to the
non-assignment form to unblock auto-memoization.
set-state-in-effect: derive updatedAtLabel at render time instead of syncing
it through useState + useEffect.
query-destructure-result: destructure useQuery results at call site in
resume-analysis and resume-thumbnail to follow TanStack Query v5 convention.
only-export-components: extract non-component exports to sibling .ts files so
Fast Refresh can preserve component state:
- getNextWeights → typography/get-next-weights.ts
- detectJsonImportType + ImportType → dialogs/resume/import.utils.ts
- getLocaleOptions → features/locale/locale-options.tsx
- preview helpers + DEFAULT_PDF_PAGE_SIZE → preview.shared.utils.ts
- resolveHighlightToolbarState + defaultHighlightColor → rich-input.utils.ts
- computeDelta + getSparklinePoints → statistics.utils.ts
no-multi-comp: split multi-component files into focused companions:
- ResumePane + ToolbarButton → routes/agent/-components/resume-pane.tsx
- DesktopBuilderShell → builder/$resumeId/-components/desktop-builder-shell.tsx
- MobileBuilderShell + helpers → builder/$resumeId/-components/mobile-builder-shell.tsx
- setBuilderLayout/getBuilderLayout moved to -store/sidebar.ts
fix(tests): add Resume type import to section-builder mocks and cast partial
mock data as unknown as Resume to satisfy stricter type checking; fix
noExplicitAny Biome errors in the same mocks.
* feat(applications): improve performance
* chore: fix knip issues
* perf(builder): halve per-keystroke render cost
Section-form fields called `form.handleSubmit()` on every keystroke, which
re-validated the whole form and toggled submit state — firing the render
cascade twice per character (~6809 renders/keystroke, FPS dropping to 9).
Persist via a form-level `listeners.onChange` instead and drop the per-field
`handleSubmit()` (basics, custom-fields, design). Narrow header/dock resume
subscriptions to metadata slices so they no longer re-render on content edits.
Cuts renders 6809 -> 3403 per keystroke (50%), 0 frame drops. Save, preview,
and design controls verified working; 449/449 web tests pass.
* perf(home): eliminate hero CLS from unreserved video box
The hero <section> is `flex items-center` (shrink-to-fit), so the video
wrapper's width depended on the video's intrinsic size, which only resolves
after the media loads. aspect-ratio couldn't reserve height without a definite
width, so the video grew from ~190px to ~563px after first paint and shoved the
centered hero text down ~373px (CLS ~0.095).
Give the wrapper a definite width (w-full + mx-auto on the CometCard) and set an
explicit aspect ratio + width/height on the video so its box is reserved before
load. CLS 0.095 -> 0; hero stays visually centered at max-w-4xl.
* docs: add application tracker guides
* chore(db): squash application migrations
* fix(email): import React in auth template for server-side rendering compatibility
* chore(release): v5.2.1
* Refactor resume rendering and builder workflows
* fix: address application tracker review findings
* feat(export): separate resume/cover-letter downloads, redesign dialog, add Markdown
Let people export the resume and cover letter as distinct documents, and add a
Markdown format alongside PDF / DOCX / JSON (handy for AI agents).
- Server/API: scope PDF generation and download URLs to a resume/cover-letter target.
- Export domain: getResumeExportData + resumeHasCoverLetter in @reactive-resume/resume.
- Redesign the download dialog: one global "What to export" scope toggle (Tabs) plus
flattened per-format rows, reusing existing UI components and design language.
- Add Markdown export (@reactive-resume/resume/markdown) with a small tiptap-HTML converter.
- Fix blank section headings in DOCX and Markdown by injecting the locale-aware
section-title resolver (titles are stored empty and resolved at render time).
- Locale catalogs updated for the new strings.
* test(e2e): open the download dialog before exporting JSON
The JSON export moved into the redesigned download dialog, so the spec now opens
the dialog from the Export sidebar section before clicking "Download JSON".
- Add `import type * as React from "react"` to the 26 previews that reference
`React.CSSProperties` under the automatic JSX runtime (React isn't a global
type namespace there, so the annotation was unresolved standalone).
- Accordion preview: use `multiple` instead of `openMultiple` — @base-ui/react
1.6 renamed the prop, so the multi-open cell wasn't actually multi-open.
Skipped CodeRabbit's BrandIcon dark-mode note: the preview renders in the
default light card (the dark <img> is hidden there); per-theme sources would
need component support it doesn't expose.
Claude-Session: https://claude.ai/code/session_01R8Aq8F1nTuvJwfut7g3DVE
The utils color fallback restore re-added the @uiw/color-convert import
to packages/utils but not to apps/server, whose bundle keeps the package
external. Production server startup failed with ERR_MODULE_NOT_FOUND,
breaking the E2E workflow on main. Re-add the dependency.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Code-review findings:
- donation-toast: restore path/secure/sameSite cookie attributes the
inlined useCookie dropped (security/scope regression).
- utils/color: restore @uiw fallback so percentage-notation rgb() still
converts (custom style-rule colors are arbitrary strings); add tests
pinning the one real difference vs the black fallback.
- knip: drop stale npm-check-updates ignoreDependencies entry.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
These three vars are only read by GitHub Actions workflows and tooling/fonts
scripts that access process.env directly — never by app/server runtime code.
Removes them from the env schema (server.ts) and turbo.json globalEnv.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
The 107-line useCookie hook had exactly one consumer (donation-toast) that
only read + set-with-expiry. Inline the two Cookies.* calls directly and
delete the hook + its 128-line test. Drop js-cookie and @types/js-cookie
from packages/ui/package.json (apps/web retains its own js-cookie dep).
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Each of the 14 section-item dialog files (award→volunteer) had identical
~50-line Create/Update shells (DialogContent + header + form + footer).
Added section-item-dialog.tsx with a SectionItemDialog wrapper that takes
title, icon, onSubmit, onCancel, isSubmitting, submitLabel, singleColumn?.
cover-letter and summary-item use singleColumn=true for their one-column
layout. custom.tsx is untouched (it creates section definitions, not items).
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Loosen useResumeExport param to ExportableResume { name, slug, data } so the
public resume page (where name may be '' for non-owner viewers) can reuse it.
getExportName() falls back to data.basics.name then slug, matching the
original inline logic.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
normalizeResumePreviewProps had exactly one production caller (preview.tsx).
Defaults now live in the ResumePreview destructuring; the normalizer and its
test cases are removed.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
defineDialogRenderer/defineDialogRendererRegistry were identity functions.
Each registry now exports a readonly AnyDialogRendererEntry[] directly;
renderer-registry.ts retains only the types. The unused 'domain' field and
its wrapping object are gone; renderers.tsx spreads the arrays directly.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Three auth-settings components (password, two-factor, social-provider) each
duplicated an identical m.div hover/tap wrapper for both branches of
match(boolean). Extracted one ActionButton wrapper and replaced match with
a plain ternary; removed ts-pattern imports.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Number(null) === 0 caused the old code to use a separate :initialized key as
a migration sentinel. A plain null check on localStorage.getItem() is
sufficient and removes the sentinel key entirely.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Extract useTypographyForm helper (captures form creation + sync) and
TypographyForm type. Extract TypographyGroupFields component with
prefix "body" | "heading" to replace 2x4 duplicated form.Field blocks.
Font Weight label ("Font Weights" vs "Font Weight") is preserved per
prefix.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
useInitializeResumeStore, useMergeResumeMetadata, useSaveStatus,
useCanUndo, useCanRedo, useUndoResume, useRedoResume each had exactly
one caller. Inline useResumeStore selectors at call sites and remove
the wrappers. Skipped usePatchResume (4 callers).
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Extract makeCustomSection factory to eliminate duplicate CustomSection
object literal. Replace hand-maintained SectionType array in
isStandardSectionId with membership check via `id in sections`.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
The uploads endpoint is public (Cache-Control: public, no auth headers),
so a plain img src suffices. Remove createPicturePreviewUrl, the useQuery
call, and the object-URL cleanup useEffect; simplify PicturePreviewControls
to use normalizedPictureUrl directly.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Remove generic selector overload from useBuilderSidebar; callers
destructure the full return object instead of using a selector that
provides no meaningful benefit (state is rebuilt on every render).
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
agent/service.ts uses findLastIndex/Array.prototype.with (ES2023); web
type-checks api source and its ES2022 lib lacked them. lib only affects
type defs, not Vite runtime output.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Finding 1: Replace hand-rolled MONTH_NAMES[12] array with Intl.DateTimeFormat("en-US", {month:"long"}).
Finding 2: Drop @uiw/color-convert fallback — parseColorString already rejects the same inputs, return "#000000" instead. Remove dep from utils and server package.json.
Finding 3: Replace 56 z.literal calls in z.union with z.enum([...]); identical parse behavior and inferred type.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Templates import no React (react-email convention); tsx resolves each
file's nearest tsconfig, so jsx:preserve made esbuild emit classic
React.createElement and background email rendering threw
'React is not defined'.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
- health.ts: swap hand-rolled withTimeout for es-toolkit's (fn-taking API); remove
redundant inner try/catches from checkDatabase/checkStorage since runCheck catches
all errors (findings 13, health cleanup)
- web.ts: merge handleWebApp/handleWebAppHead into one function; method is the only
difference — isHead determines body presence (finding 14)
- app.ts: collapse two separate GET/HEAD wildcard routes into app.on(["GET","HEAD"])
- Update web.test.ts and app.test.ts to drop handleWebAppHead references
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Removes fs, path, env, getLocalDataDirectory imports from statistics service.
A module-level Map<string, {value, cachedAt}> gives the same TTL semantics
without touching disk. Adds clearStatisticsCache() for test isolation and
updates the test to call it in afterEach instead of relying on unique
LOCAL_STORAGE_PATH temp dirs (finding 5).
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Replace 12 near-identical try/catch blocks in threads/actions/attachments/messages
handlers with a single AnyMiddleware that maps the AGENT_ENVIRONMENT_UNAVAILABLE
sentinel to PRECONDITION_FAILED ORPCError.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Move 14 identical style slots (text/heading/div/inline/link/small/bold/
richParagraph/richListItemRow/richListItemMarker/richListItemContent/
splitRow/alignEnd/picture) from all 15 template Page.tsx files into a
single createBaseTemplateStyles factory in templates/shared. Each template
now spreads …base and keeps only its real overrides. Resolved StyleSheet
values are identical to before. Update rtl-fixture and rich-text-template-
styles tests to guard the factory file rather than each template directly.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Add three shared contact-item components to packages/pdf/src/templates/shared/
contact-item.tsx alongside the existing WebsiteContactItem and CustomFieldContactItem.
Replace ~18 lines of inline email/phone/location JSX in all 15 template headers
with the shared components (EmailContactItem accepts an optional iconName prop
for ditgar's "at" variant; rhyhorn's array-push pattern also updated).
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Two match chains in sections.tsx are replaced with plain lookup maps typed
via `satisfies Record<CustomSectionType, ...>` which preserves compile-time
exhaustiveness without the ts-pattern dependency. Remove ts-pattern from
packages/pdf/package.json.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Export parseFiniteNumber and parsePxValue from icon-size.ts (already the
canonical home of these helpers). Remove the three private copies in
rich-text-spacing.ts and import the shared ones instead.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
- Delete templates.test.ts: re-tests z.enum semantics with a hardcoded
fixture that duplicates the template list in templates.ts.
- default.ts: add 2-line section(icon) helper; 12 repeated 7-field blocks
collapse to one-liners. Output is byte-identical.
- data.ts: add 2-line itemSection<T> factory; 12 baseSectionSchema.extend()
blocks collapse to one-liners. Inferred types unchanged.
- data.ts: replace 15-field hand-listed styleRuleSlotsSchema with
z.partialRecord(styleSlotSchema, styleIntentSchema); parse behaviour and
TypeScript type are equivalent (unknown keys still rejected).
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
AuthProvider was the only zod usage in the package — a z.enum solely to
infer a type. Replace with a plain union type and remove zod from
packages/auth/package.json dependencies.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
- Replace pdf-parser-system.md + docx-parser-system.md with a single
parser-system.md template; prompts.ts substitutes 6 placeholders per
source type. Produced strings are byte-identical to the former files.
- Remove makeEmptyItem recursive walker (all SECTION_ITEM_SHAPES leaves are
already zero-valued) and call structuredClone(shape) instead.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
- Add getBaseRun() next to getHtmlStyle() and replace 8 inline baseRun spreads (~27 lines).
- Hoist one mainConfig object in buildDocument; sidebar call spreads only the two
differing color keys (~17 lines).
- Remove BUILT_IN_SECTIONS Set; derive membership via `sectionId in data.sections` (~14 lines).
- Delete unreachable ordered-list numbering machinery in html-to-docx.ts: numberingRef is
always undefined through all call paths, so isOrdered && numberingRef is always false (~15 lines).
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
- Extract one TOOL_META record (title/description/inputSchema/annotations per
tool) consumed by both registerTools and buildMcpServerCard, eliminating the
~200-line duplication in the server card.
- Collapse TOOL_ANNOTATIONS from 14 × 4-line inline objects to 5 named
annotation-preset consts (READ_IDEMPOTENT, WRITE_NON_IDEMPOTENT, etc.),
saving ~55 lines.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Some providers (OpenAI Responses API, others) reject system-role messages in
the messages array with AI_InvalidPromptError. Move the analyze/parse system
prompts to the generateText system option, matching the chat handler.
97 strings translated for Amharic (am-ET), 97 for Greek (el-GR),
97 for Khmer (km-KH), and 98 for Thai (th-TH). All placeholders
preserved verbatim ({0}, {label}, <0>, __APP_VERSION__, etc.).
Claude-Session: https://claude.ai/code/session_012jucCw5SQBpWMoZYwVEbeC
Fill in 96 empty msgstr entries per locale covering new UI strings
(AI assistant, version history, dashboard, account menu, connection
status, editor controls, and more).
Claude-Session: https://claude.ai/code/session_012jucCw5SQBpWMoZYwVEbeC
Fill in all empty msgstr entries (96 per file, 92 for ro-RO) covering
new UI strings: AI assistant, version history, undo/redo, dashboard,
connection status, export data, and related builder strings.
Claude-Session: https://claude.ai/code/session_012jucCw5SQBpWMoZYwVEbeC
A Postgres connection can drop at any time — e.g. a serverless Postgres such as
Neon terminating the connection (error code 57P01). node-postgres surfaces this as
an 'error' event; without a listener node re-throws it as an unhandled 'error' and
crashes the process. Idle clients emit on the pool, but a client that is connecting
or checked out emits on the client itself, so we listen on both the pool and each
client. The pool then discards the dead client and opens a fresh one on the next
query, so the server survives transient/idle disconnects.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs: add weekly changelog entry for post-v5.1.9 changes
* docs: improve changelog title and description for SEO
---------
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
overflow: hidden on Text elements in @react-pdf/renderer v4.x clips
content at its initial computed height, hiding any text after a line
break. minWidth/maxWidth/flexShrink already handle horizontal
containment so nothing else breaks.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevent the sponsor logo images from intercepting clicks so the
anchor reliably opens atlascloud.ai in a new tab instead of the SVG
asset.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* fix: use language-specific Noto fonts for CJK PDF fallback
* feat: extend fallback to Arabic/Hebrew/Thai
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix: use non-empty placeholder for redacted resume name
* fix: update stale test title to match new placeholder behavior
---------
Co-authored-by: Albano <alumno26.fazzito.albano@ipm.edu.ar>
Some providers (notably Anthropic via proxies) wrap JSON output in
markdown code fences (```json ... ```), causing Output.object to
throw NoObjectGeneratedError / JSONParseError.
Replace Output.object with manual JSON boundary extraction that works
regardless of fencing. Also propagate the original AISDKError as cause
in throwAiProviderGatewayError for better diagnostics.
* feat(editor): add multicolor highlight with auto-contrast text
Enable the Tiptap Highlight extension in multicolor mode, replacing the
single-color yellow toggle with a full color picker (16 presets + custom).
When the chosen highlight color is perceptually dark, text inside the mark
automatically renders white for readability.
Changes span the full pipeline:
- Editor: ColorPicker UI, extended renderHTML for contrast detection
- PDF: normalizeMarkElements preserves data-color as inline style
- DOCX: mergeStyle reads actual background-color from <mark>
- Utils: new isDarkColor() luminance helper
Backward-compatible: legacy <mark> without data-color still renders yellow.
Resolves#3109
* fix: handle multicolor highlight edge cases
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* feat: add section heading icons to PDF templates
Add customizable Phosphor icons before section titles in PDF output.
Users can toggle visibility globally via a new "Hide section heading icons"
switch (independent of item-level icons) and customize individual section
icons through the builder sidebar icon picker.
- Add `icon` field to `baseSectionSchema` and `summarySchema`
- Add `hideSectionIcons` to `pageSchema` (defaults to true for backward compat)
- Implement `SectionHeadingIcon` component with heading font-size scaling
- Support "none" sentinel for per-section icon hiding
- Fallback to sensible defaults (briefcase, graduation-cap, etc.) for legacy data
- Add icon picker to builder sidebar sections and custom section dialogs
Closes#2632
* test: add unit tests for section heading icons
- Add tests for getResumeSectionIcon() covering built-in sections,
summary, custom sections, "none" sentinel, and default fallbacks
- Add schema tests for baseSectionSchema icon field, summarySchema icon,
and pageSchema hideSectionIcons default behavior
* refactor: minor updates to icon display
* Update apps/web/locales/es-ES.po
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* fix(pdf): apply custom style fontSize to icon and level indicator sizes
Map fontSize from Icon and Level Indicator custom style slots to Phosphor
icon size and level indicator dimensions, since react-pdf icons ignore
fontSize in favor of the size prop.
* fix: separate global icon and scoped level indicator font sizes
Icon slot fontSize now drives all resume icons plus level display
decorations. Level indicator fontSize overrides only within level display.
Shared sizing logic lives in schema; design sidebar preview uses global rules.
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
description:Create a bug report to help improve Reactive Resume
description:Report a reproducible problem with Reactive Resume
title:"[Bug] <title>"
labels:[bug, v5, needs triage]
assignees:"AmruthPillai"
labels:["bug","status: needs triage"]
assignees:[]
body:
- type:checkboxes
attributes:
label:Is there an existing issue for this?
description:Please search to see if an issue already exists for the bug you encountered.
label:Existing issue
description:Search open and closed issues before submitting a new report.
options:
- label:Yes,I have searched the existing issues and none of them match my problem.
- label:I searched the existing issues and could not find a matching report.
required:true
- type:dropdown
id:variant
attributes:
label:Product Variant
description:What variant of Reactive Resume are you using?
label:Product variant
description:Where does the problem occur?
options:
- Cloud (https://rxresu.me)
- Self-Hosted
- Cloud
- Self-hosted
validations:
required:true
- type:input
id:version
attributes:
label:Reactive Resume version
description:Find this in Settings or provide the container image tag or commit SHA.
placeholder:5.2.6
validations:
required:true
- type:dropdown
id:area
attributes:
label:Area
description:Choose the part of Reactive Resume most closely related to the problem.
options:
- Resume builder & data
- Templates, preview & export
- Accounts & sharing
- AI & Agent
- Language & localization
- Self-hosting
- API & integrations
- Applications & cover letters
- Other / unsure
validations:
required:true
- type:input
id:environment
attributes:
label:Environment
description:Include your operating system and browser. For self-hosted installations, also include the deployment method.
placeholder:Firefox 143 on Ubuntu 26.04, deployed with Docker Compose
validations:
required:true
- type:textarea
id:summary
attributes:
label:Describe the bug you're experiencing
description:A detailed description of what you're experiencing. Please provide as much detail as possible as it will help me diagnose and fix the issue faster.
label:Summary
description:Briefly describe the problem and its impact.
validations:
required:true
- type:textarea
id:reproduction
attributes:
label:Steps to reproduce
description:Provide the smallest reliable sequence that demonstrates the problem.
placeholder:|
1. Open ...
2. Select ...
3. Observe ...
validations:
required:true
- type:textarea
id:expected
attributes:
label:Expected behavior
validations:
required:true
- type:textarea
id:actual
attributes:
label:Actual behavior
validations:
required:true
- type:dropdown
id:template
attributes:
label:What template are you using?
description:Leave blank if the issue applies to all templates, or is not template-specific.
multiple:false
label:Template
description:Leave blank when the problem is not template-specific.
options:
- Azurill
- Bronzor
- Chikorita
- Ditto
- Ditgar
- Ditto
- Gengar
- Glalie
- Kakuna
- Lapras
- Leafish
- Meowth
- Onyx
- Pikachu
- Rhyhorn
- Scizor
validations:
required:false
- type:textarea
id:logs
attributes:
label:Anything else?
description:|
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required:false
label:Logs and screenshots
description:Add relevant logs, screenshots, or a minimal reproduction. Remove secrets and personal resume data first.
description:Suggest an feature or idea that you would like to see in Reactive Resume
description:Propose an actionable improvement to Reactive Resume
title:"[Feature] <title>"
labels:[enhancement, v5, needs triage]
assignees:"AmruthPillai"
labels:["enhancement","status: needs triage"]
assignees:[]
body:
- type:checkboxes
attributes:
label:Is there an existing issue for this feature?
description:Please search to see if an issue already exists for the feature you requested.
label:Existing issue
description:Search open and closed issues before submitting a new proposal.
options:
- label:Yes,I have searched the existing issues and it doesn't exist.
- label:I searched the existing issues and could not find a matching proposal.
required:true
- type:textarea
- type:dropdown
id:variant
attributes:
label:Feature Description
description:A detailed description of the feature you would like to see in Reactive Resume. Please provide as much detail as possible as it will help me implement the feature faster.
label:Product variant
description:Choose the primary environment for this proposal.
options:
- Cloud
- Self-hosted
validations:
required:true
- type:dropdown
id:area
attributes:
label:Area
description:Choose the part of Reactive Resume most closely related to the proposal.
options:
- Resume builder & data
- Templates, preview & export
- Accounts & sharing
- AI & Agent
- Language & localization
- Self-hosting
- API & integrations
- Applications & cover letters
- Other / unsure
validations:
required:true
- type:textarea
id:problem
attributes:
label:Problem
description:What user problem or limitation should Reactive Resume solve?
validations:
required:true
- type:textarea
id:outcome
attributes:
label:Desired outcome
description:Describe the behavior you want without prescribing an implementation.
validations:
required:true
- type:textarea
id:alternatives
attributes:
label:Alternatives considered
description:Describe current workarounds or alternatives. Write "None" if there are none.
validations:
required:true
- type:textarea
id:scope
attributes:
label:Proposed scope
description:Explain what should be included and what can remain out of scope.
validations:
required:true
- type:textarea
id:context
attributes:
label:Additional context
description:Add examples, mockups, or related issues when useful. Remove personal resume data first.
- Base: current `origin/main` at dispatch, `2a4a1583b` (`fix(pdf): restore Gengar skill rating order (#3473)`)
- Product decisions applied: Q1 explicit Show heading toggle; Q2 Move-to continuations default visible; Q3 visual omission in preview/PDF/DOCX with accessible outline labels retained.
- No `.codegraph/` directory exists in this worktree, so CodeGraph was skipped after the required presence check.
- Intent discovery ran before edits; no matching local skill was available for this schema/PDF/DOCX/web change.
## Implementation
- Added backward-compatible `showHeading` section data for summary, built-ins, and custom sections. `parseResumeData` normalizes absent legacy values to `true`; explicit `false` survives round trips.
- Added heading toggles to built-in/summary and custom section menus. Toggle mutations use `useUpdateResumeData`, preserving undo/autosave/save/reload behavior; legacy absent values are treated as visible. Existing lock fieldset remains authoritative.
- Move-to-created custom sections explicitly set `showHeading: true`, independent of source heading state or copied title.
-`SectionShell` omits complete heading/icon/decoration output when disabled in both icon and no-icon branches. Empty titles still resolve localized defaults.
- DOCX section renderers omit visible heading paragraphs for summary, built-in, and custom sections while retaining content. Screen-reader mirror continues to expose section labels regardless of visual setting.
- Added characterization for Semantic CSS `section[id="..."] section-heading { display: none; }`; body remains while heading is omitted.
Plan 23 steps 1–3 were evaluated from `origin/main` at `368858a56` (Plan 21 / PR #3477 merged). Widow/orphan UI and authored-page continuation guidance remain deferred from this execution, and Semantic CSS was not changed.
## Durable diagnostic matrix
`packages/pdf/src/templates/shared/item-pagination.test.tsx` renders physical PDF pages and checks numbered tokens exactly once for:
- an item that fits remaining space;
- an item that fits a full page but not the remaining space;
- an oversized item taller than one page;
- a two-line paragraph at a boundary;
- nested bullets; and
- built-in plus custom items in an Azurill sidebar/main-column overflow fixture.
The fixture also keeps authored `metadata.layout.pages` separate from renderer-generated physical pages.
The current deterministic baseline is: fit remainder = 1 physical page; full-page-but-not-remainder = 3 pages with sampled tokens on pages 2/2/3; oversized = 5 pages with sampled tokens on pages 1/3/5; two-line boundary = 2 pages; nested bullets = 1 page; Azurill built-in/custom/sidebar = 5 pages with sampled tokens on pages 1/4/5/5/1. Page numbers here are 1-based; every token still appears exactly once.
## Concrete blocker
React PDF's only available item-level keep-together primitive is `View wrap={false}`. A durable renderer fixture with 180 paragraph-like child views shows that a non-wrapping item cannot safely fall back when its content exceeds one page: the renderer omits the oversized tail instead of splitting it. Applying the same prop to shared `SectionItem` would therefore violate the lossless token requirement; no item schema flag or menu control was added.
Do not estimate item height from HTML length, persist physical pages, alter existing Semantic CSS, or claim #3350 complete. A future implementation needs renderer-supported conditional keep-together behavior or an actual measured two-pass fallback that preserves every token.
## Verification
-`pnpm --filter @reactive-resume/pdf exec vitest run src/templates/shared/item-pagination.test.tsx`: 7 tests passed.
- No production source or schema changes made after the unsafe fallback was reproduced.
- Undo/persistence/lock UI coverage is intentionally absent because no item control was shipped; add it only when safe fallback exists.
Revision: `2a4a1583b` (`origin/main` at run start)
Scope: Phase A, steps 1–2 only. No resolver, runtime behavior, or remote-source behavior changed.
## Drift and authority
- Worktree started clean and `HEAD` matched `origin/main`; `git diff origin/main...HEAD` was empty.
- Current catalog is `packages/fonts/src/webfontlist.json`. Its web font records point at both Google Fonts static assets and jsDelivr assets; “Google blocked” is not an offline proof.
- Browser preview is `apps/web/src/components/typography/font-display.tsx` and calls `FontFace.load()` against each catalog preview URL.
- Browser PDF preview/download is `apps/web/src/features/resume/export/pdf-document.tsx` → `@reactive-resume/pdf/browser`; registration is `packages/pdf/src/hooks/use-register-fonts.ts`.
- Server PDF is `apps/server/src/http/resume-pdf.ts` → `createResumePdfDownload`; Playwright browser routing cannot observe that process’s outbound font fetches.
- The issue is open and unmodified. PR #3455 is the approved planning PR; its plan/decision log grants execution of this bounded diagnostic and manifest evidence.
## Deterministic fixture
`tests/e2e/fixtures/offline-fonts.ts` seeds one disposable resume after sample creation. It writes the same text into basics and summary, hides the picture, selects IBM Plex Serif 400/700 for body and heading, and marks the row public for the server-PDF surface.
The exact markers are versioned as `offline-font-scripts-v1`:
| Marker | Script or coverage |
| --- | --- |
| `Latin punctuation • — “quotes” €` | Latin plus General Punctuation and currency |
| `简体中文` | Han / Simplified Chinese |
| `العربية` | Arabic |
| `עברית` | Hebrew |
| `ไทย` | Thai |
| `Emoji 🚀` | Emoji |
`tests/e2e/specs/offline-fonts.spec.ts` is opt-in (`OFFLINE_FONT_DIAGNOSTIC=1`) so the normal PR E2E suite does not become network-dependent. Each surface creates a new browser context with persisted auth state, disabled service workers, and no prior browser cache. Every non-same-origin request is aborted and recorded as `{ hostname, path }`; query strings, fragments, headers, bodies, tokens, and full URLs never enter diagnostic output. Reports are attached as JSON and emitted with the same sanitized shape.
The four surfaces are separate tests:
1. Font picker preview opens Typography → Font Family and waits for lazy `FontFace` preview loads.
2. Builder PDF preview navigates to the builder, captures the active PDF canvas, and measures marker-local raster crops.
3. Browser PDF download uses the Export dialog, rasterizes the downloaded PDF, and measures marker-local crops when generation succeeds.
4. Server PDF calls the public PDF endpoint and records text-layer marker presence when generation succeeds.
Builder/browser-PDF reports keep PDF text extraction as a separate `textLayerMarkers` signal; it does not prove visible glyph outlines. Raster evidence attaches a rendered PNG and per-marker crop metrics, failing for blank or tofu-like visible crops. Blocked browser font requests classify browser surfaces as `network-error`. The server report deliberately says `server-outbound-requests-unobservable-from-playwright`; its cold-network gate remains unresolved because server outbound capture and verifiable restart identity require external host-level controls.
## Run protocol and cold-cache boundary
Build and database setup follow `tests/e2e/README.md`. Run each surface in a separately restarted production server process so module-level PDF font registration state cannot leak between controls:
pnpm exec playwright test tests/e2e/specs/offline-fonts.spec.ts --grep "picker preview"
```
Stop and restart the production server before repeating the command with `builder PDF`, `browser PDF`, and `server PDF` grep patterns. The environment used for this change had no built `apps/server/dist` or `apps/web/dist`, no running PostgreSQL instance, and no production server to restart, so the cold E2E matrix was not run. This is an explicit infrastructure blocker, not a pass claim. The test records `serverRestartFlag` only as caller input and labels it non-proof; it does not claim a completed cold-network gate.
The current Playwright route guard cannot impose host-level egress denial on Node.js running the server. A genuinely cold server test therefore needs a separately restarted server plus host-level egress capture/deny (for example, a controlled network namespace or an approved outbound proxy). Do not infer server network behavior from an empty browser request list.
## Administrator-hosted manifest proposal
This is a proposal, not an asset download. It intentionally contains only the primary family and glyph fallbacks required by the fixture and current PDF fallback map, not the full catalog.
"Noto Sans Arabic":{"normal":{"400":"noto-sans-arabic/400.ttf","700":"noto-sans-arabic/700.ttf"},"italic":"reuse-normal","preview":"noto-sans-arabic/preview.ttf"},
"Noto Sans Hebrew":{"normal":{"400":"noto-sans-hebrew/400.ttf","700":"noto-sans-hebrew/700.ttf"},"italic":"reuse-normal","preview":"noto-sans-hebrew/preview.ttf"},
"Noto Sans Thai":{"normal":{"400":"noto-sans-thai/400.ttf","700":"noto-sans-thai/700.ttf"},"italic":"reuse-normal","preview":"noto-sans-thai/preview.ttf"},
### Candidate source, license, script, and size evidence
Sizes are `Content-Length` bytes from a HEAD request to the exact current catalog assets on 2026-09-06. Responses reported `Content-Encoding: gzip`; these are compressed transfer-size estimates, not a claim about the eventual on-disk representation. Preview paths are aliases to the selected 400 face and add no extra bytes when stored once. Primary sources: [IBM Plex LICENSE.txt](https://github.com/IBM/plex/blob/master/LICENSE.txt), [Noto core LICENSE](https://github.com/notofonts/noto-fonts/blob/main/LICENSE), [Noto CJK Sans LICENSE](https://github.com/notofonts/noto-cjk/blob/main/Sans/LICENSE), and [Noto Emoji font LICENSE](https://github.com/googlefonts/noto-emoji/blob/main/fonts/LICENSE).
| Family | Style/weights in proposal | Current catalog source version | License | Script/fallback role | Gzip bytes (selected files) | Build owner; runtime owners |
| --- | --- | --- | --- | --- | ---: | --- |
| IBM Plex Serif | normal 400/700; italic 400/700 | `fonts.gstatic.com/s/ibmplexserif/v20` | OFL 1.1, Reserved Font Name `Plex` | Primary serif; Latin and punctuation stack | 294,717 | `packages/fonts`; `apps/web` FontDisplay; `packages/pdf` registration |
| IBM Plex Sans | normal 400/700; italic 400/700 | `fonts.gstatic.com/s/ibmplexsans/v23` | OFL 1.1, Reserved Font Name `Plex` | Primary sans | 435,469 | `packages/fonts`; `apps/web` FontDisplay; `packages/pdf` registration |
| Noto Sans | normal 400/700; italic 400/700 | `fonts.gstatic.com/s/notosans/v42` | OFL 1.1 | Sans punctuation fallback | 1,236,259 | `packages/fonts`; `packages/pdf` fallback registration |
| Noto Sans SC | normal 400/700; italic reuses normal | `fonts.gstatic.com/s/notosanssc/v40` | OFL 1.1 (Noto CJK) | Simplified Han; CJK fallback | 12,766,416 | `packages/fonts`; `packages/pdf` CJK fallback |
| Noto Serif SC | normal 400/700; italic reuses normal | `fonts.gstatic.com/s/notoserifsc/v35` | OFL 1.1 (Noto CJK) | Simplified Han serif fallback | 17,350,185 | `packages/fonts`; `packages/pdf` CJK fallback |
| Noto Sans TC | normal 400/700; italic reuses normal | `fonts.gstatic.com/s/notosanstc/v39` | OFL 1.1 (Noto CJK) | Traditional Han fallback | 8,628,278 | `packages/fonts`; `packages/pdf` CJK fallback |
| Noto Serif TC | normal 400/700; italic reuses normal | `fonts.gstatic.com/s/notoseriftc/v36` | OFL 1.1 (Noto CJK) | Traditional Han serif fallback | 11,804,923 | `packages/fonts`; `packages/pdf` CJK fallback |
| Noto Sans JP | normal 400/700; italic reuses normal | `fonts.gstatic.com/s/notosansjp/v56` | OFL 1.1 (Noto CJK) | Kana and Japanese Han fallback | 6,383,035 | `packages/fonts`; `packages/pdf` CJK fallback |
| Noto Serif JP | normal 400/700; italic reuses normal | `fonts.gstatic.com/s/notoserifjp/v33` | OFL 1.1 (Noto CJK) | Kana and Japanese Han serif fallback | 8,685,862 | `packages/fonts`; `packages/pdf` CJK fallback |
| Noto Sans KR | normal 400/700; italic reuses normal | `fonts.gstatic.com/s/notosanskr/v39` | OFL 1.1 (Noto CJK) | Hangul and Korean Han fallback | 6,102,888 | `packages/fonts`; `packages/pdf` CJK fallback |
| Noto Serif KR | normal 400/700; italic reuses normal | `fonts.gstatic.com/s/notoserifkr/v31` | OFL 1.1 (Noto CJK) | Hangul and Korean Han serif fallback | 11,113,442 | `packages/fonts`; `packages/pdf` CJK fallback |
| Noto Sans Arabic | normal 400/700; italic reuses normal | `fonts.gstatic.com/s/notosansarabic/v33` | OFL 1.1 | Arabic sans fallback | 178,455 | `packages/fonts`; `packages/pdf` script fallback |
| Noto Naskh Arabic | normal 400/700; italic reuses normal | `fonts.gstatic.com/s/notonaskharabic/v44` | OFL 1.1 | Arabic serif fallback | 190,924 | `packages/fonts`; `packages/pdf` script fallback |
| Noto Sans Hebrew | normal 400/700; italic reuses normal | `fonts.gstatic.com/s/notosanshebrew/v50` | OFL 1.1 | Hebrew fallback for both serif/sans slots | 55,707 | `packages/fonts`; `packages/pdf` script fallback |
| Noto Sans Thai | normal 400/700; italic reuses normal | `fonts.gstatic.com/s/notosansthai/v29` | OFL 1.1 | Thai fallback for both serif/sans slots | 55,173 | `packages/fonts`; `packages/pdf` script fallback |
| Noto Emoji | normal 400/700; italic reuses normal | `fonts.gstatic.com/s/notoemoji/v62` | OFL 1.1 for font files; assets/tools have separate licenses | Emoji outline fallback; verify renderer support | 1,153,847 | `packages/fonts`; `packages/pdf` script fallback |
Estimated transfer size for all rows and listed styles: **87,490,700 bytes (~83.44 MiB)**. This confirms why a full-catalog bundle is out of scope. A later implementation should subset by declared glyph requirements or make the administrator choose fallback families; it must not silently fetch another CDN.
### Source and license obligations
- Pin an upstream release/commit and retain source attribution plus the complete applicable license with hosted assets. Do not use mutable `@latest` URLs as runtime sources.
- IBM Plex’s license has Reserved Font Name `Plex`; modified/subset outputs must follow OFL naming requirements.
- Noto core, Noto CJK, and Noto Emoji font files are OFL 1.1, but Noto Emoji documents separate Apache/public-domain treatment for tools and flag image assets. Bundle only font files unless those other assets are intentionally needed and separately attributed.
- License checks are build-owner responsibility (`packages/fonts`/tooling); runtime owners (`apps/web` and `packages/pdf`) consume only the validated manifest.
### Missing-family and missing-asset behavior
Local mode must resolve only same-origin administrator-hosted manifest paths. If imported resume data names an unavailable family, show an actionable missing-family error naming the family and required local asset; apply a configured local fallback only when the administrator explicitly supplied one. If a required weight/style/fallback asset is absent, fail the affected preview/export with an actionable diagnostic containing family/style/weight and local path. Never retry Google Fonts, jsDelivr, or any other remote URL in local mode.
Standard PDF families (Helvetica, Courier, Times-Roman) remain file-free. They do not prove that a document containing punctuation, CJK, Arabic, Hebrew, Thai, or emoji is network-free; the script fallback rows remain required.
## Verification record
Completed read-only checks before handoff:
- CodeGraph exploration of font catalog, picker preview, browser PDF, server PDF, and existing fallback tests.
-`pnpm dlx @tanstack/intent@latest list`: no matching local intent skill for this work.
- E2E diagnostic execution: blocked by missing build outputs and unavailable PostgreSQL/server; no success claim made.
-`pnpm exec turbo boundaries`: passed on fresh rerun (Turbo 2.10.12, 1108 files, no issues).
The implementation intentionally stops at diagnostic fixtures and manifest evidence. Shared source resolution, asset hosting, local-mode configuration, and production behavior remain Phase A step 3+ work.
- Removed multilingual markers from the fixture headline. Each marker now exists only in its dedicated summary paragraph.
- Added pure marker-location helpers. Marker lookup joins PDF text items, supports markers split across items, rejects duplicate occurrences, and rejects non-whitespace neighbors that could contaminate a local crop.
- Raster measurement still scans with antialiasing padding but counts ink only inside the marker box, preventing neighboring glyphs from making blank or tofu-like evidence pass.
- Browser PDF download now separates download errors from post-download evidence errors. A received download with failed rasterization is reported as `unresolved-raster-evidence-error` and fails the opt-in test rather than passing as a generic download error.
- Added focused pure tests covering duplicate, split, neighboring, blank, and tofu-like cases.
- Removed trailing spaces from `plan-27a-remediation.md`.
## Verification
-`pnpm exec vitest run tests/e2e/fixtures/offline-font-markers.test.ts` — 5/5 passed.
-`pnpm exec playwright test tests/e2e/specs/offline-fonts.spec.ts --list` — 4 tests collected.
-`git diff --check` — passed after remediation-document whitespace cleanup.
Full diagnostic E2E remains opt-in and was not run in this focused round. Server outbound request capture and verifiable restart identity remain explicit external host-level blockers; no production resolver changes were made.
Scope: concrete findings from `.orchestration/plan-27a-independent-review.md` only.
## Remediated findings
- Builder PDF preview and browser PDF download now produce raster evidence. The fixture stores each multilingual marker in its own summary paragraph, allowing the diagnostic to locate marker-local PDF text boxes and measure only those raster crops. Reports attach a rendered PNG plus per-marker `inkPixels`, trimmed dimensions, and status. Blank and tofu-like crops fail assertions; no whole-page snapshot is used.
- PDF text extraction is reported separately as `textLayerMarkers`. It is not described or asserted as proof of visible glyph outlines.
- Server PDF output remains text-extraction-only and is explicitly classified as `serverGateStatus: unresolved-external-host-level-blocker`. `serverRestartFlag` is caller input, not restart proof. Browser Playwright routing is not used to infer server egress, and no production resolver or instrumentation behavior was added.
-`.orchestration/plan-27a-diagnostic.md` now records a fresh boundaries pass and the corrected `87,490,700 bytes (~83.44 MiB)` arithmetic.
- Diagnostic remains opt-in through `OFFLINE_FONT_DIAGNOSTIC=1`; normal CI behavior remains unchanged. Request logs stay sanitized to hostname and pathname.
-`pnpm --filter @reactive-resume/pdf exec vitest run src/hooks/use-register-fonts.test.ts` — 35/35 passed.
-`pnpm exec turbo boundaries` — passed on fresh rerun (Turbo 2.10.12, 1108 files, no issues).
Full diagnostic E2E remains unrun because this environment lacks production build output, PostgreSQL, and a production server. Server cold-network capture and verifiable restart identity remain external host-level blockers by design; this remediation does not claim that gate is complete.
-`item-pagination.test.tsx` now derives complete numbered-token inventories for each generated fixture and asserts every token exactly once. Sampled token-to-physical-page placement checks remain separate.
- Pagination fixtures snapshot `metadata.layout.pages` before rendering and assert authored layout pages are unchanged afterward. Overflow fixtures also assert physical PDF page count exceeds authored page count.
- Unsafe `wrap={false}` renderer coverage remains diagnostic-only; no item controls, schema flags, or runtime behavior were added.
- Run `pnpm dlx @tanstack/intent@latest list` from the workspace root to see available local skills.
- If a listed skill matches the task, run `pnpm dlx @tanstack/intent@latest load <package>#<skill>` before changing files.
- Use the loaded `SKILL.md` guidance while making the change.
- Monorepos: when working across packages, run the skill check from the workspace root and prefer the local skill for the package being changed.
- Multiple matches: prefer the most specific local skill for the package or concern you are changing; load additional skills only when the task spans multiple packages or concerns.
<!-- intent-skills:end -->
### Overview
<!-- caveman-begin -->
Respond terse like smart caveman. All technical substance stay. Only fluff die.
Reactive Resume is a pnpm monorepo (Turborepo) with two deployable apps: `apps/web` (TanStack Start / React 19 / Vite) and `apps/server` (Hono / Node.js). The production Docker image runs a single Node.js process on port 3000, with `apps/server` mounting the API/auth/MCP/static routes and serving the built web app.
- Not: "Sure! I'd be happy to help you with that."
- Yes: "Bug in auth middleware. Fix:"
Internal packages are source-consumed through `package.json` export maps that point at `src` files. Do not assume package-local `dist` output exists unless a package explicitly adds it.
Auto-Clarity: drop caveman for security warnings, irreversible actions, user confused. Resume after.
- **Node.js 24** (matches Dockerfile `ARG NODE_VERSION=24`). Use `nvm install 24 && nvm use 24` if needed.
- **Docker** is required to run PostgreSQL. Start it with `sudo dockerd &` if the daemon isn't running.
- **pnpm 11.1.2** is managed via corepack (`corepack enable`).
Boundaries: code/commits/PRs written normal.
<!-- caveman-end -->
### Codebase map
## Agent skills
-`apps/web` owns TanStack Start routes, Vite config, PWA setup, oRPC browser client wiring, web features, and the resume builder UI.
-`apps/server` owns the production Hono app, route composition, auth/RPC/MCP/OpenAPI handlers, static uploads, schema JSON, web-dist fallback serving, and startup checks.
-`packages/api` contains oRPC routers, DTOs, rate limiting, and feature-owned API modules under `packages/api/src/features/*`. The router export at `@reactive-resume/api/routers` aggregates those feature routers for `/api/rpc`.
-`packages/auth` contains Better Auth config, auth helper functions, and exported auth types. The server auth adapter in `apps/server/src/http/auth.ts` delegates to `auth.handler`.
-`packages/db` contains the Drizzle client and schema. Migration files live at the repo root in `migrations/`.
-`packages/env` defines server environment validation and auto-loads the root `.env` for app/server code.
-`packages/schema` contains Zod schemas and typed resume/page/template models.
-`packages/pdf` contains the React PDF document, font registration, shared template primitives, template implementations, and browser/server PDF generation adapters. PDF.js viewer UI stays in `apps/web`.
-`packages/resume` contains pure resume-domain behavior such as JSON Patch helpers and social-network icon mapping.
-`packages/ui` contains shared Base UI/shadcn-style components and hooks.
-`packages/fonts`, `packages/email`, `packages/import`, `packages/ai`, `packages/utils`, and `packages/config` provide focused support surfaces. Prefer their existing exports over adding cross-package shortcuts.
- Development-only scripts live in `tooling/`, not under `packages/`, so packages only contain code bundled by the app/runtime.
-Issues and specs: GitHub Issues for `reactive-resume/reactive-resume`. See `docs/agents/issue-tracker.md`.
### Web app conventions
## Overview
- Routes are file-based under `apps/web/src/routes`. Do not hand-edit `apps/web/src/routeTree.gen.ts`; it is generated by TanStack Router tooling.
- Server-owned HTTP behavior lives in `apps/server/src/{http,rpc,mcp,openapi,static,startup}`. Keep API/RPC/auth/MCP/static route wiring in `apps/server`, not in web routes.
-`apps/web/src/router.tsx` initializes router context with `queryClient`, `orpc`, `theme`, `locale`, `session`, and `flags`. Reuse route context where possible instead of refetching these concerns ad hoc.
- The builder shell lives under `apps/web/src/routes/builder/$resumeId`. The nested preview route is client-only (`ssr: false`), while the public resume route `apps/web/src/routes/$username/$slug.tsx` uses `ssr: "data-only"`.
- Browser-only resume preview code lives under `apps/web/src/features/resume/preview`, and public resume PDF viewer code lives under `apps/web/src/features/resume/public`. Keep PDF.js/canvas/browser APIs out of SSR paths and out of `packages/pdf`.
- The isomorphic oRPC client is in `apps/web/src/libs/orpc/client.ts`; server calls use an in-process router client and browser calls use `/api/rpc` with credentials included.
Reactive Resume is a pnpm monorepo (Turborepo) with two deployable apps: `apps/web` (React 19 SPA with TanStack Router and Vite) and `apps/server` (Hono / Node.js). The production Docker image runs a single Node.js process on port 3000; `apps/server` mounts the API/auth/MCP/static routes and serves the built web app.
### Package and feature boundaries
Internal packages are source-consumed through `package.json` export maps pointing at `src` files. Do not assume package-local `dist` output exists unless a package explicitly adds it.
- Workspace dependencies must go through package names and package export maps. Do not import another workspace's `src` tree through repository paths, `@reactive-resume/*/src/*`, or TypeScript path aliases.
-`turbo boundaries` is the executable package-boundary check. Workspace-level `turbo.json` files declare coarse tags:
-`app:web` for the TanStack Start app.
-`app:server` and `runtime:server` for the Node/Hono process.
-`runtime:server` for server-only packages such as API/auth/db/env/email/MCP.
-`runtime:browser` for browser-only shared UI.
-`runtime:universal` for environment-neutral domain packages.
-`role:domain`, `role:infra`, `role:adapter`, `role:api`, `role:rendering`, and `role:tooling` for package intent.
- Browser/server runtime-specific code should live behind explicit export subpaths such as `@reactive-resume/pdf/browser`, `@reactive-resume/pdf/server`, or `@reactive-resume/env/server`. Keep root exports environment-neutral unless the package is intentionally server-only.
- Wildcard exports are allowed only for leaf libraries whose public surface is intentionally file-like, currently `@reactive-resume/ui/components/*`, `@reactive-resume/ui/hooks/*`, and schema resume model files. Prefer explicit exports for packages that own runtime behavior.
- Add new API procedures and business logic inside the owning `packages/api/src/features/*` module. Keep route wiring, DTO usage, helpers, and services colocated by feature/capability, then expose only intentional public surfaces through `packages/api/package.json`. Prefer `protectedProcedure` from `packages/api/src/context.ts` for authenticated procedures.
- Add database columns/tables in `packages/db/src/schema/*`, then generate root-level migrations with `dotenvx run -f .env.local -- pnpm db:generate`.
- Add or change resume data shape in `packages/schema/src/resume/*` first, then update API DTOs, importers, PDF rendering, and web forms that consume that shape.
- Add or rename templates in all relevant places: `packages/schema/src/templates.ts`, `packages/pdf/src/templates/index.ts`, template source under `packages/pdf/src/templates/<name>/`, and static previews under `apps/web/public/templates/{jpg,pdf}`.
- Resume JSON Patch behavior belongs in `@reactive-resume/resume/patch`; do not put resume-domain helpers in `@reactive-resume/utils`.
- DOCX export behavior belongs in `@reactive-resume/docx`; do not put DOCX builders in `@reactive-resume/utils`.
- Shared PDF section filtering lives in `packages/pdf/src/templates/shared/filtering.ts`. Keep template-specific visual exceptions in the owning template directory unless multiple templates need the same behavior.
-`packages/pdf/src/hooks/use-register-fonts.ts` owns React PDF font registration, standard PDF font handling, CJK fallback stacks, and global hyphenation behavior.
- PDF generation helpers live behind `@reactive-resume/pdf/browser` and `@reactive-resume/pdf/server`; locale-specific section-title resolution stays in the caller.
- MCP implementation belongs in `@reactive-resume/mcp`; app packages must not import MCP implementation from another app's source tree.
-`packages/utils` has narrowly exported helpers. If another package needs a utility, add an explicit export path instead of importing private files.
Prerequisites: **Node.js 24** (pinned in `.nvmrc`; matches Dockerfile `ARG NODE_VERSION=24`), **pnpm 12.3.4** (pinned by `packageManager` in the root `package.json`; pnpm self-manages to it, so any recent pnpm can bootstrap — the Dockerfile's `ARG PNPM_VERSION` only picks the base image) ([install guide](https://pnpm.io/installation)), and **Docker** for PostgreSQL (`sudo dockerd &` if the daemon isn't running).
Placement decision tree:
## Ownership map
1. If the change is a web route, route loader, or user-facing web workflow, start in `apps/web/src/routes` or `apps/web/src/features`.
2. If the change is a server HTTP route/adapter, startup check, static handler, MCP transport, or OpenAPI/well-known handler, start in `apps/server/src`.
3. If it is authenticated API behavior, put the contract and implementation in the owning `packages/api/src/features/*` module.
4. If it is pure resume data behavior with no DB, HTTP, DOM, or PDF renderer dependency, put it in `packages/resume`.
5. If it renders resume PDFs, put shared React PDF/template code in `packages/pdf`; put PDF.js viewer/canvas UI in `apps/web/src/features/resume`.
6. If it creates DOCX exports, put it in `packages/docx`.
7. If it exposes MCP tools/prompts/resources, put it in `packages/mcp`.
8. If it is a generic UI primitive or hook, put it in `packages/ui`; if it is workflow-specific UI, keep it in the owning web feature.
9. If it is a narrow cross-cutting helper, add an explicit `packages/utils` export only after checking that no domain package is a better owner.
Where each concern lives, and where new code for it goes:
### Database
| Area | Owner |
|------|-------|
| Web routes, loaders, user-facing workflows | `apps/web/src/routes`, `apps/web/src/features` (file-based; never hand-edit `routeTree.gen.ts`) |
| Authenticated API contracts + business logic | `packages/api/src/features/*` (oRPC routers, DTOs, rate limiting; aggregated at `@reactive-resume/api/routers` for `/api/rpc`) |
| Generic UI primitives + hooks | `packages/ui` (Base UI/shadcn-style); workflow-specific UI stays in the owning web feature |
| Focused support surfaces | `packages/fonts`, `packages/email`, `packages/import`, `packages/ai`, `packages/utils`, `packages/config` — prefer existing exports over cross-package shortcuts |
| Dev-only scripts | `tooling/`, not `packages/`, so packages only hold runtime-bundled code |
PostgreSQL runs via Docker Compose:
Narrow cross-cutting helpers go in `packages/utils` only after checking no domain package is a better owner. Specifically: resume JSON Patch behavior belongs in `@reactive-resume/resume/patch` and DOCX builders in `@reactive-resume/docx` — not in `@reactive-resume/utils`.
## Web app conventions
-`apps/web/src/router.tsx` initializes router context with `queryClient`, `orpc`, `theme`, `locale`, `session`, and `flags`. Reuse route context instead of refetching these ad hoc.
- The web app is a client-rendered SPA. `apps/server` serves `index.html` and injects page metadata (OpenGraph, canonical, JSON-LD) in `apps/server/src/static/web.ts`; there is no React SSR.
- Builder shell: `apps/web/src/routes/builder/$resumeId`. Public resume route: `apps/web/src/routes/$username/$slug.tsx`.
- Browser-only preview code: `apps/web/src/features/resume/preview`. Public PDF viewer: `apps/web/src/features/resume/public`. Keep PDF.js/canvas code in these features, not in `packages/pdf`.
- oRPC client: `apps/web/src/libs/orpc/client.ts` calls `/api/rpc` with credentials included. `apps/web/src/libs/orpc/fetch.ts` stages large request bodies through Blob on Vercel.
- For React components with explicit props, use a named props type (e.g. `type FooProps = {...}` with `function Foo(props: FooProps)`) rather than inline object annotations, especially with more than one field or with generics.
## Package boundaries
`pnpm exec turbo boundaries` is the executable check. Rules:
- Workspace deps go through package names and export maps. Never import another workspace's `src` tree via repo paths, `@reactive-resume/*/src/*`, or TS path aliases.
- Wildcard exports are allowed only for leaf libraries with an intentionally file-like surface — currently `@reactive-resume/ui/components/*`, `@reactive-resume/ui/hooks/*`, and schema resume model files. Prefer explicit exports for packages owning runtime behavior.
- Prefer `protectedProcedure` from `packages/api/src/context.ts` for authenticated procedures. Expose only intentional public surfaces through `packages/api/package.json`.
- Shared PDF section filtering: `packages/pdf/src/templates/shared/filtering.ts`. Template-specific visual exceptions stay in the owning template directory unless multiple templates need the behavior. `packages/pdf/src/hooks/use-register-fonts.ts` owns font registration, standard PDF fonts, CJK fallback stacks, and global hyphenation.
Multi-place changes:
- **Resume data shape**: `packages/schema/src/resume/*` first, then API DTOs, importers, PDF rendering, and web forms consuming it.
- **New template**: `packages/schema/src/templates.ts`, `packages/pdf/src/templates/index.ts`, source under `packages/pdf/src/templates/<name>/`, and previews under `apps/web/public/templates/{jpg,pdf}`.
- **New DB column/table**: `packages/db/src/schema/*`, then `dotenvx run -f .env.local -- pnpm db:generate`.
- **New env var**: `packages/env/src/server.ts`**and** the `globalEnv` array in `turbo.json`. Turborepo 2.x strict env mode filters out unlisted vars, so the variable will be `undefined` in child processes at runtime even when correctly set in the OS/container environment.
## Environment and database
Copy `.env.example` to `.env.local`. Three required vars: `APP_URL` (default `http://localhost:3000`), `DATABASE_URL` (default `postgresql://postgres:postgres@localhost:5432/postgres`), `AUTH_SECRET` (any non-empty string).
- **S3/SeaweedFS optional.** If `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, and `S3_BUCKET` are all set, the app uses S3-compatible storage. `.env.example` ships SeaweedFS defaults, so either start the `seaweedfs` compose service or comment those vars out to use local filesystem storage under `<workspace>/data`. `LOCAL_STORAGE_PATH` must be absolute when set.
- **`REDIS_URL` and `ENCRYPTION_SECRET`** are optional for core resume flows but both required for saved AI providers and the authenticated `/agent` workspace. Host-run dev uses `REDIS_URL=redis://localhost:6379`; the container-run app uses `redis://redis:6379`.
- **`drizzle-kit` (used by `pnpm db:migrate`) reads `DATABASE_URL` from `process.env` directly** — it does not auto-load `.env`. Run migration commands through `dotenvx`.
- The production server auto-runs migrations at startup before serving traffic, so manual `pnpm db:migrate` is mainly for first setup, migration debugging, or applying migrations without starting the app.
## Commands
Prefix dev servers and migration commands with `dotenvx run -f .env.local --`. Tests, typechecks, linters, boundary checks, and `pnpm build` do not need it; if one fails on a missing env var, rerun it with the prefix.
```
sudo docker compose -f compose.dev.yml up -d postgres
sudo docker compose -f compose.dev.yml up -d postgres # DB only
sudo docker compose -f compose.dev.yml up -d postgres redis seaweedfs seaweedfs_create_bucket # full infra
dotenvx run -f .env.local -- pnpm dev # port 3000 (dev:web for web only)
dotenvx run -f .env.local -- pnpm db:generate # db:migrate to apply
The dev default connection string is `postgresql://postgres:postgres@localhost:5432/postgres`.
Prefer package filters over repo-wide runs, e.g. `pnpm --filter web typecheck`, `pnpm --filter @reactive-resume/pdf test`. Vitest paths are package-relative under `pnpm --filter <package> test -- <path>`.
**Important**: `drizzle-kit` (used by `pnpm db:migrate`) reads `DATABASE_URL` from `process.env` directly — it does **not** auto-load the `.env` file. Run migration commands through `dotenvx`, for example `dotenvx run -f .env.local -- pnpm db:migrate`, so `DATABASE_URL` is present in the process environment.
## Gotchas
The production server runs migrations during startup before serving traffic. Manual `pnpm db:migrate` is mainly for first setup, migration debugging, or applying migrations without starting the app.
### Environment
Copy `.env.example` to `.env`. The three required variables are:
S3/SeaweedFS is optional. If `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, and `S3_BUCKET` are all set, the app uses S3-compatible storage. The checked-in `.env.example` sets SeaweedFS defaults, so either start the `seaweedfs` compose service too or comment out those S3 vars to use local filesystem storage under `<workspace>/data`. `LOCAL_STORAGE_PATH` must be absolute when set.
When running dev servers or migration commands, prefix the command with `dotenvx run -f .env.local --`. For example: `dotenvx run -f .env.local -- pnpm dev`. Tests, typechecks, linters, boundary checks, and `pnpm build` do not need this prefix by default. If one of those commands fails because a specific environment variable is required, rerun it with the `dotenvx run -f .env.local --` prefix.
### Common commands
| Task | Command |
|------|---------|
| Install deps | `pnpm install` |
| Start Postgres only | `sudo docker compose -f compose.dev.yml up -d postgres` |
| Run migrations | `dotenvx run -f .env.local -- pnpm db:migrate` |
| Dev server | `dotenvx run -f .env.local -- pnpm dev` (starts on port 3000) |
| Web dev server only | `dotenvx run -f .env.local -- pnpm dev:web` |
| Lint/format | `pnpm check` (Biome) |
| Boundary check | `pnpm exec turbo boundaries` |
| Tests | `pnpm test` (Vitest) |
| Build | `pnpm build` |
| Typecheck | `pnpm typecheck` |
For focused validation, prefer package filters before repo-wide commands, for example:
```
pnpm --filter web typecheck
pnpm --filter @reactive-resume/pdf test
pnpm --filter @reactive-resume/api test
pnpm exec turbo boundaries
```
Vitest test paths are package-relative when running through `pnpm --filter <package> test -- <path>`.
### Gotchas
- The server startup path auto-runs migrations before serving traffic, so `pnpm db:migrate` is mainly needed for first-time setup, migration debugging, or applying migrations without starting the app.
- Email sending requires SMTP config; without it, emails are logged to console. This is fine for dev — the app still functions, but email verification links appear in server logs.
- The `lefthook.yml` pre-commit hook runs `biome check` on staged files. Run `pnpm check` before committing to avoid hook failures.
-`pnpm check` is write-capable (`biome check --write --unsafe .`). Call that out when using it, and use narrower Biome commands if you need a non-mutating inspection.
- Biome uses tabs, double quotes, line width 120, organized import groups, and sorted Tailwind classes for `clsx`, `cva`, and `cn`.
- Most packages use `tsgo --noEmit` for typechecking and `vitest run --passWithNoTests` for tests.
- There may be unrelated local edits in the worktree. Inspect `git status --short` first and avoid reverting files you did not touch.
- **New env vars require a `turbo.json` entry.** Turborepo 2.x runs in strict env mode by default — it filters out env vars that are not listed in `globalEnv` (or task-level `env`/`passThroughEnv`). Any new environment variable added to `packages/env/src/server.ts` must also be added to the `globalEnv` array in `turbo.json`, or the variable will be `undefined` inside child processes at runtime even if it is correctly set in the OS/container environment.
- Email sending needs SMTP config; without it emails are logged to console. Dev still works — verification links appear in server logs.
-`lefthook.yml` pre-commit runs `biome check` on staged files. Run `pnpm check` before committing.
-`pnpm check` is write-capable. Call that out when using it, and use narrower Biome commands for a non-mutating inspection.
- Biome: tabs, double quotes, line width 120, organized import groups, sorted Tailwind classes for `clsx`, `cva`, `cn`.
- Most packages typecheck with `tsgo --noEmit` and test with `vitest run --passWithNoTests`.
- There may be unrelated local edits in the worktree. Check `git status --short` first; do not revert files you did not touch.
@@ -226,21 +226,24 @@ A custom Tailwind token `--aspect-page: 210 / 297` enforces A4 paper proportions
## Animation
Animations use the Motion library (formerly Framer Motion) and follow a consistent choreography pattern:
Motion exists to explain a change, confirm an action, or soften a jump. This is a tool people use for hours, so it stays crisp: short, precise, rarely decorative.
**Entrance animations** use a fade-up reveal: elements start at `opacity: 0, y: 20-100` and animate to `opacity: 1, y: 0`. The hero section uses a larger y-offset (100px) for dramatic effect; subsequent sections use 20px for subtlety.
**Frequency decides first.** Keyboard-initiated actions (the command palette, ⌘B sidebar toggle, zoom shortcuts, keyboard-opened menus via Base UI's `data-instant`) do not animate. Things hit tens of times a day (list rows, tooltips after the first, context menus) get opacity-only or no motion. Dialogs, sheets and toasts get a standard transition. Only rare moments (marketing pages, first load) get more.
**Timing principles:**
-**Base duration:** 0.35s–0.6s for standard section reveals, 0.45s for hero elements, up to 1.1s for the hero video entrance.
-**Stagger pattern:** Sequential delays within a group, typically 0.1s–0.15s apart (hero: 0.55s, 0.7s, 0.82s, 0.95s). For grids, use `index * 0.03`–`0.1` for per-item stagger.
- **Easing:** `easeOut` for entrances (elements decelerate into position). `easeInOut` for looping/ambient animations.
- **Performance:** Apply `will-change-[transform,opacity]` on animated elements and `will-change-transform` on continuously animated elements.
**Tokens.** Never hand-type a curve.
-CSS: `ease-out-strong` / `var(--ease-out-strong)` (`cubic-bezier(0.23, 1, 0.32, 1)`) for anything entering, exiting or responding; `ease-in-out-strong` for on-screen movement nobody is waiting on (ambient loops, carousels); `ease-drawer` for sheets. Never `ease-in`.
-Motion (JS): `EASE_OUT_STRONG` from `apps/web/src/libs/motion.ts`.
**Hover/interaction animations**are quick (0.2s) and subtle — small scale bumps (`scale: 1.01`), slight y-offsets (`y: -2`), and `active:translate-y-px` for button press.
**Durations.**Press feedback 100–160ms, tooltips/popovers/menus 150ms in and 100ms out, dialogs 200ms in and 150ms out, sheets 300ms. App UI stays under 300ms; marketing reveals may run 0.5–0.9s. Exits are faster than entrances.
**Ambient animations** loop infinitely with `easeInOut` — the scroll indicator bounces gently (`y: [0, 5, 0]` over 1.5s).
**Mechanics.**
- Popups use interruptible CSS transitions on Base UI's `data-starting-style` / `data-ending-style`, scale from `0.95` (never `0`) and grow from `origin-(--transform-origin)`. Modals stay centred.
- Animate `transform`/`translate`/`scale` and `opacity` only. No `transition-all`, no permanent `will-change` (Motion promotes layers while it animates).
- Presses use `active:scale-[0.97]`. `Button` already has it; don't wrap it in Motion hover/tap wrappers.
- Lists use `AnimatePresence initial={false}` so items animate when added or removed, not every time the list mounts.
- Continuous loops (marquees, drifting spotlights) are CSS keyframes, so they run off the main thread.
**Reduced motion:**All CSS transitions and animations collapse to `0.01ms` duration and single iteration when `prefers-reduced-motion: reduce` is active. Motion library animations should also respect this preference.
**Reduced motion:**`MotionConfig reducedMotion="user"` disables Motion transforms, and CSS transitions and animations collapse to `0.01ms` — except `animate-spin`, which keeps spinning so loading never looks frozen. Values driven by `useSpring`/`useMotionValue` bypass `MotionConfig`, so check `useReducedMotion()` there.
> **Repository moved:** Reactive Resume now lives at **[`reactive-resume/reactive-resume`](https://github.com/reactive-resume/reactive-resume)** on GitHub.
> **Docker Hub stays at `amruthpillai/reactive-resume`.** GHCR builds now publish to `ghcr.io/reactive-resume/reactive-resume`.
> Verified image tags: `latest`, `v5`, `v5.3`, and `v5.3.0` (AMD64 and ARM64). The current version was rebuilt and production redeployed for this rename; no new GitHub release or version bump was made. See [migration details](https://github.com/reactive-resume/reactive-resume/issues/3503).
> GitHub Sponsors and Open Collective funding links remain unchanged.
Reactive Resume makes building resumes straightforward. Pick a template, fill in your details, and export to PDF—no account required for basic use. For those who want more control, the entire application can be self-hosted on your own infrastructure.
Pick a template, fill in your details, and export to PDF. Basic use needs no account. If you want more control, you can run the whole application on your own infrastructure.
Built with privacy as a core principle, Reactive Resume gives you complete ownership of your data. The codebase is fully open-source under the MIT license, with no tracking, no ads, and no hidden costs.
You own your data. The codebase is opensource under the MIT license, with no tracking, no ads, and no hidden costs.
## Features
**Resume Building**
-Real-time preview as you type
-Live preview as you type
- Multiple export formats (PDF, JSON, DOCX)
- Drag-and-drop section ordering
- Custom sections for any content type
- Rich text editor with formatting support
- Rich text editor
**Templates**
-Professionally designed templates
- A4 and Letter size support
-15 templates to choose from
- A4 and Letter page sizes
- Customizable colors, fonts, and spacing
-Custom CSS for advanced styling
-Structured Style Rules for section and text styling
**Privacy & Control**
@@ -61,7 +67,7 @@ Built with privacy as a core principle, Reactive Resume gives you complete owner
- Multi-language support
- Share resumes via unique links
- Import from JSON Resume format
- Dark mode support
- Dark mode
- Passkey and two-factor authentication
## Templates
@@ -143,7 +149,7 @@ The quickest way to run Reactive Resume locally:
| [Getting Started](https://docs.rxresu.me/getting-started) | First-time setup and basic usage |
| [Self-Hosting](https://docs.rxresu.me/self-hosting/docker) | Deploy on your own server |
| [Development Setup](https://docs.rxresu.me/contributing/development) | Local development environment |
| [Project Architecture](https://docs.rxresu.me/contributing/architecture) | Codebase structure and patterns |
| [Development setup](https://docs.rxresu.me/contributing/development) | Local development environment |
| [Project architecture](https://docs.rxresu.me/contributing/architecture) | Codebase structure and patterns |
| [Exporting Your Resume](https://docs.rxresu.me/guides/exporting-your-resume) | PDF and JSON export options |
## Self-Hosting
Reactive Resume can be self-hosted using Docker. The stack includes:
Reactive Resume supports Docker and Vercel Hobby.
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Freactive-resume%2Freactive-resume&project-name=reactive-resume&repository-name=reactive-resume&env=AUTH_SECRET%2CENCRYPTION_SECRET&envDescription=Generate+two+independent+secrets+with+openssl+rand+-hex+32.+Keep+these+values+across+deployments.&envLink=https%3A%2F%2Fdocs.rxresu.me%2Fself-hosting%2Fvercel&stores=%5B%7B%22type%22%3A%22integration%22%2C%22protocol%22%3A%22storage%22%2C%22integrationSlug%22%3A%22neon%22%2C%22productSlug%22%3A%22neon%22%7D%2C%7B%22type%22%3A%22integration%22%2C%22protocol%22%3A%22storage%22%2C%22integrationSlug%22%3A%22upstash%22%2C%22productSlug%22%3A%22upstash-kv%22%7D%2C%7B%22type%22%3A%22blob%22%2C%22access%22%3A%22private%22%7D%5D)
Vercel provisions Neon PostgreSQL, private Blob storage, and Upstash Redis through its deployment wizard. Supply two persistent secrets, then deploy. See the [Vercel guide](docs/self-hosting/vercel.mdx) for setup, limits, and optional SMTP/OAuth configuration.
For Docker, the stack includes:
- **PostgreSQL** — Database for storing user data and resumes
- **SeaweedFS** (optional) — S3-compatible storage for file uploads
> **From v5.1.0 onwards** — PDF generation now runs entirely client-side via `@react-pdf/renderer`. New deployments no longer require Browserless, Chromium, or any external print service as a dependency. The `PRINTER_*` and `BROWSERLESS_*` environment variables are no longer read and can be removed from your `.env`.
> **From v5.1.0 onwards** — PDF generation runs entirely client-side via `@react-pdf/renderer`. New deployments no longer need Browserless, Chromium, or any external print service. The `PRINTER_*` and `BROWSERLESS_*` environment variables are no longer read and can be removed from your `.env`.
Pull the latest image from Docker Hub or GitHub Container Registry:
@@ -199,14 +209,14 @@ Pull the latest image from Docker Hub or GitHub Container Registry:
See the [self-hosting guide](https://docs.rxresu.me/self-hosting/docker) for complete instructions.
## Support
Reactive Resume is and always will be free and open-source. If it has helped you land a job or saved you time, please consider supporting continued development:
Reactive Resume is and always will be free and opensource. If it has helped you land a job or saved you time, please consider supporting continued development:
"Reactive Resume is a free and open-source resume builder. Use this MCP server to interact with your resume using an LLM of your choice.",
icons:[
{
src:"https://rxresu.me/icon/light.svg",
mimeType:"image/svg+xml",
theme:"light",
},
{
src:"https://rxresu.me/icon/dark.svg",
mimeType:"image/svg+xml",
theme:"dark",
},
],
},
{
instructions:[
"You are connected to Reactive Resume over MCP.",
"Authenticate with OAuth (recommended) or an API key (`x-api-key`).",
`Discover resume IDs with \`${MCP_TOOL_NAME.listResumes}\` (not \`resources/list\`).`,
`List distinct tags with \`${MCP_TOOL_NAME.listResumeTags}\`.`,
`Read schema at \`resume://_meta/schema\`; read resume JSON via \`resume://{id}\` or \`${MCP_TOOL_NAME.getResume}\`.`,
`Apply body edits with JSON Patch through \`${MCP_TOOL_NAME.patchResume}\`.`,
`Change name, slug, tags, or public visibility with \`${MCP_TOOL_NAME.updateResume}\` (returns canonical share URL; anonymous access only when \`isPublic\` is true; passwords are managed in the web app only).`,
`Import full ResumeData JSON with \`${MCP_TOOL_NAME.importResume}\`; read saved AI analysis with \`${MCP_TOOL_NAME.getResumeAnalysis}\`.`,
"Authenticate with OAuth (recommended) or an API key (`x-api-key`).",
`Discover resume IDs with \`${MCP_TOOL_NAME.listResumes}\` (not \`resources/list\`).`,
`List distinct tags with \`${MCP_TOOL_NAME.listResumeTags}\`.`,
`Read schema at \`resume://_meta/schema\`; read resume JSON via \`resume://{id}\` or \`${MCP_TOOL_NAME.getResume}\`.`,
`Apply body edits with JSON Patch through \`${MCP_TOOL_NAME.patchResume}\`.`,
`Change name, slug, tags, or public visibility with \`${MCP_TOOL_NAME.updateResume}\` (returns canonical share URL; anonymous access only when \`isPublic\` is true; passwords are managed in the web app only).`,
`Create short-lived authenticated PDF download URLs with \`${MCP_TOOL_NAME.downloadResumePdf}\`; set target to \`cover-letter\` to export a visible cover letter separately.`,
`Import full ResumeData JSON with \`${MCP_TOOL_NAME.importResume}\`.`,
"Yes. Reactive Resume is free to use, with no hidden costs, premium tiers, or subscription fees. It's open source, and it will stay free.",
},
{
question:"How is my data protected?",
answer:
"Your data is stored securely and never shared with third parties. If you want full control over it, you can self-host Reactive Resume on your own servers.",
},
{
question:"Can I export my resume to PDF?",
answer:"Yes. One click exports your resume to PDF, with your formatting and styling intact.",
},
{
question:"Is Reactive Resume available in multiple languages?",
answer:
"Yes. Pick your language on the settings page, or with the language switcher in the top right corner. If your language is missing, or the existing translation could be better, you can contribute to the translations on Crowdin.",
},
{
question:"What makes Reactive Resume different from other resume builders?",
answer:
"Reactive Resume is open source, private, and free. It shows no ads, doesn't track what you do, and doesn't lock features behind a paywall.",
},
{
question:"How do I share my resume?",
answer:"Share it with a public URL, put a password on that URL, or download the PDF and send it yourself.",
<metaname="description"content="Reactive Resume is a free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.">
<!-- Keep under ~120 characters so Google's mobile SERP snippet is not truncated at 3 lines. -->
<metaname="description"content="Free, open-source resume builder. Create, update, and share a professional resume in minutes — no ads, no paywall.">
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.