Commit Graph
348 Commits
Author SHA1 Message Date
Amruth Pillai e368e5955d chore: remove unused UI parts, dependencies and turbo tasks, and share escapeHtml 2026-09-29 10:21:20 +02:00
Amruth Pillai 0f6e08a922 refactor: drop dead render props, duplicate template tables and unused import helpers 2026-09-29 10:19:02 +02:00
Amruth Pillai a42cacc057 refactor(api): share cookie, signature and AI error handling instead of copying them 2026-09-29 10:15:56 +02:00
Amruth Pillai 8951f45a3a refactor(api): drop hand-rolled helpers the platform or one caller already covers 2026-09-29 10:13:19 +02:00
Amruth Pillai 2506509538 feat: make cover letters documents of their own
Resumes no longer hold cover-letter sections. A migration saves every
letter a resume carried as a letter linked to that resume's details and
design, hands a resume's only letter to its only letter-less application,
then removes the sections from resumes and their layouts. rollback.sql
puts them back.

Every resume write on the server moves any letter it still carries into
a saved letter in the same transaction, so stale tabs, older files, API
clients and restored versions keep working without duplicating letters.

The resume editor no longer adds or imports letters, the resume download
drops its Cover letter tab, and resume PDF downloads (API, signed links,
MCP) no longer take a cover-letter target. copy_embedded_cover_letter and
POST /cover-letters/from-resume are removed.
2026-09-29 09:30:07 +02:00
Amruth Pillai 48793e04be chore(pdf): drop what the Forme port left unused 2026-09-29 08:40:22 +02:00
Amruth Pillai b775cbc15c fix(ui): let callers name an icon's slot again 2026-09-29 08:35:34 +02:00
Amruth Pillai cbc76b03b1 refactor!: contract the redesign's legacy fields
The expand steps of the redesign kept older app versions and API clients
working. This removes those compatibility paths:

- Entry date text (period/date) is written from the structured dates on
  every save; an edit to the text alone is overwritten.
- application.archived is dropped; archived rows close first. CSV import
  still reads the flag from older exports.
- resume_version.label is dropped; versions are named by kind and name.
- rejected is no longer accepted as a stage; remaining rows and history
  close first. CSV import still maps it to closed.

BREAKING CHANGE: older app versions and API clients that read or write
archived, version labels, the rejected stage or date text alone no
longer work. migrations/20260929063245_contract_redesign_legacy_fields
has a rollback.sql that restores the dropped columns.
2026-09-29 08:34:27 +02:00
Amruth Pillai 29ab0326b2 fix(pdf): mirror the award title and date right to left 2026-09-29 08:27:24 +02:00
Amruth Pillai 3405200cf4 fix(pdf): keep a list marker with its item's first line 2026-09-29 08:25:06 +02:00
Amruth Pillai 5dc67c3bd4 fix(web): restyle the custom CSS editor in the app's inks and show its toolbar icons 2026-09-29 08:17:12 +02:00
Amruth Pillai 49422e98f2 feat: give a letter its own type, colors and page 2026-09-29 08:14:37 +02:00
Amruth Pillai 17d25c5ffa test(pdf): letters print the sender's header in every template 2026-09-29 08:08:12 +02:00
Amruth Pillai 6c2bc74f92 feat: pin the exported PDF's findings to the page in Check
The file-level check places each finding's evidence on its page: the
rule's own box, or the line holding its snippet. Check keeps the report
with the resume it ran on and, while the resume is unchanged, outlines
each finding on the page with a pin that opens the full report.
2026-09-29 08:05:41 +02:00
Amruth Pillai 098df60230 feat: choose which side the sidebar sits on
Two-column templates take a sidebarSide from the layout (left or right).
Without one, each template keeps its own side and right-to-left pages
mirror it. The PDF templates reverse only their columns, header band
and sidebar background, and the DOCX export follows the same choice.
Design's Sidebar panel offers Left and Right.
2026-09-29 08:00:33 +02:00
Amruth Pillai 5f7ed15a5b feat(web): dock the text toolbar above the phone keyboard
On phones the formatting toolbar follows the visual viewport and sits on
the keyboard, 44px tall, with Improve and Done. Done closes the keyboard.
Larger screens keep the toolbar inside the field.
2026-09-29 07:59:59 +02:00
Amruth Pillai 722f5fa14c fix(pdf): close the remaining gaps in the forme port
Work around more Forme 0.25 layout defects in the converter: row-reverse
laid out as row, min and max widths ignored along a row, text dropping
view children, empty text taking a line, overflowing optimal line
breaks, a text in a splitting row wrecking later pages, page breaks on
row items, four equal border sides, percentage corner radii, pictures
under their shadow, and imported table rows. Rotation is left out while
Forme misplaces rotated boxes; the stylesheet editor warns about it and
about percentage padding.

