Commit Graph
797 Commits
Author SHA1 Message Date
Syed Ali Abbas ZaidiandAmruth Pillai cce6d64afa feat(import): parse a PDF resume without an AI provider (#3400)
* 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>
2026-09-05 09:53:37 -07:00
Emanuele Tonello ef47baf243 fix(applications): handle cover letter copy failures (#3394)
* fix(applications): handle cover letter copy failures

* style(applications): format clipboard error toast

* refactor(applications): memoize copy draft handler
2026-09-05 09:51:24 -07:00
Emanuele TonelloandAmruth Pillai 1f0844b39c feat(applications): add contact email and phone (#3396)
* feat(applications): add contact email and phone

* fix(applications): validate imported contact emails

* fix(applications): validate all imported contact fields

* fix(applications): preserve data when contact validation fails

* test(applications): complete contact export fixture

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-09-05 09:51:21 -07:00
Diego Vega CentenoandAmruth Pillai 8df1b25550 feat(skills): add inline layout option for skill items (#3358)
* 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>
2026-09-05 09:51:14 -07:00
Santhi PrakashandAmruth Pillai ea3980cba0 fix(components/form): resolve FormControl label target regressions (#3369) (#3387)
* 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>
2026-09-05 09:35:00 -07:00
Amruth Pillai cddb01f037 fix(sharing): record public PDF download statistics (#3414)
* fix(sharing): record public PDF download statistics

* docs(api): explain download statistics access cookie
2026-09-05 09:34:52 -07:00
Amruth Pillai fe9b59e111 fix: restore MCP OAuth registration and authorization (#3421)
* fix: align MCP OAuth provider schema and authorization flow

* test: isolate OpenAPI generation from OAuth initialization

* fix: accept auth routes without a callback query

* fix: require explicit OAuth consent and preserve signed requests

* test: verify OAuth audiences through real MCP initialization

* test(e2e): isolate OAuth token audience validation
2026-09-05 09:33:15 -07:00
github-actions[bot]andCrowdin Bot 0207e5dfcc [skip ci] chore(i18n): sync translations from crowdin (#3441)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-09-05 09:02:30 -07:00
Amruth Pillai fa41150723 fix: preserve photo compression during cropping and show upload limits (#3420)
* fix: preserve photo compression during cropping and show upload limits

* fix: bound cropped image size before upload
2026-09-05 08:59:13 -07:00
Amruth Pillai 779ea5cb4a fix(resume): reject invalid submitted write values (#3413) 2026-09-05 08:51:55 -07:00
Amruth Pillai 5a6f5d4d68 fix: label remaining website and picture inputs (#3424)
* fix: connect remaining website and picture labels to inputs

* test(builder): use realistic website input events
2026-09-05 08:51:22 -07:00
Amruth Pillai 0878b256a9 fix(sharing): use neutral social preview image (#3410)
* fix(sharing): use neutral social preview image

* fix(sharing): use neutral server social preview
2026-09-05 08:51:19 -07:00
Emanuele Tonello bf27792ca0 feat(applications): attach generated cover letter PDFs (#3395)
* feat(applications): attach generated cover letter PDFs

* fix(applications): isolate generated cover letter PDFs
2026-09-05 08:51:03 -07:00
Amruth Pillai cd1c597ff0 fix(server): expose build version in health endpoint (#3404)
* fix(server): expose build version in health endpoint

* fix(server): redact public health failure details
2026-09-05 08:50:17 -07:00
Amruth Pillai a3585a24e0 feat(applications): export filtered applications as CSV (#3426)
* 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
2026-09-05 08:46:52 -07:00
github-actions[bot]andCrowdin Bot a12e32ddac [skip ci] chore(i18n): sync translations from crowdin (#3439)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-09-05 08:46:45 -07:00
Amruth Pillai 7d809da6f8 feat(pdf): add opt-in German hyphenation (#3435) 2026-09-05 08:29:22 -07:00
Amruth Pillai 57fee67d2d fix(pdf): render picture borders and soft shadows (#3427)
* fix(pdf): render picture borders and soft shadows

* fix(pdf): preserve picture padding and bound shadow rendering
2026-09-05 08:29:17 -07:00
github-actions[bot]andCrowdin Bot 47fc16d806 [skip ci] chore(i18n): sync translations from crowdin (#3436)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-09-05 08:28:43 -07:00
Amruth Pillai 1f308af728 feat: add compact resume view with session preferences (#3425)
* feat: add compact resume view and session preferences

* test: match resume cards by literal names
2026-09-05 07:32:14 -07:00
Amruth Pillai 321f2fb43f fix(builder): validate and confirm resume passwords (#3407)
* fix(builder): validate and confirm resume passwords

* test(sharing): exercise password confirmation in browser flow
2026-09-05 07:32:10 -07:00
Amruth Pillai 18b5aa4745 fix(sharing): hide signup link when registration is disabled (#3409) 2026-09-05 07:32:07 -07:00
Amruth Pillai 35cecf9c91 fix(storage): support S3 buckets with object ACLs disabled (#3432) 2026-09-05 07:29:42 -07:00
Amruth Pillai 735e700929 fix(stylesheet): keep color picker state aligned with source (#3431)
* 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
2026-09-05 07:29:39 -07:00
Amruth Pillai 97ccb4ba06 fix(builder): remove sections emptied by item moves (#3417) 2026-09-05 07:29:32 -07:00
Amruth Pillai 2cbb0f63e7 fix(builder): preserve explicit HTTP URLs (#3403) 2026-09-05 07:29:20 -07:00
Amruth Pillai 00a1357deb fix(applications): show saved notes in detail view (#3402) 2026-09-05 13:26:52 +02:00
Amruth Pillai 84645f122b chore: update dependencies 2026-09-04 11:05:22 +02:00
Amruth Pillai f29b92e2fb chore(copy): rewrite marketing, app, and docs copy to read less AI-generated
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.
2026-08-28 22:18:29 +02:00
Amruth Pillai 3fa9de140c chore: update translations 2026-08-27 07:41:45 +00:00
Amruth Pillai c288675b16 Release v5.2.9 (#3382)
* 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.
2026-08-27 03:37:01 +02:00
github-actions[bot]andCrowdin Bot 7fac6f29c0 Sync Translations from Crowdin (#3381)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-08-26 16:25:56 +02:00
Amruth Pillai 3c195dc3f8 Release v5.2.8 (#3375)
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
2026-08-24 21:44:16 +02:00
github-actions[bot]andCrowdin Bot 3221afda9d Sync Translations from Crowdin (#3365)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-08-20 10:38:31 +02:00
Amruth Pillai 8ce899a04b feat(agent): omit resume documents from the copied conversation json 2026-08-20 09:40:57 +02:00
Amruth Pillai 39590eaff6 fix(auth): allow unlinking providers after the session ages past a day (#3364)
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.
2026-08-20 08:20:18 +02:00
Amruth Pillai c8081ac2fe feat(agent): adopt AI SDK v7 — crash safety, context pruning, HITL approvals (#3362)
* 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
2026-08-20 08:06:53 +02:00
github-actions[bot]andCrowdin Bot 128916b9a0 [skip ci] chore(i18n): sync translations from crowdin (#3346)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-08-18 03:50:35 +02:00
Amruth Pillai 24c15cd8cd chore(i18n): fill missing translations
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.
2026-08-17 22:54:57 +02:00
Amruth Pillai 6e3853fe13 chore(i18n): extract catalogs
Picks up the strings added and removed by the toast migration, the account page
rename and the autofill change.
2026-08-17 22:32:34 +02:00
Amruth Pillai 9dc2aade46 chore(deps): update dependencies
Routine version bumps across the workspace. The @react-pdf/textkit patch is
renamed to drop the pinned version so it survives the next bump.
2026-08-17 22:32:33 +02:00
Amruth Pillai eedf2faf02 feat(agent): let the assistant ask clarifying questions
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.
2026-08-17 22:32:33 +02:00
Amruth Pillai da2f1f8244 refactor(applications): autofill from a pasted posting instead of a URL
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.
2026-08-17 22:32:32 +02:00
Amruth Pillai 7a14b0dfbc refactor(settings): rename the danger zone page to account
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.
2026-08-17 22:32:32 +02:00
Amruth Pillai 23ceee2148 refactor(web): move toast call sites to the new component
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.
2026-08-17 22:32:32 +02:00
Amruth Pillai bfdd29f941 test(server): generate the OpenAPI spec once per suite
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.
2026-08-17 22:19:52 +02:00
Amruth Pillai f4bf6887b9 test(stylesheet): complete system variables at the end of the prefix
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.
2026-08-17 22:19:52 +02:00
Amruth Pillai 7c7dbaf21d fix(agent): keep the chat composer focused while streaming
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.
2026-08-17 22:19:52 +02:00
Amruth Pillai 762b999d1e fix(agent): key chat message parts by index
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.
2026-08-17 22:19:52 +02:00
Amruth Pillai 9d0dc36706 feat(seo): render social card metadata for public resumes
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.
2026-08-17 22:19:52 +02:00