The page map rebuilds blocks Forme drops from its layout, the browser
renderer rejects documents whose fonts can't load so callers fall back
to the server PDF, and every render gets fresh font entries. Glalie's
sidebar is one band at the combined tint.

Tests are re-baselined where they encoded react-pdf metrics, and engine
limits (list marker keep-together, RTL line order, characters above
U+FFFF, percentage padding) are expected failures. The plan logs the
migration in section 13.
2026-09-29 07:37:25 +02:00
Amruth Pillai c0bf1aebaf feat(pdf): remove react-pdf and move every consumer to forme
The web preview, downloads, template gallery, server export and public
PDF render through Forme. react-pdf, react-pdf-html, the react-pdf
hyphenation package, the Phosphor react-pdf icons and the four patches
are gone. Hyphenation now follows the page language for every language
Forme has patterns for.

Semantic CSS keeps its language; declarations Forme can't draw raise an
ENGINE_UNSUPPORTED warning in the editor. The page map rebuilds blocks
Forme leaves out of its layout when they break across pages, and a
render that misplaces a box is repeated with nested rows kept whole.

Tests move to a small shim with the react-pdf calls they were written
against; tests of react-pdf internals are dropped. Forme limits are
recorded as expected failures (RTL line order, characters above
U+FFFF).
2026-09-29 07:11:40 +02:00
Amruth Pillai bc28441b66 feat(pdf): render resumes with the forme engine
Templates keep their react-pdf-style primitives. A small React renderer
records what they draw, and the result is converted to a Forme document:
styles, fonts (with ligatures disabled for text extraction), Phosphor
icons as SVG paths, pictures fitted by object-fit, and the page map from
source locations. Workarounds for Forme 0.25 layout defects live in the
converter: opaque translucent colours, border insets, row splitting,
column gaps across pages, absolute positioning and repeated backgrounds.

Consumers, the Semantic CSS adapter and the old react-pdf dependencies
follow in later commits.
2026-09-29 06:22:38 +02:00
Amruth Pillai dcddc5639a fix(ui): 44px touch targets and readable descriptions on tinted rows
On coarse pointers, buttons, switches, checkboxes and tabs get an invisible hit area of at least 44x44 centred on them, without changing how they look. A checked switch row's description steps up to ink-2, since ink-3 falls short of 4.5:1 on the accent tint. Adds contrastOnWhite to the colour utilities.
2026-09-29 01:22:19 +02:00
Amruth Pillai 55a3284360 refactor(web): move the app chrome from phosphor to material symbols
Auth pages, dialogs, the command palette, the user menu, Applications, the rich-text toolbar, the stylesheet editor and the error screens use the Material Symbols set instead of Phosphor; the icon subset gains the 35 glyphs they need. Phosphor stays for brand logos, the landing page and the icons printed on resumes. Also removes the form field and test mock that only wrapped the old rich input.
2026-09-29 01:01:16 +02:00
Amruth Pillai f8767c32f1 refactor(ui): retire the shadcn colour aliases
Every class that used the old names (muted, primary, card, border, input and the rest) now uses the Desk & Paper token it resolved to, with text colours on their text tokens (accent-text, danger-text, ink-3). The compatibility block in the theme is gone.
2026-09-29 00:56:59 +02:00
Amruth Pillai 3a69dfc4d5 refactor(ui): remove the agent chat primitives and sidebar
Attachment, bubble, empty, marker, message, message scroller, questionnaire, resizable and sidebar had no users after the assistant moved into the editor, and neither did react-resizable-panels or @shadcn/react.
2026-09-29 00:55:50 +02:00
Amruth Pillai 55db11aea8 feat(api): include applications in the account export
Export everything downloads documents and applications together, so the account export now returns the user's applications, without the owner id.
2026-09-29 00:33:29 +02:00
Amruth Pillai e1d5b9ba9b feat(web): the assistant beside the document, and Improve in every text field
The assistant opens beside the resume or letter it works on: a third column
at 1280px and wider, in place of the left panel from 1024px, a drawer below
that, and full screen on phones. The bar's button and Mod+J toggle it.

The panel sets up a provider in place, suggests what to ask, streams replies
with Stop and Continue, asks clarifying questions, and shows proposed edits as
change sets with page marks, the outline's "n proposed" pill and the page
caption. Context chips decide whether the next message shares the document and
the posting. Errors keep the message with Retry and Switch model, past
conversations are grouped by document with their outcomes, and the
conversation can be copied as a transcript.

Improve in the rich-text toolbar suggests a stronger verb, a result, a shorter
line or the user's own request for the line holding the caret, and replaces it
only on Replace.

The Agents pages, the builder's assistant sheet and the application copilot
panel are removed; /agent links redirect to the document with the assistant
open. Command palette Ask, Job match's missing terms, Applications' Prepare for
next step and Copy for a job all open the assistant on the right document.
Removes the unused react-resizable-panels and @shadcn/helpers dependencies.
2026-09-29 00:11:16 +02:00
Amruth Pillai 6463a9e9c1 feat(api): an assistant bound to a resume or letter that proposes edits
Threads belong to a resume or a cover letter (agent_threads.cover_letter_id)
and count the edits they proposed and the user accepted. The propose_edits
tool rewrites or adds passages by id, resolved against the document as it is
now, and the edit statuses are stored with the message. read_letter joins
read_resume; apply_resume_patch, approvals, revert and archiving are removed.

Message context lets the user leave the document or the linked posting out of
a message; the posting now includes the application's notes. Redis is
optional: without it replies stream directly.

The proposal core (passages, additions, states) moves to
@reactive-resume/resume/proposals so the server and the web app share it.

Adds ai.improve, which suggests a rewrite of one line (stronger verb, a
result, shorter, or the user's own request) and says when it states
something new.
2026-09-29 00:11:00 +02:00
Amruth Pillai aa9a5113c0 fix(pdf): keep the letter's header when the page tree is built 2026-09-28 23:08:10 +02:00
Amruth Pillai fdad39c632 feat(api): structured letters with live links, versions and a streaming draft 2026-09-28 23:08:04 +02:00
Amruth Pillai 462db6011a feat(web): applications as a grouped list, a rebuilt detail sheet and an add dialog
- The page: CSV import/export behind one icon, Add application, a dismissible
  nudge for the application waiting longest without a reply (10+ days),
  List · Board · Insights · Calendar, search across role, company, contacts
  and tags, and Show closed.
- List (the default): grouped by stage in the order that needs you first,
  with collapsible groups, the next step (warn when overdue), what was sent,
  sorting from the headers and row checkboxes for bulk moves, tags, closing
  and deleting. Phones get two-line rows and no board.
- Board: a column per stage; drops and Move to… show the same toast.
- Detail sheet (480 px, full screen on phones): the stage stepper with Move to
  next, the next step (the next interview or follow-up) with Edit and Add to
  calendar (.ics), what was sent (opening the version sent, read-only, in
  History), Tailor a resume and Write a letter, editable salary and source,
  contacts, tags, autosaved notes and the activity timeline. Close
  application… takes a reason; Delete is in ⋯ and asks first.
- Add dialog: paste a link or posting; its role, company and requirements are
  read into editable fields and the posting is saved with the application.
  Add, or Add and tailor a resume.
- Insights: how far applications get (from their stage history), how many
  heard back and how fast, and tailored against base resumes, above the
  existing charts.
- CSV import shows how columns were matched before saving and lets you
  download the rows it skips; export adds the closed reason.
- The builder opens History on a version from ?version=.
2026-09-28 22:14:20 +02:00
Amruth Pillai ce0b4c606a feat(api): a closed stage with reasons, what was sent, posting requirements and a posting reader
- Applications end in a `closed` stage with a reason (not selected, withdrew,
  accepted another offer, no response). The migration moves `rejected` to
  closed + not selected and archived applications to closed, rewrites
  `rejected` in their stage history, and ships rollback.sql for older
  versions. `archived` stays, deprecated; `rejected` is still accepted as
  input and means closed.
- Once an application with a linked resume reaches Applied, the resume is
  saved as a "sent" version named after the company, and the application keeps
  its id and the resume's Check score then.
- New columns: closed_reason, cover_letter_id (backfilled where exactly one
  letter was written for the application), sent_resume_version_id,
  sent_check_score and requirements.
- applications.ai.parsePosting reads a pasted link or posting. Links are
  fetched on the server: https only, public addresses checked at connect time,
  three redirects, 2 MB and 10 s at most. A page's JobPosting data fills the
  fields without AI; with a provider, the model reads role, company,
  location, salary and requirements.
- MCP application tools take closedReason and coverLetterId.
2026-09-28 21:44:55 +02:00
Amruth Pillai 25306630e7 fix(docx): print pages the way the PDF does for one-column templates and full-width pages 2026-09-28 21:35:39 +02:00
Amruth Pillai 1608d56903 feat(api): passage rewrites in the writing review, and a resume's linked application
- ai.atsReview takes optional passages (id, where, text). A suggestion that
  rewrites one names it in passageId with the whole new passage, which the
  editor offers as a proposal to accept or reject.
- The review prompt fills its placeholders in one pass, so resume text that
  looks like a placeholder is sent as it is.
- resume.getById returns applicationId, the job application a resume was made
  for, so Check's job match can read its posting.
2026-09-28 21:33:18 +02:00
Amruth Pillai e26ce08fc5 feat(resume): check categories, stable issue keys, ignores and a two-column rule
- Every live check has a category (contact details, dates, layout, section
  headings, writing). Reports score the applicable rules, per category too.
- Findings carry a key that uses entry ids instead of array indexes, so it
  survives reordering. Keys in the new metadata.check.ignored set findings
  aside without counting them against the score.
- TWO_COLUMN_LAYOUT flags a two-column template that prints a sidebar.
- Full-width pages print no sidebar, so sections placed only there are now
  reported as never printing instead of passing as main-column content.
- metadata.check also holds job-posting terms hidden as not true; public
  viewers don't receive it.
- ats-pdf exports the job-description matcher, spelling variants and the
  semantics reader for the editor's job match and parser view.
2026-09-28 21:33:11 +02:00
Amruth Pillai daa7d331f3 feat(web): documents home, trash and a new-document dialog
The dashboard becomes Documents: resumes and saved letters in one
library, in a new app shell (a 240px sidebar, an icon rail on tablets and
bottom tabs on phones) with Documents, Applications, Trash (when it has
items), New (N) and the account row.

- Documents: All / Resumes / Letters with counts, search (/) across
  titles, tags and linked applications, sort, grid or list (remembered on
  the device), tag chips once tags exist, cards with the real first page,
  "Resume · Edited 2h ago", the linked application, lock and "New" badges,
  and the first-run screen. A file dropped anywhere on the page imports.
- One menu on cards, rows and right-click/long-press: Open, Rename
  (inline), Duplicate, Copy for a job… (resumes) or Link to application…
  (letters), Tags…, Lock editing, and Move to Trash with Undo.
- Trash lists days left, with Restore and Delete now (asks once).
- New: import a file in three labelled steps with the result and flagged
  fields (resumes, and saved letters' JSON), copy a resume for a job,
  start blank (named after its headline until renamed), a new letter, or
  a sample resume.

Also:
- /dashboard/resumes and /dashboard/cover-letters redirect to Documents.
- Settings pages sit behind a tab strip until they're regrouped, and the
  account menu gains Settings.
- A letter written inside a resume can be copied to Documents from its
  entry menu (Q3k), replacing the library's copy action.
- The builder's document menu and the letter editor move documents to
  Trash instead of deleting them.
- Dialogs reopened right after closing start fresh.
- Removed: the resume and letter libraries and the old create and import
  dialogs.
2026-09-28 20:38:54 +02:00
Amruth Pillai d4406c729b feat(api): name imports after their person and keep locked documents' details
An imported resume is named from its content (the person's name, else the
headline) instead of a random name. Renaming, tagging or linking a locked
document is refused, as moving it to Trash already was.
2026-09-28 20:38:32 +02:00
Amruth Pillai a568f64b43 feat(api): one documents library with Trash, job links and automatic names
Schema: resume gains application_id (the job a copy was made for),
trashed_at and auto_name; cover_letter gains tags, is_locked and
trashed_at.

A new documents router treats resumes and saved letters as one library:
list (live or in Trash, with the linked application), counts, rename,
setTags, setLocked, linkApplication, trash, restore, purge (only from
Trash) and copyForJob, which duplicates a resume, links the copy to the
application and gives the application the copy when it has none.

- resume.delete and coverLetters.delete now move to Trash. Documents stay
  there for 30 days and are purged when their owner next lists documents,
  so no scheduler is needed.
- Documents in Trash are left out of resume and letter lists, and a resume
  in Trash isn't shared: getBySlug and verifyPassword skip it.
- Locked documents can't be moved to Trash; locked letters can't be edited.
- A blank resume created with autoName takes its headline as its name
  until someone renames it.
2026-09-28 19:59:03 +02:00
Amruth Pillai c8df749258 feat(web): one sheet for sharing, downloads and version history
Share & export is one 440px sheet (a full-height bottom sheet on phones)
with Link, Download and History tabs. Share opens Link, the ▾ beside
Download PDF opens Download, the clock opens History, and ⌘⇧S / ⌘⇧E open
their tabs. On desktop the page moves 120px aside so it stays visible.

- Link: the public switch card; the address with a live check (300 ms),
  the reason it can't be used and a suggestion, saved only once valid, so
  the old address stays live; Copy ("Copied" for 2 s); visitor downloads;
  the password (Q3a); Open public page; a QR code; Share via… where the
  platform offers it; and views, downloads and time since the last view
  over 30 days with a daily chart, or the explanation while the link is
  off.
- Download: format cards explained by when to use them, Resume or Cover
  letter (with the resume header option) when the resume has a letter,
  the file name recruiters see (First-Last-Resume by default, unsafe
  characters stripped), a non-blocking note about open Check issues, and
  progress inside the button; a failure offers Try again and PDF.
- History: name the current state; a timeline of Now, sessions, named
  versions, restores and where the document came from. Picking a version
  shows it on the page, read-only and outlined, with its banner; Restore
  saves "Before restore" first. Named versions can be renamed or deleted.

Also:
- Autosaves send this visit's session id, and naming or restoring a
  version saves pending edits first.
- The one-click PDF and every export use the First-Last-Resume name.
- Resume dialogs stop asking for a slug (it's edited in Share), which also
  stops Rename from overwriting a custom slug with one made from the name.
- The public route redirects a renamed resume's old address.
- Removed: the download dialog, the version-history menu and the sharing,
  statistics and export sections.
2026-09-28 19:48:08 +02:00
Amruth Pillai 060750a868 feat(api): version kinds, session autosaves, named versions and slug redirects
Versions:
- resume_version gains kind (created, import, auto, named, before-restore,
  restored, ai, sent), name and session_id. The migration backfills kind
  from the old English labels; label stays for API clients.
- Each editor visit sends a session id with resume.update, and its saves
  keep one autosave version, refreshed at most every two minutes. Calls
  without a session keep the old throttled autosave.
- Creating a resume writes a "created" version and importing an "import"
  one, so history is never empty.
- New procedures: getVersion (for previews), createVersion (name the
  current state), renameVersion and deleteVersion (named versions only).
- Retention replaces the 30-row cap: autosaves, AI edits and restores
  expire after 90 days, pruned when a resume gets a new version (there is
  no scheduler, and the Vercel entry skips startup hooks); a cap of 500
  autosaves per resume bounds storage.

Slugs:
- resume.checkSlug validates ^[a-z0-9]+(-[a-z0-9]+)*$, reports which of the
  user's resumes uses a taken slug, and suggests a free one.
- A changed slug must match the pattern (existing ones keep working until
  changed). The old slug is kept in resume_slug_redirect for 30 days, and
  getBySlug and verifyPassword resolve it; the response carries the current
  slug so clients can redirect.
- create and duplicate make the slug optional and generate a unique one
  from the name; duplicate no longer falls back to the original's slug,
  which always collided.

The migration also applies the pending drop of the redundant
resume_user_id_index removed from the schema in b953435f2.
2026-09-28 19:46:41 +02:00
Amruth Pillai 85352ee11b feat(schema): describe template layouts in one place
Columns, sidebar side, header placement and ATS safety for every template
now live in templateLayouts. The template gallery metadata and the layout
editor read it, and a DOCX test checks the two-column configurations
against it.
2026-09-28 19:04:03 +02:00
Amruth Pillai bda01febd5 feat(schema): structured resume dates with a read-time upgrade and dual write
Dated entries and roles carry dates (start, end, present, and raw when the
text couldn't be read exactly). The parser moves to the schema package and
reports how each date was written; parseResumeData fills dates, infers the
date format from how dates were typed and rewrites the legacy period/date
text from them, so older clients and API readers keep working. API writes
sync against the stored data, so an edit to the text alone is read back
into dates.

getById and getBySlug return upgraded data, ATS date rules and sorting read
dates, JSON Resume and LinkedIn imports map their dates directly, entry
titles may be empty (drafts aren't printed), and the MCP schema resource is
generated live in place of the stale schema.json.
2026-09-28 18:20:18 +02:00
Amruth Pillai f73238ba3b feat(pdf): pass the page map through the browser renderer 2026-09-28 17:17:56 +02:00
Amruth Pillai a887a72d77 feat(ui): add a breakpoint hook, panel icons and CSS-drawn icon glyphs
Icons now draw their ligature from data-icon in a pseudo-element, so icon
names stay out of copied text, find-in-page and text queries. useBreakpoint
reports the design system's mobile, tablet, desktop and wide ranges.
2026-09-28 17:17:54 +02:00
Amruth Pillai 0f934bd849 feat(ui): adopt Desk & Paper tokens, fonts, System theme and restyled primitives
Replace the achromatic shadcn palette with the Desk & Paper tokens (warm
neutrals, one moss accent, danger, warn and info), exposed to Tailwind
under their spec names; the previous names resolve to the new tokens
until every screen is rebuilt. Hard-coded palette classes become semantic
tokens and stage colors follow the spec.

Fonts move to Newsreader, Hanken Grotesk and JetBrains Mono. The theme
gains a System option that follows the operating system live, applied
before first paint, and Base UI now receives the locale's direction.

Primitives follow the component spec: button variants primary,
secondary, ghost, danger and link with a loading state, accent focus
rings on inputs, SwitchRow, semantic badges, segmented and underline
tabs, restyled menus, dialogs, sheets, tooltips and command bar, and a
single bottom-center toast with an Undo action. New: IconButton,
SegmentedControl, RadioGroup and NativeSelect.
2026-09-28 16:29:30 +02:00
Amruth Pillai 4485825dfe feat(ui): add a self-hosted Material Symbols icon subset
Icon renders Material Symbols Rounded at weight 300 from a subset font
that holds only the glyphs listed in packages/ui/src/icons/names.ts.
pnpm icons:build checks every name against Google's codepoints and
regenerates the font; a test keeps the manifest and the list in sync.
Icons are aria-hidden and untranslated, and directional ones mirror in
right-to-left layouts.
2026-09-28 16:29:17 +02:00
Amruth Pillai def4f72169 feat(pdf): map rendered header, section and item boxes
Templates tag the views that own the header, each section and each item
with data-resume-node. ResumeDocument's new onPageMap callback reads React
PDF's layout tree after each render and returns page-relative boxes, so the
editor can link lines on the page to entries in the panel. The attribute
stays on layout nodes and never reaches the PDF.
2026-09-28 16:01:16 +02:00
Amruth Pillai 328bf73cee chore(i18n): drop unregistered ckb-IR catalog
Crowdin shipped an empty Central Kurdish (ckb-IR) catalog with no translated
strings. The locale is not registered in the Lingui config or locale schema,
so remove the file and its PDF section title entry.
2026-09-28 08:59:38 +02:00
FalconSpyClaude Opus 5.5Amruth Pillaiautofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1b78e546e2 feat(applications): schedule interviews and view them on a calendar (#3539)
* feat(applications): schedule interviews and view them on a calendar

Interviews (screening, technical, behavioral, onsite, other) are stored as
"interview" entries on an application's activity timeline, so an application
can have any number of them and they show in its timeline without a
migration. Each interview has a start date-time (timezone-aware), duration,
and optional location and notes.

- schema: interview timeline entry type, interviewDetailsSchema, INTERVIEW_KINDS
- api: addInterview / updateInterview procedures (delete via timeline entry);
  generic timeline updates now only edit text on note entries
- web: Calendar view on the Applications page (month grid, type legend,
  upcoming list grouped by day, schedule button with application picker,
  per-day "+" and "+N more" popover), Interviews section and interview
  dialog in the application detail panel, interview rows in the timeline
  and CSV export
- mcp: add_application_interview / update_application_interview tools
- i18n: extract new strings into all locale catalogs (English fallback)
- docs: MCP tool table, scheduling guide section, resume-builder skill

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(applications): keep interview dates in local time and guard generic timeline edits

- Format the timeline date chip for interview entries in the viewer's local
  timezone so it matches the interview's date-time label; stage and note
  entries still render in UTC.
- Reject interview entries in the generic updateTimelineEntry path. A
  day-granular date edit kept the UTC time of day and could move an interview
  to the wrong local day; callers are pointed to updateInterview
  (update_application_interview). The MCP tool description now says so, and
  a service test covers the rejection.
- Associate each interview dialog label with its control via useId/htmlFor.
- Drop the duplicate onInput handler on the date-time input; onChange covers
  controlled inputs.
- Give the calendar's per-day schedule button an accessible name that
  includes the date, and update the extracted locale catalogs for the new
  message.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-09-28 08:57:25 +02:00
Syed Ali Abbas ZaidiandAmruth Pillai fb756026fa feat(import): add LinkedIn data export as a resume import source (#3538)
* feat(import): add LinkedIn data export as a resume import source

LinkedIn's "Get a copy of your data" export ships a ZIP of per-topic
CSVs (Profile, Positions, Education, Skills, Languages,
Certifications). Reading these directly gives structured data without
needing a connected AI provider, unlike the existing PDF/DOCX import
path.

Also fixes a latent bug found while building this: parseJSONResume
(and the new LinkedIn parser) built their result via a shallow spread
of the shared `defaultResumeData` singleton, so assigning into
`result.sections.x` mutated that singleton in place and leaked section
data into the next unrelated import call in the same process. Both now
start from a structuredClone.

* fix(import): escape LinkedIn text, harden zip parsing and date handling

Move escapeHtml and toHtml from the plain-text importer into html.ts and
use them for LinkedIn summary, position descriptions and education notes,
so CSV text is HTML-escaped and line breaks become paragraphs or bullet
lists instead of collapsing into one run-on paragraph.

Only an empty end date now marks an entry as ongoing. Date cells that are
not "Mon YYYY" are kept verbatim, so a finished role with an unexpected
date format no longer reads as "Present".

Unzip only the six CSVs the importer reads, matched by exact file name,
and reject any of them larger than 5 MB. This avoids inflating the rest
of a complete LinkedIn export in the browser and stops Learning_Profile.csv
being read as Profile.csv.

Map LinkedIn's five language proficiency options onto levels 5 to 1,
falling back to parseLevel for anything else. Drop the literal BOM strip,
which TextDecoder already handles.

The import dialog no longer mentions an AI provider in the loading toast
for LinkedIn imports, which are parsed entirely in the browser.

Add a regression test for the JSON Resume importer leaking section data
through the shared defaultResumeData object, plus LinkedIn tests for HTML
escaping, unrecognised end dates, BOM headers, exact file name matching,
language levels and oversized entries.

---------

Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
2026-09-28 08:57:20 +02:00
Amruth Pillai 6db26e9b5c chore(pdf): regenerate section title catalog for ckb-IR locale 2026-09-28 08:07:15 +02:00
Amruth Pillai 2b31d70a8a chore: update translations 2026-09-28 07:44:52 +02:00